origin33/app/html/js/akamai-viewer.unmin.js

8056 lines
231 KiB
JavaScript
Raw Permalink Normal View History

2024-03-19 12:48:13 +00:00
/*! Copyright 2019, Akamai Technologies, Inc. All Rights Reserved. akamai-viewer-0.7.2 */ /**
* Generic animation class with support for dropped frames both optional easing and duration.
*
* Optional duration is useful when the lifetime is defined by another condition than time
* e.g. speed of an animating object, etc.
*
* Dropped frame logic allows to keep using the same updater logic independent from the actual
* rendering. This eases a lot of cases where it might be pretty complex to break down a state
* based on the pure time difference.
*/
(function (global) {
var time = Date.now || function () {
return +new Date();
};
var desiredFrames = 60;
var millisecondsPerSecond = 1000;
var running = {};
var counter = 1;
// Create namespaces
if (!global.core) {
global.core = { effect: {} };
} else if (!core.effect) {
core.effect = {};
}
core.effect.Animate = {
/**
* A requestAnimationFrame wrapper / polyfill.
*
* @param callback {Function} The callback to be invoked before the next repaint.
* @param root {HTMLElement} The root element for the repaint
*/
requestAnimationFrame: function () {
// Check for request animation Frame support
var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
var isNative = !!requestFrame;
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
isNative = false;
}
if (isNative) {
return function (callback, root) {
requestFrame(callback, root);
};
}
var TARGET_FPS = 60;
var requests = {};
var requestCount = 0;
var rafHandle = 1;
var intervalHandle = null;
var lastActive = +new Date();
return function (callback, root) {
var callbackHandle = rafHandle++;
// Store callback
requests[callbackHandle] = callback;
requestCount++;
// Create timeout at first request
if (intervalHandle === null) {
intervalHandle = setInterval(function () {
var time = +new Date();
var currentRequests = requests;
// Reset data structure before executing callbacks
requests = {};
requestCount = 0;
for (var key in currentRequests) {
if (currentRequests.hasOwnProperty(key)) {
currentRequests[key](time);
lastActive = time;
}
}
// Disable the timeout when nothing happens for a certain
// period of time
if (time - lastActive > 2500) {
clearInterval(intervalHandle);
intervalHandle = null;
}
}, 1000 / TARGET_FPS);
}
return callbackHandle;
};
}(),
/**
* Stops the given animation.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation was stopped (aka, was running before)
*/
stop: function (id) {
var cleared = running[id] != null;
if (cleared) {
running[id] = null;
}
return cleared;
},
/**
* Whether the given animation is still running.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation is still running
*/
isRunning: function (id) {
return running[id] != null;
},
/**
* Start the animation.
*
* @param stepCallback {Function} Pointer to function which is executed on every step.
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
* @param verifyCallback {Function} Executed before every animation step.
* Signature of the method should be `function() { return continueWithAnimation; }`
* @param completedCallback {Function}
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
* @param duration {Integer} Milliseconds to run the animation
* @param easingMethod {Function} Pointer to easing function
* Signature of the method should be `function(percent) { return modifiedValue; }`
* @param root {Element ? document.body} Render root, when available. Used for internal
* usage of requestAnimationFrame.
* @return {Integer} Identifier of animation. Can be used to stop it any time.
*/
start: function (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
var start = time();
var lastFrame = start;
var percent = 0;
var dropCounter = 0;
var id = counter++;
if (!root) {
root = document.body;
}
// Compacting running db automatically every few new animations
if (id % 20 === 0) {
var newRunning = {};
for (var usedId in running) {
newRunning[usedId] = true;
}
running = newRunning;
}
// This is the internal step method which is called every few milliseconds
var step = function (virtual) {
// Normalize virtual value
var render = virtual !== true;
// Get current time
var now = time();
// Verification is executed before next animation step
if (!running[id] || verifyCallback && !verifyCallback(id)) {
running[id] = null;
completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false);
return;
}
// For the current rendering to apply let's update omitted steps in memory.
// This is important to bring internal state variables up-to-date with progress in time.
if (render) {
var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
step(true);
dropCounter++;
}
}
// Compute percent value
if (duration) {
percent = (now - start) / duration;
if (percent > 1) {
percent = 1;
}
}
// Execute step callback, then...
var value = easingMethod ? easingMethod(percent) : percent;
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
running[id] = null;
completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, percent === 1 || duration == null);
} else if (render) {
lastFrame = now;
core.effect.Animate.requestAnimationFrame(step, root);
}
};
// Mark as running
running[id] = true;
// Init first step
core.effect.Animate.requestAnimationFrame(step, root);
// Return unique animation ID
return id;
}
};
})(this);
var Scroller;
(function () {
/**
* A pure logic 'component' for 'virtual' scrolling/zooming.
*/
Scroller = function (callback, options) {
this.__callback = callback;
this.options = {
/** Enable scrolling on x-axis */
scrollingX: true,
/** Enable scrolling on y-axis */
scrollingY: true,
/** Enable animations for deceleration, snap back, zooming and scrolling */
animating: true,
/** duration for animations triggered by scrollTo/zoomTo */
animationDuration: 250,
/** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
bouncing: true,
/** Enable locking to the main axis if user moves only slightly on one of them at start */
locking: true,
/** Enable pagination mode (switching between full page content panes) */
paging: false,
/** Enable snapping of content to a configured pixel grid */
snapping: false,
/** Enable zooming of content via API, fingers and mouse wheel */
zooming: false,
/** Minimum zoom level */
minZoom: 0.5,
/** Maximum zoom level */
maxZoom: 3
};
for (var key in options) {
this.options[key] = options[key];
}
};
// Easing Equations (c) 2003 Robert Penner, all rights reserved.
// Open source under the BSD License.
/**
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
**/
var easeOutCubic = function (pos) {
return Math.pow(pos - 1, 3) + 1;
};
/**
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
**/
var easeInOutCubic = function (pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 3);
}
return 0.5 * (Math.pow(pos - 2, 3) + 2);
};
var members = {
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: STATUS
---------------------------------------------------------------------------
*/
/** {Boolean} Whether only a single finger is used in touch handling */
__isSingleTouch: false,
/** {Boolean} Whether a touch event sequence is in progress */
__isTracking: false,
/**
* {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when
* a gesturestart event happens. This has higher priority than dragging.
*/
__isGesturing: false,
/**
* {Boolean} Whether the user has moved by such a distance that we have enabled
* dragging mode. Hint: It's only enabled after some pixels of movement to
* not interrupt with clicks etc.
*/
__isDragging: false,
/**
* {Boolean} Not touching and dragging anymore, and smoothly animating the
* touch sequence using deceleration.
*/
__isDecelerating: false,
/**
* {Boolean} Smoothly animating the currently configured change
*/
__isAnimating: false,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: DIMENSIONS
---------------------------------------------------------------------------
*/
/** {Integer} Available outer left position (from document perspective) */
__clientLeft: 0,
/** {Integer} Available outer top position (from document perspective) */
__clientTop: 0,
/** {Integer} Available outer width */
__clientWidth: 0,
/** {Integer} Available outer height */
__clientHeight: 0,
/** {Integer} Outer width of content */
__contentWidth: 0,
/** {Integer} Outer height of content */
__contentHeight: 0,
/** {Integer} Snapping width for content */
__snapWidth: 100,
/** {Integer} Snapping height for content */
__snapHeight: 100,
/** {Integer} Height to assign to refresh area */
__refreshHeight: null,
/** {Boolean} Whether the refresh process is enabled when the event is released now */
__refreshActive: false,
/** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
__refreshActivate: null,
/** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
__refreshDeactivate: null,
/** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
__refreshStart: null,
/** {Number} Zoom level */
__zoomLevel: 1,
/** {Number} Scroll position on x-axis */
__scrollLeft: 0,
/** {Number} Scroll position on y-axis */
__scrollTop: 0,
/** {Integer} Maximum allowed scroll position on x-axis */
__maxScrollLeft: 0,
/** {Integer} Maximum allowed scroll position on y-axis */
__maxScrollTop: 0,
/* {Number} Scheduled left position (final position when animating) */
__scheduledLeft: 0,
/* {Number} Scheduled top position (final position when animating) */
__scheduledTop: 0,
/* {Number} Scheduled zoom level (final scale when animating) */
__scheduledZoom: 0,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: LAST POSITIONS
---------------------------------------------------------------------------
*/
/** {Number} Left position of finger at start */
__lastTouchLeft: null,
/** {Number} Top position of finger at start */
__lastTouchTop: null,
/** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
__lastTouchMove: null,
/** {Array} List of positions, uses three indexes for each state: left, top, timestamp */
__positions: null,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: DECELERATION SUPPORT
---------------------------------------------------------------------------
*/
/** {Integer} Minimum left scroll position during deceleration */
__minDecelerationScrollLeft: null,
/** {Integer} Minimum top scroll position during deceleration */
__minDecelerationScrollTop: null,
/** {Integer} Maximum left scroll position during deceleration */
__maxDecelerationScrollLeft: null,
/** {Integer} Maximum top scroll position during deceleration */
__maxDecelerationScrollTop: null,
/** {Number} Current factor to modify horizontal scroll position with on every step */
__decelerationVelocityX: null,
/** {Number} Current factor to modify vertical scroll position with on every step */
__decelerationVelocityY: null,
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Configures the dimensions of the client (outer) and content (inner) elements.
* Requires the available space for the outer element and the outer size of the inner element.
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
*
* @param clientWidth {Integer ? null} Inner width of outer element
* @param clientHeight {Integer ? null} Inner height of outer element
* @param contentWidth {Integer ? null} Outer width of inner element
* @param contentHeight {Integer ? null} Outer height of inner element
*/
setDimensions: function (clientWidth, clientHeight, contentWidth, contentHeight) {
var self = this;
// Only update values which are defined
if (clientWidth) {
self.__clientWidth = clientWidth;
}
if (clientHeight) {
self.__clientHeight = clientHeight;
}
if (contentWidth) {
self.__contentWidth = contentWidth;
}
if (contentHeight) {
self.__contentHeight = contentHeight;
}
// Refresh maximums
self.__computeScrollMax();
// Refresh scroll position
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
},
/**
* Sets the client coordinates in relation to the document.
*
* @param left {Integer ? 0} Left position of outer element
* @param top {Integer ? 0} Top position of outer element
*/
setPosition: function (left, top) {
var self = this;
self.__clientLeft = left || 0;
self.__clientTop = top || 0;
},
/**
* Configures the snapping (when snapping is active)
*
* @param width {Integer} Snapping width
* @param height {Integer} Snapping height
*/
setSnapSize: function (width, height) {
var self = this;
self.__snapWidth = width;
self.__snapHeight = height;
},
/**
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
* the official Twitter client.
*
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
*/
activatePullToRefresh: function (height, activateCallback, deactivateCallback, startCallback) {
var self = this;
self.__refreshHeight = height;
self.__refreshActivate = activateCallback;
self.__refreshDeactivate = deactivateCallback;
self.__refreshStart = startCallback;
},
/**
* Signalizes that pull-to-refresh is finished.
*/
finishPullToRefresh: function () {
var self = this;
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
},
/**
* Returns the scroll position and zooming values
*
* @return {Map} `left` and `top` scroll position and `zoom` level
*/
getValues: function () {
var self = this;
return {
left: self.__scrollLeft,
top: self.__scrollTop,
zoom: self.__zoomLevel
};
},
/**
* Returns the maximum scroll values
*
* @return {Map} `left` and `top` maximum scroll values
*/
getScrollMax: function () {
var self = this;
return {
left: self.__maxScrollLeft,
top: self.__maxScrollTop
};
},
/**
* Zooms to the given level. Supports optional animation. Zooms
* the center when no coordinates are given.
*
* @param level {Number} Level to zoom to
* @param animate {Boolean ? false} Whether to use animation
* @param originLeft {Number ? null} Zoom in at given left coordinate
* @param originTop {Number ? null} Zoom in at given top coordinate
*/
zoomTo: function (level, animate, originLeft, originTop) {
var self = this;
if (!self.options.zooming) {
throw new Error("Zooming is not enabled!");
}
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
}
var oldLevel = self.__zoomLevel;
// Normalize input origin to center of viewport if not defined
if (originLeft == null) {
originLeft = self.__clientWidth / 2;
}
if (originTop == null) {
originTop = self.__clientHeight / 2;
}
// Limit level according to configuration
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
// Recompute maximum values while temporary tweaking maximum scroll ranges
self.__computeScrollMax(level);
// Recompute left and top coordinates based on new zoom level
var left = (originLeft + self.__scrollLeft) * level / oldLevel - originLeft;
var top = (originTop + self.__scrollTop) * level / oldLevel - originTop;
// Limit x-axis
if (left > self.__maxScrollLeft) {
left = self.__maxScrollLeft;
} else if (left < 0) {
left = 0;
}
// Limit y-axis
if (top > self.__maxScrollTop) {
top = self.__maxScrollTop;
} else if (top < 0) {
top = 0;
}
// Push values out
self.__publish(left, top, level, animate);
},
/**
* Zooms the content by the given factor.
*
* @param factor {Number} Zoom by given factor
* @param animate {Boolean ? false} Whether to use animation
* @param originLeft {Number ? 0} Zoom in at given left coordinate
* @param originTop {Number ? 0} Zoom in at given top coordinate
*/
zoomBy: function (factor, animate, originLeft, originTop) {
var self = this;
self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop);
},
/**
* Scrolls to the given position. Respect limitations and snapping automatically.
*
* @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
* @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
* @param animate {Boolean?false} Whether the scrolling should happen using an animation
* @param zoom {Number?null} Zoom level to go to
*/
scrollTo: function (left, top, animate, zoom) {
var self = this;
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
}
// Correct coordinates based on new zoom level
if (zoom != null && zoom !== self.__zoomLevel) {
if (!self.options.zooming) {
throw new Error("Zooming is not enabled!");
}
left *= zoom;
top *= zoom;
// Recompute maximum values while temporary tweaking maximum scroll ranges
self.__computeScrollMax(zoom);
} else {
// Keep zoom when not defined
zoom = self.__zoomLevel;
}
if (!self.options.scrollingX) {
left = self.__scrollLeft;
} else {
if (self.options.paging) {
left = Math.round(left / self.__clientWidth) * self.__clientWidth;
} else if (self.options.snapping) {
left = Math.round(left / self.__snapWidth) * self.__snapWidth;
}
}
if (!self.options.scrollingY) {
top = self.__scrollTop;
} else {
if (self.options.paging) {
top = Math.round(top / self.__clientHeight) * self.__clientHeight;
} else if (self.options.snapping) {
top = Math.round(top / self.__snapHeight) * self.__snapHeight;
}
}
// Limit for allowed ranges
left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
top = Math.max(Math.min(self.__maxScrollTop, top), 0);
// Don't animate when no change detected, still call publish to make sure
// that rendered position is really in-sync with internal data
if (left === self.__scrollLeft && top === self.__scrollTop) {
animate = false;
}
// Publish new values
self.__publish(left, top, zoom, animate);
},
/**
* Scroll by the given offset
*
* @param left {Number ? 0} Scroll x-axis by given offset
* @param top {Number ? 0} Scroll x-axis by given offset
* @param animate {Boolean ? false} Whether to animate the given change
*/
scrollBy: function (left, top, animate) {
var self = this;
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
},
/*
---------------------------------------------------------------------------
EVENT CALLBACKS
---------------------------------------------------------------------------
*/
/**
* Mouse wheel handler for zooming support
*/
doMouseZoom: function (wheelDelta, timeStamp, pageX, pageY) {
var self = this;
var change = wheelDelta > 0 ? 0.97 : 1.03;
return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop);
},
/**
* Touch start handler for scrolling support
*/
doTouchStart: function (touches, timeStamp) {
// Array-like check is enough here
if (touches.length == null) {
throw new Error("Invalid touch list: " + touches);
}
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
}
// Stop animation
if (self.__isAnimating) {
core.effect.Animate.stop(self.__isAnimating);
self.__isAnimating = false;
}
// Use center point when dealing with two fingers
var currentTouchLeft, currentTouchTop;
var isSingleTouch = touches.length === 1;
if (isSingleTouch) {
currentTouchLeft = touches[0].pageX;
currentTouchTop = touches[0].pageY;
} else {
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
}
// Store initial positions
self.__initialTouchLeft = currentTouchLeft;
self.__initialTouchTop = currentTouchTop;
// Store current zoom level
self.__zoomLevelStart = self.__zoomLevel;
// Store initial touch positions
self.__lastTouchLeft = currentTouchLeft;
self.__lastTouchTop = currentTouchTop;
// Store initial move time stamp
self.__lastTouchMove = timeStamp;
// Reset initial scale
self.__lastScale = 1;
// Reset locking flags
self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
// Reset tracking flag
self.__isTracking = true;
// Dragging starts directly with two fingers, otherwise lazy with an offset
self.__isDragging = !isSingleTouch;
// Some features are disabled in multi touch scenarios
self.__isSingleTouch = isSingleTouch;
// Clearing data structure
self.__positions = [];
},
/**
* Touch move handler for scrolling support
*/
doTouchMove: function (touches, timeStamp, scale) {
// Array-like check is enough here
if (touches.length == null) {
throw new Error("Invalid touch list: " + touches);
}
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
// Ignore event when tracking is not enabled (event might be outside of element)
if (!self.__isTracking) {
return;
}
var currentTouchLeft, currentTouchTop;
// Compute move based around of center of fingers
if (touches.length === 2) {
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
} else {
currentTouchLeft = touches[0].pageX;
currentTouchTop = touches[0].pageY;
}
var positions = self.__positions;
// Are we already is dragging mode?
if (self.__isDragging) {
// Compute move distance
var moveX = currentTouchLeft - self.__lastTouchLeft;
var moveY = currentTouchTop - self.__lastTouchTop;
// Read previous scroll position and zooming
var scrollLeft = self.__scrollLeft;
var scrollTop = self.__scrollTop;
var level = self.__zoomLevel;
// Work with scaling
if (scale != null && self.options.zooming) {
var oldLevel = level;
// Recompute level based on previous scale and new scale
level = level / self.__lastScale * scale;
// Limit level according to configuration
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
// Only do further compution when change happened
if (oldLevel !== level) {
// Compute relative event position to container
var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
var currentTouchTopRel = currentTouchTop - self.__clientTop;
// Recompute left and top coordinates based on new zoom level
scrollLeft = (currentTouchLeftRel + scrollLeft) * level / oldLevel - currentTouchLeftRel;
scrollTop = (currentTouchTopRel + scrollTop) * level / oldLevel - currentTouchTopRel;
// Recompute max scroll values
self.__computeScrollMax(level);
}
}
if (self.__enableScrollX) {
scrollLeft -= moveX;
var maxScrollLeft = self.__maxScrollLeft;
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
// Slow down on the edges
if (self.options.bouncing) {
scrollLeft += moveX / 2;
} else if (scrollLeft > maxScrollLeft) {
scrollLeft = maxScrollLeft;
} else {
scrollLeft = 0;
}
}
}
// Compute new vertical scroll position
if (self.__enableScrollY) {
scrollTop -= moveY;
var maxScrollTop = self.__maxScrollTop;
if (scrollTop > maxScrollTop || scrollTop < 0) {
// Slow down on the edges
if (self.options.bouncing) {
scrollTop += moveY / 2;
// Support pull-to-refresh (only when only y is scrollable)
if (!self.__enableScrollX && self.__refreshHeight != null) {
if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
self.__refreshActive = true;
if (self.__refreshActivate) {
self.__refreshActivate();
}
} else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
}
}
} else if (scrollTop > maxScrollTop) {
scrollTop = maxScrollTop;
} else {
scrollTop = 0;
}
}
}
// Keep list from growing infinitely (holding min 10, max 20 measure points)
if (positions.length > 60) {
positions.splice(0, 30);
}
// Track scroll movement for decleration
positions.push(scrollLeft, scrollTop, timeStamp);
// Sync scroll position
self.__publish(scrollLeft, scrollTop, level);
// Otherwise figure out whether we are switching into dragging mode now.
} else {
var minimumTrackingForScroll = self.options.locking ? 3 : 0;
var minimumTrackingForDrag = 5;
var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
}
// Update last touch positions and time stamp for next event
self.__lastTouchLeft = currentTouchLeft;
self.__lastTouchTop = currentTouchTop;
self.__lastTouchMove = timeStamp;
self.__lastScale = scale;
},
/**
* Touch end handler for scrolling support
*/
doTouchEnd: function (timeStamp) {
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
// Ignore event when tracking is not enabled (no touchstart event on element)
// This is required as this listener ('touchmove') sits on the document and not on the element itself.
if (!self.__isTracking) {
return;
}
// Not touching anymore (when two finger hit the screen there are two touch end events)
self.__isTracking = false;
// Be sure to reset the dragging flag now. Here we also detect whether
// the finger has moved fast enough to switch into a deceleration animation.
if (self.__isDragging) {
// Reset dragging flag
self.__isDragging = false;
// Start deceleration
// Verify that the last move detected was in some relevant time frame
if (self.__isSingleTouch && self.options.animating && timeStamp - self.__lastTouchMove <= 100) {
// Then figure out what the scroll position was about 100ms ago
var positions = self.__positions;
var endPos = positions.length - 1;
var startPos = endPos;
// Move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && positions[i] > self.__lastTouchMove - 100; i -= 3) {
startPos = i;
}
// If start and stop position is identical in a 100ms timeframe,
// we cannot compute any useful deceleration.
if (startPos !== endPos) {
// Compute relative movement between these two points
var timeOffset = positions[endPos] - positions[startPos];
var movedLeft = self.__scrollLeft - positions[startPos - 2];
var movedTop = self.__scrollTop - positions[startPos - 1];
// Based on 50ms compute the movement to apply for each render step
self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
// How much velocity is required to start the deceleration
var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
// Verify that we have enough velocity to start deceleration
if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
// Deactivate pull-to-refresh when decelerating
if (!self.__refreshActive) {
self.__startDeceleration(timeStamp);
}
}
}
}
}
// If this was a slower move it is per default non decelerated, but this
// still means that we want snap back to the bounds which is done here.
// This is placed outside the condition above to improve edge case stability
// e.g. touchend fired without enabled dragging. This should normally do not
// have modified the scroll positions or even showed the scrollbars though.
if (!self.__isDecelerating) {
if (self.__refreshActive && self.__refreshStart) {
// Use publish instead of scrollTo to allow scrolling to out of boundary position
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
if (self.__refreshStart) {
self.__refreshStart();
}
} else {
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
// Directly signalize deactivation (nothing todo on refresh?)
if (self.__refreshActive) {
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
}
}
}
// Fully cleanup list
self.__positions.length = 0;
},
/*
---------------------------------------------------------------------------
PRIVATE API
---------------------------------------------------------------------------
*/
/**
* Applies the scroll position to the content element
*
* @param left {Number} Left scroll position
* @param top {Number} Top scroll position
* @param animate {Boolean?false} Whether animation should be used to move to the new coordinates
*/
__publish: function (left, top, zoom, animate) {
var self = this;
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
var wasAnimating = self.__isAnimating;
if (wasAnimating) {
core.effect.Animate.stop(wasAnimating);
self.__isAnimating = false;
}
if (animate && self.options.animating) {
// Keep scheduled positions for scrollBy/zoomBy functionality
self.__scheduledLeft = left;
self.__scheduledTop = top;
self.__scheduledZoom = zoom;
var oldLeft = self.__scrollLeft;
var oldTop = self.__scrollTop;
var oldZoom = self.__zoomLevel;
var diffLeft = left - oldLeft;
var diffTop = top - oldTop;
var diffZoom = zoom - oldZoom;
var step = function (percent, now, render) {
if (render) {
self.__scrollLeft = oldLeft + diffLeft * percent;
self.__scrollTop = oldTop + diffTop * percent;
self.__zoomLevel = oldZoom + diffZoom * percent;
// Push values out
if (self.__callback) {
self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
}
}
};
var verify = function (id) {
return self.__isAnimating === id;
};
var completed = function (renderedFramesPerSecond, animationId, wasFinished) {
if (animationId === self.__isAnimating) {
self.__isAnimating = false;
}
if (self.options.zooming) {
self.__computeScrollMax();
}
};
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
} else {
self.__scheduledLeft = self.__scrollLeft = left;
self.__scheduledTop = self.__scrollTop = top;
self.__scheduledZoom = self.__zoomLevel = zoom;
// Push values out
if (self.__callback) {
self.__callback(left, top, zoom);
}
// Fix max scroll ranges
if (self.options.zooming) {
self.__computeScrollMax();
}
}
},
/**
* Recomputes scroll minimum values based on client dimensions and content dimensions.
*/
__computeScrollMax: function (zoomLevel) {
var self = this;
if (zoomLevel == null) {
zoomLevel = self.__zoomLevel;
}
self.__maxScrollLeft = Math.max(self.__contentWidth * zoomLevel - self.__clientWidth, 0);
self.__maxScrollTop = Math.max(self.__contentHeight * zoomLevel - self.__clientHeight, 0);
},
/*
---------------------------------------------------------------------------
ANIMATION (DECELERATION) SUPPORT
---------------------------------------------------------------------------
*/
/**
* Called when a touch sequence end and the speed of the finger was high enough
* to switch into deceleration mode.
*/
__startDeceleration: function (timeStamp) {
var self = this;
if (self.options.paging) {
var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
var clientWidth = self.__clientWidth;
var clientHeight = self.__clientHeight;
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
// Each page should have exactly the size of the client area.
self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
} else {
self.__minDecelerationScrollLeft = 0;
self.__minDecelerationScrollTop = 0;
self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
self.__maxDecelerationScrollTop = self.__maxScrollTop;
}
// Wrap class method
var step = function (percent, now, render) {
self.__stepThroughDeceleration(render);
};
// How much velocity is required to keep the deceleration running
var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
// Detect whether it's still worth to continue animating steps
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
var verify = function () {
return Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
};
var completed = function (renderedFramesPerSecond, animationId, wasFinished) {
self.__isDecelerating = false;
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
};
// Start animation and switch on flag
self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
},
/**
* Called on every step of the animation
*
* @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
*/
__stepThroughDeceleration: function (render) {
var self = this;
//
// COMPUTE NEXT SCROLL POSITION
//
// Add deceleration to scroll position
var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
//
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
//
if (!self.options.bouncing) {
var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
if (scrollLeftFixed !== scrollLeft) {
scrollLeft = scrollLeftFixed;
self.__decelerationVelocityX = 0;
}
var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
if (scrollTopFixed !== scrollTop) {
scrollTop = scrollTopFixed;
self.__decelerationVelocityY = 0;
}
}
//
// UPDATE SCROLL POSITION
//
if (render) {
self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
} else {
self.__scrollLeft = scrollLeft;
self.__scrollTop = scrollTop;
}
//
// SLOW DOWN
//
// Slow down velocity on every iteration
if (!self.options.paging) {
// This is the factor applied to every iteration of the animation
// to slow down the process. This should emulate natural behavior where
// objects slow down when the initiator of the movement is removed
var frictionFactor = 0.95;
self.__decelerationVelocityX *= frictionFactor;
self.__decelerationVelocityY *= frictionFactor;
}
//
// BOUNCING SUPPORT
//
if (self.options.bouncing) {
var scrollOutsideX = 0;
var scrollOutsideY = 0;
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
var penetrationDeceleration = 0.03;
var penetrationAcceleration = 0.08;
// Check limits
if (scrollLeft < self.__minDecelerationScrollLeft) {
scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
} else if (scrollLeft > self.__maxDecelerationScrollLeft) {
scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
}
if (scrollTop < self.__minDecelerationScrollTop) {
scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
} else if (scrollTop > self.__maxDecelerationScrollTop) {
scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
}
// Slow down until slow enough, then flip back to snap position
if (scrollOutsideX !== 0) {
if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
} else {
self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
}
}
if (scrollOutsideY !== 0) {
if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
} else {
self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
}
}
}
}
};
// Copy over members to prototype
for (var key in members) {
Scroller.prototype[key] = members[key];
}
})();
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['shoestring'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = factory();
} else {
// Browser globals
factory();
}
})(function () {
var win = typeof window !== "undefined" ? window : this;
var doc = win.document;
/**
* The shoestring object constructor.
*
* @param {string,object} prim The selector to find or element to wrap.
* @param {object} sec The context in which to match the `prim` selector.
* @returns shoestring
* @this window
*/
function shoestring(prim, sec) {
var pType = typeof prim,
ret = [],
sel;
// return an empty shoestring object
if (!prim) {
return new Shoestring(ret);
}
// ready calls
if (prim.call) {
return shoestring.ready(prim);
}
// handle re-wrapping shoestring objects
if (prim.constructor === Shoestring && !sec) {
return prim;
}
// if string starting with <, make html
if (pType === "string" && prim.indexOf("<") === 0) {
var dfrag = doc.createElement("div");
dfrag.innerHTML = prim;
// TODO depends on children (circular)
return shoestring(dfrag).children().each(function () {
dfrag.removeChild(this);
});
}
// if string, it's a selector, use qsa
if (pType === "string") {
if (sec) {
return shoestring(sec).find(prim);
}
sel = doc.querySelectorAll(prim);
return new Shoestring(sel, prim);
}
// array like objects or node lists
if (Object.prototype.toString.call(pType) === '[object Array]' || win.NodeList && prim instanceof win.NodeList) {
return new Shoestring(prim, prim);
}
// if it's an array, use all the elements
if (prim.constructor === Array) {
return new Shoestring(prim, prim);
}
// otherwise assume it's an object the we want at an index
return new Shoestring([prim], prim);
}
var Shoestring = function (ret, prim) {
this.length = 0;
this.selector = prim;
shoestring.merge(this, ret);
};
// TODO only required for tests
Shoestring.prototype.reverse = [].reverse;
// For adding element set methods
shoestring.fn = Shoestring.prototype;
shoestring.Shoestring = Shoestring;
// For extending objects
// TODO move to separate module when we use prototypes
shoestring.extend = function (first, second) {
for (var i in second) {
if (second.hasOwnProperty(i)) {
first[i] = second[i];
}
}
return first;
};
// taken directly from jQuery
shoestring.merge = function (first, second) {
var len, j, i;
len = +second.length, j = 0, i = first.length;
for (; j < len; j++) {
first[i++] = second[j];
}
first.length = i;
return first;
};
// expose
win.shoestring = shoestring;
/**
* Make an HTTP request to a url.
*
* **NOTE** the following options are supported:
*
* - *method* - The HTTP method used with the request. Default: `GET`.
* - *data* - Raw object with keys and values to pass with request as query params. Default `null`.
* - *headers* - Set of request headers to add. Default `{}`.
* - *async* - Whether the opened request is asynchronouse. Default `true`.
* - *success* - Callback for successful request and response. Passed the response data.
* - *error* - Callback for failed request and response.
* - *cancel* - Callback for cancelled request and response.
*
* @param {string} url The url to request.
* @param {object} options The options object, see Notes.
* @return shoestring
* @this shoestring
*/
shoestring.ajax = function (url, options) {
var params = "",
req = new XMLHttpRequest(),
settings,
key;
settings = shoestring.extend({}, shoestring.ajax.settings);
if (options) {
shoestring.extend(settings, options);
}
if (!url) {
url = settings.url;
}
if (!req || !url) {
return;
}
// create parameter string from data object
if (settings.data) {
for (key in settings.data) {
if (settings.data.hasOwnProperty(key)) {
if (params !== "") {
params += "&";
}
params += encodeURIComponent(key) + "=" + encodeURIComponent(settings.data[key]);
}
}
}
// append params to url for GET requests
if (settings.method === "GET" && params) {
url += "?" + params;
}
req.open(settings.method, url, settings.async);
if (req.setRequestHeader) {
req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Set 'Content-type' header for POST requests
if (settings.method === "POST" && params) {
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
for (key in settings.headers) {
if (settings.headers.hasOwnProperty(key)) {
req.setRequestHeader(key, settings.headers[key]);
}
}
}
req.onreadystatechange = function () {
if (req.readyState === 4) {
// Trim the whitespace so shoestring('<div>') works
var res = (req.responseText || '').replace(/^\s+|\s+$/g, '');
if (req.status.toString().indexOf("0") === 0) {
return settings.cancel(res, req.status, req);
} else if (req.status.toString().match(/^(4|5)/) && RegExp.$1) {
return settings.error(res, req.status, req);
} else if (settings.success) {
return settings.success(res, req.status, req);
}
}
};
if (req.readyState === 4) {
return req;
}
// Send request
if (settings.method === "POST" && params) {
req.send(params);
} else {
req.send();
}
return req;
};
shoestring.ajax.settings = {
success: function () {},
error: function () {},
cancel: function () {},
method: "GET",
async: true,
data: null,
headers: {}
};
/**
* Helper function wrapping a call to [ajax](ajax.js.html) using the `GET` method.
*
* @param {string} url The url to GET from.
* @param {function} callback Callback to invoke on success.
* @return shoestring
* @this shoestring
*/
shoestring.get = function (url, callback) {
return shoestring.ajax(url, { success: callback });
};
/**
* Load the HTML response from `url` into the current set of elements.
*
* @param {string} url The url to GET from.
* @param {function} callback Callback to invoke after HTML is inserted.
* @return shoestring
* @this shoestring
*/
shoestring.fn.load = function (url, callback) {
var self = this,
args = arguments,
intCB = function (data) {
self.each(function () {
shoestring(this).html(data);
});
if (callback) {
callback.apply(self, args);
}
};
shoestring.ajax(url, { success: intCB });
return this;
};
/**
* Helper function wrapping a call to [ajax](ajax.js.html) using the `POST` method.
*
* @param {string} url The url to POST to.
* @param {object} data The data to send.
* @param {function} callback Callback to invoke on success.
* @return shoestring
* @this shoestring
*/
shoestring.post = function (url, data, callback) {
return shoestring.ajax(url, { data: data, method: "POST", success: callback });
};
/**
* Iterates over `shoestring` collections.
*
* @param {function} callback The callback to be invoked on each element and index
* @return shoestring
* @this shoestring
*/
shoestring.fn.each = function (callback) {
return shoestring.each(this, callback);
};
shoestring.each = function (collection, callback) {
var val;
for (var i = 0, il = collection.length; i < il; i++) {
val = callback.call(collection[i], i, collection[i]);
if (val === false) {
break;
}
}
return collection;
};
/**
* Check for array membership.
*
* @param {object} needle The thing to find.
* @param {object} haystack The thing to find the needle in.
* @return {boolean}
* @this window
*/
shoestring.inArray = function (needle, haystack) {
var isin = -1;
for (var i = 0, il = haystack.length; i < il; i++) {
if (haystack.hasOwnProperty(i) && haystack[i] === needle) {
isin = i;
}
}
return isin;
};
/**
* Bind callbacks to be run when the DOM is "ready".
*
* @param {function} fn The callback to be run
* @return shoestring
* @this shoestring
*/
shoestring.ready = function (fn) {
if (ready && fn) {
fn.call(doc);
} else if (fn) {
readyQueue.push(fn);
} else {
runReady();
}
return [doc];
};
// TODO necessary?
shoestring.fn.ready = function (fn) {
shoestring.ready(fn);
return this;
};
// Empty and exec the ready queue
var ready = false,
readyQueue = [],
runReady = function () {
if (!ready) {
while (readyQueue.length) {
readyQueue.shift().call(doc);
}
ready = true;
}
};
// If DOM is already ready at exec time, depends on the browser.
// From: https://github.com/mobify/mobifyjs/blob/526841be5509e28fc949038021799e4223479f8d/src/capture.js#L128
if (doc.attachEvent ? doc.readyState === "complete" : doc.readyState !== "loading") {
runReady();
} else {
doc.addEventListener("DOMContentLoaded", runReady, false);
doc.addEventListener("readystatechange", runReady, false);
win.addEventListener("load", runReady, false);
}
/**
* Checks the current set of elements against the selector, if one matches return `true`.
*
* @param {string} selector The selector to check.
* @return {boolean}
* @this {shoestring}
*/
shoestring.fn.is = function (selector) {
var ret = false,
self = this,
parents,
check;
// assume a dom element
if (typeof selector !== "string") {
// array-like, ie shoestring objects or element arrays
if (selector.length && selector[0]) {
check = selector;
} else {
check = [selector];
}
return _checkElements(this, check);
}
parents = this.parent();
if (!parents.length) {
parents = shoestring(doc);
}
parents.each(function (i, e) {
var children;
children = e.querySelectorAll(selector);
ret = _checkElements(self, children);
});
return ret;
};
function _checkElements(needles, haystack) {
var ret = false;
needles.each(function () {
var j = 0;
while (j < haystack.length) {
if (this === haystack[j]) {
ret = true;
}
j++;
}
});
return ret;
}
/**
* Get data attached to the first element or set data values on all elements in the current set.
*
* @param {string} name The data attribute name.
* @param {any} value The value assigned to the data attribute.
* @return {any|shoestring}
* @this shoestring
*/
shoestring.fn.data = function (name, value) {
if (name !== undefined) {
if (value !== undefined) {
return this.each(function () {
if (!this.shoestringData) {
this.shoestringData = {};
}
this.shoestringData[name] = value;
});
} else {
if (this[0]) {
if (this[0].shoestringData) {
return this[0].shoestringData[name];
}
}
}
} else {
return this[0] ? this[0].shoestringData || {} : undefined;
}
};
/**
* Remove data associated with `name` or all the data, for each element in the current set.
*
* @param {string} name The data attribute name.
* @return shoestring
* @this shoestring
*/
shoestring.fn.removeData = function (name) {
return this.each(function () {
if (name !== undefined && this.shoestringData) {
this.shoestringData[name] = undefined;
delete this.shoestringData[name];
} else {
this[0].shoestringData = {};
}
});
};
/**
* An alias for the `shoestring` constructor.
*/
if (typeof win.$ === "undefined") {
win.$ = shoestring;
}
/**
* Add a class to each DOM element in the set of elements.
*
* @param {string} className The name of the class to be added.
* @return shoestring
* @this shoestring
*/
shoestring.fn.addClass = function (className) {
var classes = className.replace(/^\s+|\s+$/g, '').split(" ");
return this.each(function () {
for (var i = 0, il = classes.length; i < il; i++) {
if (this.className !== undefined && (this.className === "" || !this.className.match(new RegExp("(^|\\s)" + classes[i] + "($|\\s)")))) {
this.className += " " + classes[i];
}
}
});
};
/**
* Add elements matching the selector to the current set.
*
* @param {string} selector The selector for the elements to add from the DOM
* @return shoestring
* @this shoestring
*/
shoestring.fn.add = function (selector) {
var ret = [];
this.each(function () {
ret.push(this);
});
shoestring(selector).each(function () {
ret.push(this);
});
return shoestring(ret);
};
/**
* Insert an element or HTML string after each element in the current set.
*
* @param {string|HTMLElement} fragment The HTML or HTMLElement to insert.
* @return shoestring
* @this shoestring
*/
shoestring.fn.after = function (fragment) {
if (typeof fragment === "string" || fragment.nodeType !== undefined) {
fragment = shoestring(fragment);
}
if (fragment.length > 1) {
fragment = fragment.reverse();
}
return this.each(function (i) {
for (var j = 0, jl = fragment.length; j < jl; j++) {
var insertEl = i > 0 ? fragment[j].cloneNode(true) : fragment[j];
this.parentNode.insertBefore(insertEl, this.nextSibling);
}
});
};
/**
* Insert an element or HTML string as the last child of each element in the set.
*
* @param {string|HTMLElement} fragment The HTML or HTMLElement to insert.
* @return shoestring
* @this shoestring
*/
shoestring.fn.append = function (fragment) {
if (typeof fragment === "string" || fragment.nodeType !== undefined) {
fragment = shoestring(fragment);
}
return this.each(function (i) {
for (var j = 0, jl = fragment.length; j < jl; j++) {
this.appendChild(i > 0 ? fragment[j].cloneNode(true) : fragment[j]);
}
});
};
/**
* Insert the current set as the last child of the elements matching the selector.
*
* @param {string} selector The selector after which to append the current set.
* @return shoestring
* @this shoestring
*/
shoestring.fn.appendTo = function (selector) {
return this.each(function () {
shoestring(selector).append(this);
});
};
/**
* Get the value of the first element of the set or set the value of all the elements in the set.
*
* @param {string} name The attribute name.
* @param {string} value The new value for the attribute.
* @return {shoestring|string|undefined}
* @this {shoestring}
*/
shoestring.fn.attr = function (name, value) {
var nameStr = typeof name === "string";
if (value !== undefined || !nameStr) {
return this.each(function () {
if (nameStr) {
this.setAttribute(name, value);
} else {
for (var i in name) {
if (name.hasOwnProperty(i)) {
this.setAttribute(i, name[i]);
}
}
}
});
} else {
return this[0] ? this[0].getAttribute(name) : undefined;
}
};
/**
* Insert an element or HTML string before each element in the current set.
*
* @param {string|HTMLElement} fragment The HTML or HTMLElement to insert.
* @return shoestring
* @this shoestring
*/
shoestring.fn.before = function (fragment) {
if (typeof fragment === "string" || fragment.nodeType !== undefined) {
fragment = shoestring(fragment);
}
return this.each(function (i) {
for (var j = 0, jl = fragment.length; j < jl; j++) {
this.parentNode.insertBefore(i > 0 ? fragment[j].cloneNode(true) : fragment[j], this);
}
});
};
/**
* Get the children of the current collection.
* @return shoestring
* @this shoestring
*/
shoestring.fn.children = function () {
var ret = [],
childs,
j;
this.each(function () {
childs = this.children;
j = -1;
while (j++ < childs.length - 1) {
if (shoestring.inArray(childs[j], ret) === -1) {
ret.push(childs[j]);
}
}
});
return shoestring(ret);
};
/**
* Clone and return the current set of nodes into a new `shoestring` object.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.clone = function () {
var ret = [];
this.each(function () {
ret.push(this.cloneNode(true));
});
return shoestring(ret);
};
/**
* Find an element matching the selector in the set of the current element and its parents.
*
* @param {string} selector The selector used to identify the target element.
* @return shoestring
* @this shoestring
*/
shoestring.fn.closest = function (selector) {
var ret = [];
if (!selector) {
return shoestring(ret);
}
this.each(function () {
var element,
$self = shoestring(element = this);
if ($self.is(selector)) {
ret.push(this);
return;
}
while (element.parentElement) {
if (shoestring(element.parentElement).is(selector)) {
ret.push(element.parentElement);
break;
}
element = element.parentElement;
}
});
return shoestring(ret);
};
shoestring.cssExceptions = {
'float': ['cssFloat']
};
(function () {
var cssExceptions = shoestring.cssExceptions;
// marginRight instead of margin-right
function convertPropertyName(str) {
return str.replace(/\-([A-Za-z])/g, function (match, character) {
return character.toUpperCase();
});
}
function _getStyle(element, property) {
return win.getComputedStyle(element, null).getPropertyValue(property);
}
var vendorPrefixes = ['', '-webkit-', '-ms-', '-moz-', '-o-', '-khtml-'];
/**
* Private function for getting the computed style of an element.
*
* **NOTE** Please use the [css](../css.js.html) method instead.
*
* @method _getStyle
* @param {HTMLElement} element The element we want the style property for.
* @param {string} property The css property we want the style for.
*/
shoestring._getStyle = function (element, property) {
var convert, value, j, k;
if (cssExceptions[property]) {
for (j = 0, k = cssExceptions[property].length; j < k; j++) {
value = _getStyle(element, cssExceptions[property][j]);
if (value) {
return value;
}
}
}
for (j = 0, k = vendorPrefixes.length; j < k; j++) {
convert = convertPropertyName(vendorPrefixes[j] + property);
// VendorprefixKeyName || key-name
value = _getStyle(element, convert);
if (convert !== property) {
value = value || _getStyle(element, property);
}
if (vendorPrefixes[j]) {
// -vendorprefix-key-name
value = value || _getStyle(element, vendorPrefixes[j] + property);
}
if (value) {
return value;
}
}
return undefined;
};
})();
(function () {
var cssExceptions = shoestring.cssExceptions;
// marginRight instead of margin-right
function convertPropertyName(str) {
return str.replace(/\-([A-Za-z])/g, function (match, character) {
return character.toUpperCase();
});
}
/**
* Private function for setting the style of an element.
*
* **NOTE** Please use the [css](../css.js.html) method instead.
*
* @method _setStyle
* @param {HTMLElement} element The element we want to style.
* @param {string} property The property being used to style the element.
* @param {string} value The css value for the style property.
*/
shoestring._setStyle = function (element, property, value) {
var convertedProperty = convertPropertyName(property);
element.style[property] = value;
if (convertedProperty !== property) {
element.style[convertedProperty] = value;
}
if (cssExceptions[property]) {
for (var j = 0, k = cssExceptions[property].length; j < k; j++) {
element.style[cssExceptions[property][j]] = value;
}
}
};
})();
/**
* Get the compute style property of the first element or set the value of a style property
* on all elements in the set.
*
* @method _setStyle
* @param {string} property The property being used to style the element.
* @param {string|undefined} value The css value for the style property.
* @return {string|shoestring}
* @this shoestring
*/
shoestring.fn.css = function (property, value) {
if (!this[0]) {
return;
}
if (typeof property === "object") {
return this.each(function () {
for (var key in property) {
if (property.hasOwnProperty(key)) {
shoestring._setStyle(this, key, property[key]);
}
}
});
} else {
// assignment else retrieve first
if (value !== undefined) {
return this.each(function () {
shoestring._setStyle(this, property, value);
});
}
return shoestring._getStyle(this[0], property);
}
};
/**
* Returns the indexed element wrapped in a new `shoestring` object.
*
* @param {integer} index The index of the element to wrap and return.
* @return shoestring
* @this shoestring
*/
shoestring.fn.eq = function (index) {
if (this[index]) {
return shoestring(this[index]);
}
return shoestring([]);
};
/**
* Filter out the current set if they do *not* match the passed selector or
* the supplied callback returns false
*
* @param {string,function} selector The selector or boolean return value callback used to filter the elements.
* @return shoestring
* @this shoestring
*/
shoestring.fn.filter = function (selector) {
var ret = [];
this.each(function (index) {
var wsel;
if (typeof selector === 'function') {
if (selector.call(this, index) !== false) {
ret.push(this);
}
// document node
} else if (this.nodeType === 9) {
if (this === selector) {
ret.push(this);
}
} else {
if (!this.parentNode) {
var context = shoestring(doc.createDocumentFragment());
context[0].appendChild(this);
wsel = shoestring(selector, context);
} else {
wsel = shoestring(selector, this.parentNode);
}
if (shoestring.inArray(this, wsel) > -1) {
ret.push(this);
}
}
});
return shoestring(ret);
};
/**
* Find descendant elements of the current collection.
*
* @param {string} selector The selector used to find the children
* @return shoestring
* @this shoestring
*/
shoestring.fn.find = function (selector) {
var ret = [],
finds;
this.each(function () {
finds = this.querySelectorAll(selector);
for (var i = 0, il = finds.length; i < il; i++) {
ret = ret.concat(finds[i]);
}
});
return shoestring(ret);
};
/**
* Returns the first element of the set wrapped in a new `shoestring` object.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.first = function () {
return this.eq(0);
};
/**
* Returns the raw DOM node at the passed index.
*
* @param {integer} index The index of the element to wrap and return.
* @return {HTMLElement|undefined|array}
* @this shoestring
*/
shoestring.fn.get = function (index) {
// return an array of elements if index is undefined
if (index === undefined) {
var elements = [];
for (var i = 0; i < this.length; i++) {
elements.push(this[i]);
}
return elements;
} else {
return this[index];
}
};
/**
* Private function for setting/getting the offset property for height/width.
*
* **NOTE** Please use the [width](width.js.html) or [height](height.js.html) methods instead.
*
* @param {shoestring} set The set of elements.
* @param {string} name The string "height" or "width".
* @param {float|undefined} value The value to assign.
* @return shoestring
* @this window
*/
shoestring._dimension = function (set, name, value) {
var offsetName;
if (value === undefined) {
offsetName = name.replace(/^[a-z]/, function (letter) {
return letter.toUpperCase();
});
return set[0]["offset" + offsetName];
} else {
// support integer values as pixels
value = typeof value === "string" ? value : value + "px";
return set.each(function () {
this.style[name] = value;
});
}
};
/**
* Gets the height value of the first element or sets the height for the whole set.
*
* @param {float|undefined} value The value to assign.
* @return shoestring
* @this shoestring
*/
shoestring.fn.height = function (value) {
return shoestring._dimension(this, "height", value);
};
var set = function (html) {
if (typeof html === "string" || typeof html === "number") {
return this.each(function () {
this.innerHTML = "" + html;
});
} else {
var h = "";
if (typeof html.length !== "undefined") {
for (var i = 0, l = html.length; i < l; i++) {
h += html[i].outerHTML;
}
} else {
h = html.outerHTML;
}
return this.each(function () {
this.innerHTML = h;
});
}
};
/**
* Gets or sets the `innerHTML` from all the elements in the set.
*
* @param {string|undefined} html The html to assign
* @return {string|shoestring}
* @this shoestring
*/
shoestring.fn.html = function (html) {
if (typeof html !== "undefined") {
return set.call(this, html);
} else {
// get
var pile = "";
this.each(function () {
pile += this.innerHTML;
});
return pile;
}
};
(function () {
function _getIndex(set, test) {
var i, result, element;
for (i = result = 0; i < set.length; i++) {
element = set.item ? set.item(i) : set[i];
if (test(element)) {
return result;
}
// ignore text nodes, etc
// NOTE may need to be more permissive
if (element.nodeType === 1) {
result++;
}
}
return -1;
}
/**
* Find the index in the current set for the passed selector.
* Without a selector it returns the index of the first node within the array of its siblings.
*
* @param {string|undefined} selector The selector used to search for the index.
* @return {integer}
* @this {shoestring}
*/
shoestring.fn.index = function (selector) {
var self, children;
self = this;
// no arg? check the children, otherwise check each element that matches
if (selector === undefined) {
children = (this[0] && this[0].parentNode || doc.documentElement).childNodes;
// check if the element matches the first of the set
return _getIndex(children, function (element) {
return self[0] === element;
});
} else {
// check if the element matches the first selected node from the parent
return _getIndex(self, function (element) {
return element === shoestring(selector, element.parentNode)[0];
});
}
};
})();
/**
* Insert the current set after the elements matching the selector.
*
* @param {string} selector The selector after which to insert the current set.
* @return shoestring
* @this shoestring
*/
shoestring.fn.insertAfter = function (selector) {
return this.each(function () {
shoestring(selector).after(this);
});
};
/**
* Insert the current set before the elements matching the selector.
*
* @param {string} selector The selector before which to insert the current set.
* @return shoestring
* @this shoestring
*/
shoestring.fn.insertBefore = function (selector) {
return this.each(function () {
shoestring(selector).before(this);
});
};
/**
* Returns the last element of the set wrapped in a new `shoestring` object.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.last = function () {
return this.eq(this.length - 1);
};
/**
* Returns a `shoestring` object with the set of siblings of each element in the original set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.next = function () {
var result = [];
// TODO need to implement map
this.each(function () {
var children, item, found;
// get the child nodes for this member of the set
children = shoestring(this.parentNode)[0].childNodes;
for (var i = 0; i < children.length; i++) {
item = children.item(i);
// found the item we needed (found) which means current item value is
// the next node in the list, as long as it's viable grab it
// NOTE may need to be more permissive
if (found && item.nodeType === 1) {
result.push(item);
break;
}
// find the current item and mark it as found
if (item === this) {
found = true;
}
}
});
return shoestring(result);
};
/**
* Removes elements from the current set.
*
* @param {string} selector The selector to use when removing the elements.
* @return shoestring
* @this shoestring
*/
shoestring.fn.not = function (selector) {
var ret = [];
this.each(function () {
var found = shoestring(selector, this.parentNode);
if (shoestring.inArray(this, found) === -1) {
ret.push(this);
}
});
return shoestring(ret);
};
/**
* Returns an object with the `top` and `left` properties corresponging to the first elements offsets.
*
* @return object
* @this shoestring
*/
shoestring.fn.offset = function () {
return {
top: this[0].offsetTop,
left: this[0].offsetLeft
};
};
/**
* Returns the set of first parents for each element in the current set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.parent = function () {
var ret = [],
parent;
this.each(function () {
// no parent node, assume top level
// jQuery parent: return the document object for <html> or the parent node if it exists
parent = this === doc.documentElement ? doc : this.parentNode;
// if there is a parent and it's not a document fragment
if (parent && parent.nodeType !== 11) {
ret.push(parent);
}
});
return shoestring(ret);
};
/**
* Returns the set of all parents matching the selector if provided for each element in the current set.
*
* @param {string} selector The selector to check the parents with.
* @return shoestring
* @this shoestring
*/
shoestring.fn.parents = function (selector) {
var ret = [];
this.each(function () {
var curr = this,
match;
while (curr.parentElement && !match) {
curr = curr.parentElement;
if (selector) {
if (curr === shoestring(selector)[0]) {
match = true;
if (shoestring.inArray(curr, ret) === -1) {
ret.push(curr);
}
}
} else {
if (shoestring.inArray(curr, ret) === -1) {
ret.push(curr);
}
}
}
});
return shoestring(ret);
};
/**
* Add an HTML string or element before the children of each element in the current set.
*
* @param {string|HTMLElement} fragment The HTML string or element to add.
* @return shoestring
* @this shoestring
*/
shoestring.fn.prepend = function (fragment) {
if (typeof fragment === "string" || fragment.nodeType !== undefined) {
fragment = shoestring(fragment);
}
return this.each(function (i) {
for (var j = 0, jl = fragment.length; j < jl; j++) {
var insertEl = i > 0 ? fragment[j].cloneNode(true) : fragment[j];
if (this.firstChild) {
this.insertBefore(insertEl, this.firstChild);
} else {
this.appendChild(insertEl);
}
}
});
};
/**
* Add each element of the current set before the children of the selected elements.
*
* @param {string} selector The selector for the elements to add the current set to..
* @return shoestring
* @this shoestring
*/
shoestring.fn.prependTo = function (selector) {
return this.each(function () {
shoestring(selector).prepend(this);
});
};
/**
* Returns a `shoestring` object with the set of *one* siblingx before each element in the original set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.prev = function () {
var result = [];
// TODO need to implement map
this.each(function () {
var children, item, found;
// get the child nodes for this member of the set
children = shoestring(this.parentNode)[0].childNodes;
for (var i = children.length - 1; i >= 0; i--) {
item = children.item(i);
// found the item we needed (found) which means current item value is
// the next node in the list, as long as it's viable grab it
// NOTE may need to be more permissive
if (found && item.nodeType === 1) {
result.push(item);
break;
}
// find the current item and mark it as found
if (item === this) {
found = true;
}
}
});
return shoestring(result);
};
/**
* Returns a `shoestring` object with the set of *all* siblings before each element in the original set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.prevAll = function () {
var result = [];
this.each(function () {
var $previous = shoestring(this).prev();
while ($previous.length) {
result.push($previous[0]);
$previous = $previous.prev();
}
});
return shoestring(result);
};
// Property normalization, a subset taken from jQuery src
shoestring.propFix = {
"class": "className",
contenteditable: "contentEditable",
"for": "htmlFor",
readonly: "readOnly",
tabindex: "tabIndex"
};
/**
* Gets the property value from the first element or sets the property value on all elements of the currrent set.
*
* @param {string} name The property name.
* @param {any} value The property value.
* @return {any|shoestring}
* @this shoestring
*/
shoestring.fn.prop = function (name, value) {
if (!this[0]) {
return;
}
name = shoestring.propFix[name] || name;
if (value !== undefined) {
return this.each(function () {
this[name] = value;
});
} else {
return this[0][name];
}
};
/**
* Remove an attribute from each element in the current set.
*
* @param {string} name The name of the attribute.
* @return shoestring
* @this shoestring
*/
shoestring.fn.removeAttr = function (name) {
return this.each(function () {
this.removeAttribute(name);
});
};
/**
* Remove a class from each DOM element in the set of elements.
*
* @param {string} className The name of the class to be removed.
* @return shoestring
* @this shoestring
*/
shoestring.fn.removeClass = function (cname) {
var classes = cname.replace(/^\s+|\s+$/g, '').split(" ");
return this.each(function () {
var newClassName, regex;
for (var i = 0, il = classes.length; i < il; i++) {
if (this.className !== undefined) {
regex = new RegExp("(^|\\s)" + classes[i] + "($|\\s)", "gmi");
newClassName = this.className.replace(regex, " ");
this.className = newClassName.replace(/^\s+|\s+$/g, '');
}
}
});
};
/**
* Remove the current set of elements from the DOM.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.remove = function () {
return this.each(function () {
if (this.parentNode) {
this.parentNode.removeChild(this);
}
});
};
/**
* Remove a proprety from each element in the current set.
*
* @param {string} name The name of the property.
* @return shoestring
* @this shoestring
*/
shoestring.fn.removeProp = function (property) {
var name = shoestring.propFix[property] || property;
return this.each(function () {
this[name] = undefined;
delete this[name];
});
};
/**
* Replace each element in the current set with that argument HTML string or HTMLElement.
*
* @param {string|HTMLElement} fragment The value to assign.
* @return shoestring
* @this shoestring
*/
shoestring.fn.replaceWith = function (fragment) {
if (typeof fragment === "string") {
fragment = shoestring(fragment);
}
var ret = [];
if (fragment.length > 1) {
fragment = fragment.reverse();
}
this.each(function (i) {
var clone = this.cloneNode(true),
insertEl;
ret.push(clone);
// If there is no parentNode, this is pointless, drop it.
if (!this.parentNode) {
return;
}
if (fragment.length === 1) {
insertEl = i > 0 ? fragment[0].cloneNode(true) : fragment[0];
this.parentNode.replaceChild(insertEl, this);
} else {
for (var j = 0, jl = fragment.length; j < jl; j++) {
insertEl = i > 0 ? fragment[j].cloneNode(true) : fragment[j];
this.parentNode.insertBefore(insertEl, this.nextSibling);
}
this.parentNode.removeChild(this);
}
});
return shoestring(ret);
};
shoestring.inputTypes = ["text", "hidden", "password", "color", "date", "datetime",
// "datetime\-local" matched by datetime
"email", "month", "number", "range", "search", "tel", "time", "url", "week"];
shoestring.inputTypeTest = new RegExp(shoestring.inputTypes.join("|"));
/**
* Serialize child input element values into an object.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.serialize = function () {
var data = {};
shoestring("input, select", this).each(function () {
var type = this.type,
name = this.name,
value = this.value;
if (shoestring.inputTypeTest.test(type) || (type === "checkbox" || type === "radio") && this.checked) {
data[name] = value;
} else if (this.nodeName === "SELECT") {
data[name] = this.options[this.selectedIndex].nodeValue;
}
});
return data;
};
/**
* Get all of the sibling elements for each element in the current set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.siblings = function () {
if (!this.length) {
return shoestring([]);
}
var sibs = [],
el = this[0].parentNode.firstChild;
do {
if (el.nodeType === 1 && el !== this[0]) {
sibs.push(el);
}
el = el.nextSibling;
} while (el);
return shoestring(sibs);
};
var getText = function (elem) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if (!nodeType) {
// If no nodeType, this is expected to be an array
while (node = elem[i++]) {
// Do not traverse comment nodes
ret += getText(node);
}
} else if (nodeType === 1 || nodeType === 9 || nodeType === 11) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if (typeof elem.textContent === "string") {
return elem.textContent;
} else {
// Traverse its children
for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
ret += getText(elem);
}
}
} else if (nodeType === 3 || nodeType === 4) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
/**
* Recursively retrieve the text content of the each element in the current set.
*
* @return shoestring
* @this shoestring
*/
shoestring.fn.text = function () {
return getText(this);
};
/**
* Get the value of the first element or set the value of all elements in the current set.
*
* @param {string} value The value to set.
* @return shoestring
* @this shoestring
*/
shoestring.fn.val = function (value) {
var el;
if (value !== undefined) {
return this.each(function () {
if (this.tagName === "SELECT") {
var optionSet,
option,
options = this.options,
values = [],
i = options.length,
newIndex;
values[0] = value;
while (i--) {
option = options[i];
if (option.selected = shoestring.inArray(option.value, values) >= 0) {
optionSet = true;
newIndex = i;
}
}
// force browsers to behave consistently when non-matching value is set
if (!optionSet) {
this.selectedIndex = -1;
} else {
this.selectedIndex = newIndex;
}
} else {
this.value = value;
}
});
} else {
el = this[0];
if (el.tagName === "SELECT") {
if (el.selectedIndex < 0) {
return "";
}
return el.options[el.selectedIndex].value;
} else {
return el.value;
}
}
};
/**
* Gets the width value of the first element or sets the width for the whole set.
*
* @param {float|undefined} value The value to assign.
* @return shoestring
* @this shoestring
*/
shoestring.fn.width = function (value) {
return shoestring._dimension(this, "width", value);
};
/**
* Wraps the child elements in the provided HTML.
*
* @param {string} html The wrapping HTML.
* @return shoestring
* @this shoestring
*/
shoestring.fn.wrapInner = function (html) {
return this.each(function () {
var inH = this.innerHTML;
this.innerHTML = "";
shoestring(this).append(shoestring(html).html(inH));
});
};
function initEventCache(el, evt) {
if (!el.shoestringData) {
el.shoestringData = {};
}
if (!el.shoestringData.events) {
el.shoestringData.events = {};
}
if (!el.shoestringData.loop) {
el.shoestringData.loop = {};
}
if (!el.shoestringData.events[evt]) {
el.shoestringData.events[evt] = [];
}
}
function addToEventCache(el, evt, eventInfo) {
var obj = {};
obj.isCustomEvent = eventInfo.isCustomEvent;
obj.callback = eventInfo.callfunc;
obj.originalCallback = eventInfo.originalCallback;
obj.namespace = eventInfo.namespace;
el.shoestringData.events[evt].push(obj);
if (eventInfo.customEventLoop) {
el.shoestringData.loop[evt] = eventInfo.customEventLoop;
}
}
/**
* Bind a callback to an event for the currrent set of elements.
*
* @param {string} evt The event(s) to watch for.
* @param {object,function} data Data to be included with each event or the callback.
* @param {function} originalCallback Callback to be invoked when data is define.d.
* @return shoestring
* @this shoestring
*/
shoestring.fn.bind = function (evt, data, originalCallback) {
if (typeof data === "function") {
originalCallback = data;
data = null;
}
var evts = evt.split(" ");
// NOTE the `triggeredElement` is purely for custom events from IE
function encasedCallback(e, namespace, triggeredElement) {
var result;
if (e._namespace && e._namespace !== namespace) {
return;
}
e.data = data;
e.namespace = e._namespace;
var returnTrue = function () {
return true;
};
e.isDefaultPrevented = function () {
return false;
};
var originalPreventDefault = e.preventDefault;
var preventDefaultConstructor = function () {
if (originalPreventDefault) {
return function () {
e.isDefaultPrevented = returnTrue;
originalPreventDefault.call(e);
};
} else {
return function () {
e.isDefaultPrevented = returnTrue;
e.returnValue = false;
};
}
};
// thanks https://github.com/jonathantneal/EventListener
e.target = triggeredElement || e.target || e.srcElement;
e.preventDefault = preventDefaultConstructor();
e.stopPropagation = e.stopPropagation || function () {
e.cancelBubble = true;
};
result = originalCallback.apply(this, [e].concat(e._args));
if (result === false) {
e.preventDefault();
e.stopPropagation();
}
return result;
}
return this.each(function () {
var domEventCallback,
customEventCallback,
customEventLoop,
oEl = this;
for (var i = 0, il = evts.length; i < il; i++) {
var split = evts[i].split("."),
evt = split[0],
namespace = split.length > 0 ? split[1] : null;
domEventCallback = function (originalEvent) {
if (oEl.ssEventTrigger) {
originalEvent._namespace = oEl.ssEventTrigger._namespace;
originalEvent._args = oEl.ssEventTrigger._args;
oEl.ssEventTrigger = null;
}
return encasedCallback.call(oEl, originalEvent, namespace);
};
customEventCallback = null;
customEventLoop = null;
initEventCache(this, evt);
this.addEventListener(evt, domEventCallback, false);
addToEventCache(this, evt, {
callfunc: customEventCallback || domEventCallback,
isCustomEvent: !!customEventCallback,
customEventLoop: customEventLoop,
originalCallback: originalCallback,
namespace: namespace
});
}
});
};
shoestring.fn.on = shoestring.fn.bind;
/**
* Unbind a previous bound callback for an event.
*
* @param {string} event The event(s) the callback was bound to..
* @param {function} callback Callback to unbind.
* @return shoestring
* @this shoestring
*/
shoestring.fn.unbind = function (event, callback) {
var evts = event ? event.split(" ") : [];
return this.each(function () {
if (!this.shoestringData || !this.shoestringData.events) {
return;
}
if (!evts.length) {
unbindAll.call(this);
} else {
var split, evt, namespace;
for (var i = 0, il = evts.length; i < il; i++) {
split = evts[i].split("."), evt = split[0], namespace = split.length > 0 ? split[1] : null;
if (evt) {
unbind.call(this, evt, namespace, callback);
} else {
unbindAll.call(this, namespace, callback);
}
}
}
});
};
function unbind(evt, namespace, callback) {
var bound = this.shoestringData.events[evt];
if (!(bound && bound.length)) {
return;
}
var matched = [],
j,
jl;
for (j = 0, jl = bound.length; j < jl; j++) {
if (!namespace || namespace === bound[j].namespace) {
if (callback === undefined || callback === bound[j].originalCallback) {
this.removeEventListener(evt, bound[j].callback, false);
matched.push(j);
}
}
}
for (j = 0, jl = matched.length; j < jl; j++) {
this.shoestringData.events[evt].splice(j, 1);
}
}
function unbindAll(namespace, callback) {
for (var evtKey in this.shoestringData.events) {
unbind.call(this, evtKey, namespace, callback);
}
}
shoestring.fn.off = shoestring.fn.unbind;
/**
* Bind a callback to an event for the currrent set of elements, unbind after one occurence.
*
* @param {string} event The event(s) to watch for.
* @param {function} callback Callback to invoke on the event.
* @return shoestring
* @this shoestring
*/
shoestring.fn.one = function (event, callback) {
var evts = event.split(" ");
return this.each(function () {
var thisevt,
cbs = {},
$t = shoestring(this);
for (var i = 0, il = evts.length; i < il; i++) {
thisevt = evts[i];
cbs[thisevt] = function (e) {
var $t = shoestring(this);
for (var j in cbs) {
$t.unbind(j, cbs[j]);
}
return callback.apply(this, [e].concat(e._args));
};
$t.bind(thisevt, cbs[thisevt]);
}
});
};
/**
* Trigger an event on the first element in the set, no bubbling, no defaults.
*
* @param {string} event The event(s) to trigger.
* @param {object} args Arguments to append to callback invocations.
* @return shoestring
* @this shoestring
*/
shoestring.fn.triggerHandler = function (event, args) {
var e = event.split(" ")[0],
el = this[0],
ret;
// See this.fireEvent( 'on' + evts[ i ], document.createEventObject() ); instead of click() etc in trigger.
if (doc.createEvent && el.shoestringData && el.shoestringData.events && el.shoestringData.events[e]) {
var bindings = el.shoestringData.events[e];
for (var i in bindings) {
if (bindings.hasOwnProperty(i)) {
event = doc.createEvent("Event");
event.initEvent(e, true, true);
event._args = args;
args.unshift(event);
ret = bindings[i].originalCallback.apply(event.target, args);
}
}
}
return ret;
};
/**
* Trigger an event on each of the DOM elements in the current set.
*
* @param {string} event The event(s) to trigger.
* @param {object} args Arguments to append to callback invocations.
* @return shoestring
* @this shoestring
*/
shoestring.fn.trigger = function (event, args) {
var evts = event.split(" ");
return this.each(function () {
var split, evt, namespace;
for (var i = 0, il = evts.length; i < il; i++) {
split = evts[i].split("."), evt = split[0], namespace = split.length > 0 ? split[1] : null;
if (evt === "click") {
if (this.tagName === "INPUT" && this.type === "checkbox" && this.click) {
this.click();
return false;
}
}
if (doc.createEvent) {
var event = doc.createEvent("Event");
event.initEvent(evt, true, true);
event._args = args;
event._namespace = namespace;
this.dispatchEvent(event);
}
}
});
};
return shoestring;
});
if (!window.jQuery) {
window.jQuery = window.jQuery || window.shoestring;
}
(function (exports) {
/**
* The Akamai component namespace contains the {@link Akamai.Viewer}, {@link
* Akamai.Spin360}, {@link Akamai.Carousel}, and {@link Akamai.Zoom} components.
* The {@link Akamai.Viewer} component manages the other components depending on
* the state of the markup it is provided, start there for more.
*
* @example <caption>On-ready automatic initialization with jQuery</caption>
* $(function(){
* $( "[data-akamai-viewer]" ).each(function(i, element){
* var viewer = Akamai.Viewer( element );
* ...
* // example API use, stoping autoration of first {@link Akamai.Spin360}
* viewer.getCarousels()[0].getSpin360s()[0].stopSpin();
* })
* })
*
* @namespace Akamai
*/
exports.Akamai = exports.Akamai || {};
window.componentNamespace = "Akamai";
})(typeof exports === 'undefined' ? window : exports);
(function (window, $) {
var $window, $doc;
$window = $(window);
$doc = $(document.documentElement);
var ns = window.componentNamespace = window.componentNamespace || "FG";
window[ns] = window[ns] || {};
Function.prototype.bind = Function.prototype.bind || function (context) {
var self = this;
return function () {
self.apply(context, arguments);
};
};
var Tau = window[ns].Tau = function (element, options) {
var startIndex, reducedStepSize;
this.element = element;
this.options = options || {};
this.$element = $(element);
this.$initial = this.$element.find("img");
this.$loading = this.$element.find(".loading");
this.index = 0;
// frame count by order of precendence
// 1. initial frames when they are specified explicitly
// 2. the data attribute on the initial image
// 3. the configured number of frames
this.frames = this.$initial.length > 1 ? this.$initial.length : parseInt(this.$initial.attr("data-frames"), 10) || this.options.frames;
// grab the user specified step size for when the browser is less-abled
reducedStepSize = parseInt(this.$initial.attr("data-reduced-step-size"), 10) || 4;
// TODO sort out a better qualification for the full set of images?
this.stepSize = window.requestAnimationFrame ? 1 : reducedStepSize;
// grab the user specified auto start delay
this.autoRotateStartDelay = (this.options.autoplay || {}).delay || parseInt(this.$initial.attr("data-auto-rotate-delay"), 10) || Tau.autoRotateStartDelay;
this.mouseMoveBinding = this.rotateEvent.bind(this);
this.touchMoveBinding = this.rotateEvent.bind(this);
this.path = new Tau.Path();
// make sure the initial image stays visible after enhance
this.$initial.first().addClass("focused");
// hide all other images
this.$element.addClass("tau-enhanced");
// create a rendering spot to force decoding in IE and prevent blinking
this.$render = $("<div data-render class=\"render\"></div>").css("position", "absolute").css("left", "0").css("top", "0").prependTo(this.element);
if (this.options.canvas !== false) {
this.canvas = $("<canvas/>").prependTo(this.element)[0];
if (this.canvas.getContext) {
this.canvasCtx = this.canvas.getContext("2d");
this.$element.addClass("tau-canvas");
$(window).bind("resize", function () {
clearTimeout(this.canvasResizeTimeout);
this.canvasResizeTimeout = setTimeout(this.renderCanvas.bind(this), 100);
}.bind(this));
}
}
if (this.options.controls) {
this.options.controls.text = this.options.controls.text || {
play: "Spin Object",
left: "Rotate Left",
right: "Rotate Right"
};
this.createControls();
}
// create the rest of the images
this.createImages();
// set the initial index and image
if (this.options.autoplay && this.options.autoplay.enabled) {
// start the automatic rotation
this.autostartTimeout = setTimeout(this.autoRotate.bind(this), this.autoRotateStartDelay);
}
// setup the event bindings for touch drag and mouse drag rotation
this.bind();
};
Tau.autoRotateTraversalTime = 4500;
Tau.autoRotateStartDelay = 100;
Tau.verticalScrollRatio = 4;
// Tau.decelTimeStep = Tau.autoRotateDelay / 2;
// Tau.decel = Tau.decelTimeStep / 8;
Tau.maxVelocity = 60;
Tau.prototype.createControls = function () {
this.$controls = $("<div class='tau-controls'></div>");
if (this.options.controls.play) {
this.$controls.append(this.controlAnchorMarkup("play"));
}
if (this.options.controls.arrows) {
this.$controls.prepend(this.controlAnchorMarkup("left")).append(this.controlAnchorMarkup("right"));
}
this.$controls.bind("mousedown touchstart", this.onControlDown.bind(this));
this.$controls.bind("mouseup", this.onControlUp.bind(this));
// prevent link clicks from bubbling
this.$controls.bind("click", function (event) {
if ($(event.target).is("a")) {
event.preventDefault();
}
});
this.$element.append(this.$controls);
};
Tau.prototype.controlAnchorMarkup = function (name) {
var text = this.options.controls.text[name];
return "<a href='#' data-tau-controls='" + name + "' title='" + text + "'>" + text + "</a>";
};
Tau.prototype.onControlDown = function (event) {
var $link = $(event.target).closest("a");
switch ($link.attr("data-tau-controls")) {
case "left":
this.$element.addClass("control-left-down");
this.stopAutoRotate();
this.autoRotate();
break;
case "right":
this.$element.addClass("control-right-down");
this.stopAutoRotate();
this.autoRotate(true);
break;
}
};
Tau.prototype.onControlUp = function (event) {
var $link = $(event.target).closest("a");
switch ($link.attr("data-tau-controls")) {
case "left":
case "right":
this.$element.removeClass("control-left-down");
this.$element.removeClass("control-right-down");
this.stopAutoRotate();
break;
case "play":
if (this.autoInterval) {
this.stopAutoRotate();
} else {
this.autoRotate();
}
break;
}
};
Tau.prototype.change = function (delta) {
this.goto(this.options.reverse ? this.index - delta : this.index + delta);
};
Tau.prototype.goto = function (index) {
var $next,
normalizedIndex,
imageCount = this.$images.length;
index = index % imageCount;
// stay within the bounds of the array
normalizedIndex = (imageCount + index) % imageCount;
// set the next image that's going to be shown/focused
$next = this.$images.eq(normalizedIndex);
// skip this action if the desired image isn't loaded yet
// TODO do something fancier here instead of just throwing up hands
if (!$next[0].tauImageLoaded) {
this.showLoading();
return false;
}
// hide any image that happens to be visible (initial image when canvas)
if (this.$current) {
this.$current.removeClass("focused");
} else {
this.$images.removeClass("focused");
}
// record the current focused image and make it visible
this.$current = $next;
// record the updated index only after advancing is possible
this.index = normalizedIndex;
if (this.canvasCtx) {
return this.renderCanvas();
} else {
// show the new focused image
this.$current.addClass("focused");
return true;
}
};
Tau.prototype.renderCanvas = function () {
var $img = this.$current;
var img = $img[0];
var width = img.naturalWidth;
var height = img.naturalHeight;
var parentWidth = this.element.clientWidth;
var calcHeight = parentWidth / width * height;
if (!width || !height || !img.complete) {
return false;
}
if (this.canvas.width !== parentWidth || this.canvas.height !== calcHeight || parentWidth && calcHeight) {
this.canvas.width = parentWidth;
this.canvas.height = calcHeight;
}
this.canvasCtx.drawImage(img, 0, 0, parentWidth, calcHeight);
return true;
};
// TODO transplant the attributes from the initial image
Tau.prototype.createImages = function () {
var src, frames, html, $new, boundImageLoaded;
// if there are no image elements, raise an exception
if (this.$initial.length < 1) {
throw new Error("At least one image required");
}
this.loadedCount = 0;
// if there is only one image element, assume it's a template
if (this.$initial.length == 1) {
this.markImageLoaded(this.$initial[0]);
src = this.options.template || this.$initial.attr("data-src-template");
var imgs = [];
for (var i = this.stepSize + 1; i <= this.frames; i += this.stepSize) {
html = "<img src=" + src.replace("$FRAME", i) + "></img>";
$new = $(html);
imgs.push($new);
}
$.each(imgs, function (i, e) {
var $img = $(e);
$img.bind("load error", function (e) {
this.imageLoaded(i, e.target, e);
}.bind(this));
this.$element.append($img);
this.$render.append($img.html());
}.bind(this));
// take all the child images and use them as frames of the rotation
this.$images = this.$element.children().filter("img");
this.$current = this.$images;
this.goto(0);
} else {
// take all the child images and use them as frames of the rotation
this.$images = this.$element.children().filter("img");
this.$images.each(function (i, e) {
// if the image height is greater than zero we assume the image is loaded
// otherwise we bind to onload and pray that we win the race
if ($(e).height() > 0) {
this.imageLoaded(i, e);
} else {
$(e).bind("load error", function (event) {
this.imageLoaded(i, event.target, event);
}.bind(this));
}
}.bind(this));
}
};
Tau.prototype.imageLoaded = function (index, element, event) {
var initTriggered = false;
this.markImageLoaded(element);
// if the isn't going to play automatically and the first image is
// loaded make sure to render it
if (this.$element.find("img")[0] == element && (!event || event.type !== "error") && (!this.options.autoplay || !this.options.autoplay.enabled)) {
this.goto(0);
this.$element.trigger("tau.init");
initTriggered = true;
}
this.loadedCount++;
if (this.loadedCount >= this.frames - 1) {
this.hideLoading();
if (!initTriggered) {
this.$element.trigger("tau.init");
this.initialized = true;
}
}
};
Tau.prototype.markImageLoaded = function (element) {
element.tauImageLoaded = true;
};
Tau.prototype.bind = function () {
this.$element.bind("mousedown touchstart", this.track.bind(this));
};
Tau.prototype.autoRotate = function (right) {
// already rotating
if (this.autoInterval) {
return;
}
this.$element.addClass("spinning");
// move once initially
this.change(right ? -1 : 1);
// move after the interval
this.autoInterval = setInterval(function () {
this.change(right ? -1 : 1);
}.bind(this), this.autoRotateDelay() * this.stepSize);
this.$element.trigger("tau.auto-rotate-start");
};
Tau.prototype.autoRotateDelay = function () {
return (this.options.interval || Tau.autoRotateTraversalTime) / this.frames;
};
Tau.prototype.stopAutoRotate = function () {
clearInterval(this.autoInterval);
clearInterval(this.autostartTimeout);
this.$element.removeClass("spinning");
this.autoInterval = undefined;
this.$element.trigger("tau.auto-rotate-stop");
};
Tau.prototype.track = function (event) {
var point;
// ignore tracking on control clicks
if ($(event.target).closest(".tau-controls").length) {
return;
}
// prevent dragging behavior for mousedown
if (event.type === "mousedown") {
event.preventDefault();
}
if (event.type === "touchstart") {
this.$element.trigger("tau.touch-tracking-start");
} else {
this.$element.trigger("tau.mouse-tracking-start");
}
if (this.tracking) {
return;
}
$doc.one("mouseup", this.release.bind(this));
$doc.one("touchend", this.release.bind(this));
this.tracking = true;
// clean out the path since we'll need a new one for decel
this.path.reset();
// show the cursor as grabbing
this.cursorGrab();
// By default the number of pixels required to move the carousel by one
// frame is the ratio of the tau element width to the number of frames. That
// is, by default the user should be able to see the full rotation by moving
// their input device from one side of the tau element to the other.
var defaultThreshold = this.$element[0].clientWidth / this.frames;
// divide the default by the sensitivity. If the senstivity is greater than
// 1 it will require less effort (smaller distance) to advance the rotation
// by a single slide. If the sensitivity is less than 1 it will require more
// effort
this.rotateThreshold = defaultThreshold / (this.options.sensitivity || 1);
// record the x for threshold calculations
point = this.getPoint(event);
this.downX = point.x;
this.downY = point.y;
this.downIndex = this.index;
$doc.bind("mousemove", this.mouseMoveBinding);
$doc.bind("touchmove", this.touchMoveBinding);
};
Tau.prototype.slow = function () {
// if the path gets broken during the decel just stop
if (!this.path.isSufficient()) {
this.clearSlowInterval();
return;
}
this.rotate({
x: this.path.last().x + this.velocity,
y: this.path.last().y
});
if (this.velocity > 0) {
this.velocity = this.velocity - this.decelVal();
if (this.velocity <= 0) {
this.clearSlowInterval();
}
} else {
this.velocity = this.velocity + this.decelVal();
if (this.velocity >= 0) {
this.clearSlowInterval();
}
}
};
Tau.prototype.decelVal = function () {
return this.decelTimeStep() / 8;
};
Tau.prototype.clearSlowInterval = function () {
clearInterval(this.slowInterval);
this.velocity = 0;
this.slowInterval = undefined;
};
Tau.prototype.decel = function () {
var velocity, sign;
// if we don't have two points of mouse or touch tracking this won't work
if (!this.path.isSufficient()) {
return;
}
// determine the starting velocity based on the traced path
velocity = this.path.velocity(this.decelTimeStep());
// borrowed from http://stackoverflow.com/questions/7624920/number-sign-in-javascript
sign = velocity > 0 ? 1 : velocity < 0 ? -1 : 0;
// keep a lid on how fast the rotation spins out
if (Math.abs(velocity) > Tau.maxVelocity) {
velocity = sign * Tau.maxVelocity;
}
this.velocity = velocity;
this.slowInterval = setInterval(this.slow.bind(this), this.decelTimeStep());
};
Tau.prototype.decelTimeStep = function () {
return this.autoRotateDelay() / 2;
};
Tau.prototype.release = function (event) {
if ($(event.target).closest(".tau-controls").length) {
return;
}
if (!this.tracking) {
return;
}
if (event.type === "touchend") {
this.$element.trigger("tau.touch-tracking-stop");
} else {
this.$element.trigger("tau.mouse-tracking-stop");
}
this.decel();
this.cursorRelease();
// TODO sort out why shoestring borks when unbinding with a string split list
$doc.unbind("mousemove", this.mouseMoveBinding);
$doc.unbind("touchmove", this.touchMoveBinding);
this.tracking = false;
};
Tau.prototype.cursorGrab = function () {
$doc.addClass("grabbing");
};
Tau.prototype.cursorRelease = function () {
$doc.removeClass("grabbing");
};
Tau.prototype.showLoading = function () {
this.$loading.attr("style", "display: block");
};
Tau.prototype.hideLoading = function () {
this.$loading.attr("style", "display: none");
};
Tau.prototype.getPoint = function (event) {
var touch = event.touches || event.originalEvent && event.originalEvent.touches;
if (touch) {
return {
x: touch[0].pageX,
y: touch[0].pageY
};
}
return {
x: event.pageX || event.clientX,
y: event.pageY || event.clientY
};
};
Tau.prototype.rotateEvent = function (event) {
// NOTE it might be better to prevent when the rotation returns anything BUT false
// so that slow drags still get the scroll prevented
if (this.rotate(this.getPoint(event))) {
event.preventDefault();
}
};
Tau.prototype.rotate = function (point) {
var deltaX, deltaY;
deltaX = point.x - this.downX;
deltaY = point.y - this.downY;
// if the movement on the Y dominates X then skip and allow scroll
if (Math.abs(deltaY) / Math.abs(deltaX) >= Tau.verticalScrollRatio) {
return false;
}
// since we're rotating record the point for decel
this.path.record(point);
// NOTE to reverse the spin direction add the delta/thresh to the downIndex
if (Math.abs(deltaX) >= this.rotateThreshold) {
// NOTE works better on mousedown, here allows autorotate to continue
this.stopAutoRotate();
var index;
if (this.options.reverse) {
index = this.downIndex + Math.round(deltaX / this.rotateThreshold);
} else {
index = this.downIndex - Math.round(deltaX / this.rotateThreshold);
}
this.goto(index);
return true;
}
};
})(this, jQuery);
(function (window, $) {
var ns = window.componentNamespace = window.componentNamespace || "FG";
window[ns] = window[ns] || {};
// IE 8
Date.now = Date.now || function now() {
return new Date().getTime();
};
var Path = window[ns].Tau.Path = function () {
this.reset();
};
Path.prototype.isSufficient = function () {
return !!this.prevPoint && this.prevPrevPoint;
};
Path.prototype.distance = function () {
return this.prevPoint.x - this.prevPrevPoint.x;
};
Path.prototype.duration = function () {
return this.prevTime - this.prevPrevTime;
};
// TODO sort out variable names
Path.prototype.record = function (point) {
this.prevPrevTime = this.prevTime;
this.prevPrevPoint = this.prevPoint;
// record the most recent drag point for decel on release
this.prevTime = Date.now();
this.prevPoint = point;
};
Path.prototype.velocity = function (timeStep) {
var distance, time;
distance = this.distance();
time = this.duration();
return distance / (time / timeStep);
};
Path.prototype.reset = function () {
this.prevPoint = undefined;
this.prevTime = undefined;
this.prevPrevTime = undefined;
this.prevPrevPoint = undefined;
};
Path.prototype.last = function () {
return this.prevPoint;
};
})(this, jQuery);
(function (exports, $) {
exports.Akamai = exports.Akamai || {};
var Util = exports.Akamai.Util = {};
Util.preflight = function (context, element, options, name) {
name = name || context.constructor.name;
context._$el = $(element);
if (!context._$el.length) {
throw new Error(name + " component requires element");
}
// If the element has already been instantiated with this component, skip
if (context._$el.data(name)) {
return context._$el.data(name);
}
// Store the instance for access and to prevent double init above
context._$el.data(name, context);
// make sure the element has the data attribute for CSS that keys off it
context._$el.attr(context.constructor._dataAttr, true);
context._options = Util.options(context.constructor.defaultOptions || {}, options || {}, context._$el, name);
// retain the options decided at instantiation for reseting the options
// when the breakpoint options don't apply (are "backed out")
context._originalOptions = Util.extend(true, {}, context._options);
// map all the child component events to the wrapper component events
Util.mapEvents(context);
return false;
};
Util.component = function (name, async, init) {
if (!init) {
init = async;
async = false;
}
var constr = function (element, options) {
// do the preflight
var existing = Util.preflight(this, element, options, name);
// return on double init
if (existing) {
this._trigger("init");
return existing;
}
// call the init code
init.apply(this, arguments);
// trigger the init event
if (!async) {
this._trigger("init");
}
};
constr.name = name;
constr._dataAttr = "data-akamai-" + name.toLowerCase();
// static constructor for many possible objects
constr.createMany = Util.createMany(constr);
// component prefixed event trigger
constr.prototype._trigger = Util.trigger(name);
return constr;
};
Util.mapEvents = function (context) {
var mapping = context.constructor._componentEventMapping || {};
for (var event in mapping) {
if (mapping.hasOwnProperty(event)) {
var mappedTo = mapping[event].to || mapping[event];
var $el = mapping[event].selector ? context._$el.find(mapping[event].selector) : undefined;
Util.mapEvent(context, event, mappedTo, $el);
}
}
};
Util.mapEvent = function (context, from, to, $el) {
($el || context._$el).bind(from, function () {
context._trigger(to);
}.bind(context));
};
Util.createMany = function (constructor, selector) {
selector = selector || "[" + constructor._dataAttr + "]";
return function (element, options) {
var $comps = $(element).find(selector);
// sigh, shoestring needs map
var objects = [];
$comps.each(function (i, el) {
objects.push(new constructor(el, options));
});
return objects;
};
};
/**
* Helper for building assertions into methods based on property requirements
* @private
* @static
* @param {prop} prop - the property required for the function to operate correctly
* @param {fn} function - the function definition
*/
Util.propRequired = function (prop, msg, fn) {
return function () {
var args = arguments;
if (!this[prop]) {
throw new Error(msg);
}
return fn.apply(this, args);
};
};
Util.trigger = function (componentName) {
componentName = componentName.toLowerCase();
return function (event) {
var fullEvent = "akamai-" + componentName + "-" + event;
// NOTE we assume here that if jQuery is present we will only want to trigger
// with jQuery and that we are using the same DOM lib across the library
this._$el.trigger(fullEvent);
};
};
Util.camelToKabob = function (string) {
return string.replace(/([A-Z]+)/g, function (m) {
return "-" + m.toLowerCase();
});
};
Util.options = function (defaults, options, $element, name) {
var dataAttrOptions = {};
// make sure to make a new copy of the defaults so we don't mess with the original
var cloneDefaults = Util.extend(true, {}, defaults);
if ($element && name) {
dataAttrOptions = Util.getDataAttrOptions($element, defaults, "data-akamai-" + name.toLowerCase());
}
// use the following precedence, prefering settings further to the right
// defaults -> options -> datattributes
var finalOptions = Util.extend(true, Util.extend(true, cloneDefaults, dataAttrOptions), options);
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
if ($element && name) {
Util.setDataAttrOptions($element, defaults, finalOptions, name);
}
return finalOptions;
};
Util.getDataAttrOptions = function ($element, defaults, prefix) {
var dataAttrOptions = {};
for (var prop in defaults) {
if (defaults.hasOwnProperty(prop)) {
var dashedProp = Util.camelToKabob(prop);
var currentDataProp = prefix + "-" + dashedProp;
if (/object/.test(typeof defaults[prop]) && !(defaults[prop] instanceof Array)) {
var subDefaults = defaults[prop];
var newPrefix = currentDataProp;
var subConfig = Util.getDataAttrOptions($element, subDefaults, newPrefix);
dataAttrOptions[prop] = subConfig;
} else {
var value = $element.attr(currentDataProp);
value = Util.coerceAttrVal(value);
if (value !== undefined && value !== null) {
// set the config
dataAttrOptions[prop] = value;
}
}
}
}
return dataAttrOptions;
};
Util.setDataAttrOptions = function ($element, defaults, options, name) {
Util.setDataAttrOptionsPrefix($element, defaults, options, "data-akamai-" + name.toLowerCase());
};
Util.setDataAttrOptionsPrefix = function ($element, defaults, options, prefix) {
defaults = defaults || {};
for (var prop in options) {
if (options.hasOwnProperty(prop) && prop !== "breakpoints") {
var dashedProp = Util.camelToKabob(prop);
var currentDataProp = prefix + "-" + dashedProp;
if (/object/.test(typeof options[prop])) {
var subOptions = options[prop];
var newPrefix = currentDataProp;
Util.setDataAttrOptionsPrefix($element, defaults[prop], subOptions, newPrefix);
} else {
// if the $element had the attribute already or the property exists
// as one of the defaults then we can write it to the element.
if ($element.attr(currentDataProp) || defaults[prop] !== undefined) {
$element.attr(currentDataProp, options[prop]);
}
}
}
}
};
Util.coerceAttrVal = function (value) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
if (/^[0-9]+$/.test(value)) {
return parseInt(value, 10);
}
if (/^[0-9]+.[0-9]+$/.test(value)) {
return parseFloat(value, 10);
}
return value;
};
// Due to jQuery, but with less argument handling
Util.extend = function (deep, target, options) {
var name, src, copy, copyIsArray, clone;
// Only deal with non-null/undefined values
if (options != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
if (deep && copy && Util.isPlainObject(copy)) {
clone = src && Util.isPlainObject(src) ? src : {};
// Never move original objects, clone them
target[name] = Util.extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
// Return the modified object
return target;
};
// Due to jQuery
Util.isPlainObject = function (obj) {
var proto, Ctor;
// taken from https://github.com/jupiter/simple-mock/pull/3/files
if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") {
return false;
}
return true;
};
// Due to jQuery
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
Util.trim = function (text) {
return text == null ? "" : (text + "").replace(rtrim, "");
};
Util.map = function (array, fn) {
return Util.reduce(array, function (acc, val, i) {
acc.push(fn(val, i));
return acc;
}, []);
};
Util.reduce = function (array, fn, acc) {
for (var i = 0; i < array.length; i++) {
acc = fn(acc, array[i], i);
}
return acc;
};
Util.log = function (msg, type) {
type = type || "log";
if (console) {
console[type](msg);
}
};
Util.mapJSONVals = function (obj, callback, key) {
var result = {};
if (obj.map) {
return obj.map(function (o) {
return Util.mapJSONVals(o, callback);
});
}
if (["string", "number", "boolean"].indexOf(typeof obj) >= 0) {
return callback(obj, key);
}
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
result[prop] = Util.mapJSONVals(obj[prop], callback, prop);
}
}
return result;
};
Util.escapeJSONVals = function (obj) {
return Util.mapJSONVals(obj, function (val) {
if (typeof val === "string") {
return escapeHTML(val);
}
return val;
});
};
// https://github.com/janl/mustache.js/blob/23beb3a8805c9a857e3ea777431481599fab503e/mustache.js#L60
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHTML(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) {
return entityMap[s];
});
}
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
var Advanceable = {
extension: {
next: function (callback) {
this.goto(this.getIndex() + 1, function () {
if (callback) {
callback();
};
this._trigger("next");
}.bind(this));
},
previous: function (callback) {
this.goto(this.getIndex() - 1, function () {
if (callback) {
callback();
};
this._trigger("previous");
}.bind(this));
}
},
extend: function (constr) {
if (!constr.prototype.goto || !constr.prototype.getIndex) {
throw new Error("Advanceable mixin requires `goto` and `getIndex` methods");
}
$.extend(constr.prototype, this.extension);
}
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.Advanceable = Advanceable;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
// TODO this should be handled in an image component
(function (exports, $) {
var Sourceable = {
extension: {
_srcArray: function (url, options, includeW) {
var policy = options.policy || options.thumbnail && options.thumbnail.policy;
var widthParam = options.widthParam;
return options.widths.map(function (w) {
var src = url + (url.indexOf("?") === -1 ? "?" : "&") + widthParam + "=" + w;
if (policy) {
src += "&impolicy=" + policy;
}
//note src width for srcset (eg ' 500w')
if (includeW) {
src += " " + w + "w";
}
return src;
});
},
_srcset: function (url, options) {
return this._srcArray(url, options, true).join(", ");
},
_fallbackSrc: function (url, options) {
return this._srcArray(url, options)[0];
},
_largestSrc: function (url, options) {
return this._srcArray(url, options).pop();
}
},
extendStatic: function (constr) {
Akamai.Util.extend(false, constr, this.extension);
}
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.Sourceable = Sourceable;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (w, undefined) {
// requestAnimationFrame pfill
var raf = function () {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) {
w.setTimeout(callback, 1000 / 60);
};
}();
/* toss scrolls and element with easing
// elem is the element to scroll
// options hash:
* left is the desired horizontal scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
* top is the desired vertical scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
* duration is the number of milliseconds the throw will take. Default is 100.
* easing is an optional custom easing function. Default is w.toss.easing. Must follow the easing function signature
*/
w.toss = function (elem, options) {
toss.tossing(elem, false);
var i = 0,
sLeft = elem.scrollLeft,
sTop = elem.scrollTop,
// Toss defaults
op = {
top: "+0",
left: "+0",
duration: 200,
easing: toss.easing,
finished: function () {}
},
endLeft,
endTop;
// Mixin based on predefined defaults
if (options) {
for (var j in op) {
if (options[j] !== undefined) {
op[j] = options[j];
}
}
}
// Convert relative values to ints
// First the left val
if (typeof op.left === "string") {
op.left = parseFloat(op.left);
endLeft = op.left + sLeft;
} else {
endLeft = op.left;
op.left = op.left - sLeft;
}
// Then the top val
if (typeof op.top === "string") {
op.top = parseFloat(op.top);
endTop = op.top + sTop;
} else {
endTop = op.top;
op.top = op.top - sTop;
}
toss.tossing(elem, true);
var startTime = new Date().getTime();
var endTime = startTime + op.duration;
var run = function () {
var curTime = new Date().getTime();
// if tossing is suddenly not true, return the callback
if (!toss.tossing(elem)) {
if (op.finished) {
op.finished();
}
}
// if the time is still less than the end of duration, keep scrolling
else if (curTime < endTime) {
i = (curTime - startTime) / op.duration * op.duration;
elem.scrollLeft = op.easing(i, sLeft, op.left, op.duration);
elem.scrollTop = op.easing(i, sTop, op.top, op.duration);
return raf(run);
}
// if time is up,
else {
elem.scrollLeft = endLeft;
elem.scrollTop = endTop;
if (op.finished) {
op.finished();
}
toss.tossing(elem, false);
}
};
raf(run);
// Return the values, post-mixin, with end values specified
return { top: endTop, left: endLeft, duration: op.duration, easing: op.easing };
};
// tossing object keeps track of currently tossing elements. true during a programatic scroll
var tossingElems = {};
toss.tossing = function (elem, state) {
if (state !== undefined) {
tossingElems[elem] = state;
}
return tossingElems[elem];
};
// Easing can use any of Robert Penner's equations (http://www.robertpenner.com/easing_terms_of_use.html). By default, toss includes ease-out-cubic
// arguments: t = current iteration, b = initial value, c = end value, d = total iterations
// use w.toss.easing to provide a custom function externally, or pass an easing function as a callback to the toss method
toss.easing = function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
};
//retain old api
toss.toss = toss;
})(this);
;(function (w, $) {
var pluginName = "snapper";
$.fn[pluginName] = function (optionsOrMethod) {
var pluginArgs = arguments;
var scrollListening = true;
// css snap points feature test.
// even if this test passes, several behaviors will still be polyfilled, such as snapping after resize, and animated advancing of slides with anchor links or next/prev links
var testProp = "scroll-snap-type";
// test latest spec first. then fallback older
var snapSupported = w.CSS && w.CSS.supports && (w.CSS.supports(testProp, "x mandatory") || w.CSS.supports(testProp, "mandatory") || w.CSS.supports("-webkit-" + testProp, "mandatory") || w.CSS.supports("-ms-" + testProp, "mandatory"));
// get the snapper_item elements whose left offsets fall within the scroll pane. Returns a wrapped array.
function itemsAtOffset(elem, offset) {
var $childNodes = $(elem).find("." + pluginName + "_item");
var containWidth = $(elem).width();
var activeItems = [];
$childNodes.each(function (i) {
if (this.offsetLeft >= offset - 5 && this.offsetLeft < offset + containWidth - 5) {
activeItems.push(this);
}
});
return $(activeItems);
}
function outerWidth($elem) {
return $elem.width() + parseFloat($elem.css("margin-left")) + parseFloat($elem.css("margin-right"));
}
function outerHeight($elem) {
return $elem.height() + parseFloat($elem.css("margin-bottom")) + parseFloat($elem.css("margin-top"));
}
// snapEvent dispatches the "snapper.snap" event.
// The snapper_item elements with left offsets that are inside the scroll viewport are listed in an array in the second callback argument's activeSlides property.
// use like this: $( ".snapper" ).bind( "snapper.snap", function( event, data ){ console.log( data.activeSlides ); } );
function snapEvent(elem, x, prefix) {
prefix = prefix ? prefix + "-" : "";
var activeSlides = itemsAtOffset(elem, x);
$(elem).trigger(pluginName + "." + prefix + "snap", { activeSlides: activeSlides });
}
// optional: include toss() in your page to get a smooth scroll, otherwise it'll just jump to the slide
function goto(elem, x, nothrow, callback) {
scrollListening = false;
snapEvent(elem, x);
var after = function () {
elem.scrollLeft = x;
$(elem).closest("." + pluginName).removeClass(pluginName + "-looping");
$(elem).trigger(pluginName + ".after-goto", {
activeSlides: itemsAtOffset(elem, x)
});
if (callback) {
callback();
};
snapEvent(elem, x, "after");
scrollListening = true;
};
// backport to old toss for compat
if (!w.toss && w.overthrow) {
w.toss = w.overthrow.toss;
}
if (typeof w.toss !== "undefined" && !nothrow) {
w.toss(elem, { left: x, finished: after });
} else {
elem.scrollLeft = x;
after();
}
}
var result, innerResult;
// Loop through snapper elements and enhance/bind events
result = this.each(function () {
if (innerResult !== undefined) {
return;
}
var self = this;
var $self = $(self);
var addNextPrev = $self.is("[data-" + pluginName + "-nextprev]");
var autoTimeout;
var $slider = $("." + pluginName + "_pane", self);
var enhancedClass = pluginName + "-enhanced";
var $itemsContain = $slider.find("." + pluginName + "_items");
var $items = $itemsContain.children();
$items.addClass(pluginName + "_item");
var numItems = $items.length;
var $nav = $("." + pluginName + "_nav", self);
var navSelectedClass = pluginName + "_nav_item-selected";
var useDeepLinking = $self.attr("data-snapper-deeplinking") !== "false";
if (typeof optionsOrMethod === "string") {
var args = Array.prototype.slice.call(pluginArgs, 1);
var index;
var itemWidth = $itemsContain.width() / numItems;
switch (optionsOrMethod) {
case "goto":
index = args[0] % numItems;
// width / items * index to make sure it goes
offset = itemWidth * index;
goto($slider[0], offset, false, function () {
// snap the scroll to the right position
snapScroll();
// invoke the callback if it was supplied
if (typeof args[1] === "function") {
args[1]();
}
});
break;
case "getIndex":
// NOTE make the scroll left value large enough to overcome
// subpixel widths
innerResult = Math.floor(($slider[0].scrollLeft + 1) / itemWidth);
break;
case "updateWidths":
updateWidths();
break;
}
return;
}
// avoid double enhance activities
if ($self.attr("data-" + pluginName + "-enhanced")) {
return;
}
// NOTE all state manipulation has to come after method invocation to
// avoid monkeying with the DOM when it's unwarranted
var $navInner = $nav.find("." + pluginName + "_nav_inner");
if (!$navInner.length) {
$navInner = $('<div class="' + pluginName + '_nav_inner"></div>').append($nav.children()).appendTo($nav);
}
// give the pane a tabindex for arrow key handling
$slider.attr("tabindex", "0");
function getAutoplayInterval() {
var autoTiming = $self.attr("data-autoplay") || $self.attr("data-snapper-autoplay");
var parseError = false;
if (autoTiming) {
try {
autoTiming = parseInt(autoTiming, 10);
} catch (e) {
parseError = true;
}
// if NaN or there was an error throw an exception
if (!autoTiming || parseError) {
var msg = "Snapper: `data-autoplay` must have an natural number value.";
throw new Error(msg);
}
}
return autoTiming;
}
// this function updates the widths of the items within the slider, and their container.
// It factors in margins and converts those to values that make sense when all items are placed in a long row
function updateWidths() {
var itemsContainStyle = $itemsContain.attr("style");
$itemsContain.attr("style", "");
var itemStyle = $items.eq(0).attr("style");
$items.eq(0).attr("style", "");
var sliderWidth = $slider.width();
var itemWidth = $items.eq(0).width();
var computed = w.getComputedStyle($items[0], null);
var itemLeftMargin = parseFloat(computed.getPropertyValue("margin-left"));
var itemRightMargin = parseFloat(computed.getPropertyValue("margin-right"));
var outerItemWidth = itemWidth + itemLeftMargin + itemRightMargin;
$items.eq(0).attr("style", itemStyle);
$itemsContain.attr("style", itemsContainStyle);
var parentWidth = numItems / Math.round(sliderWidth / outerItemWidth) * 100;
var iPercentWidth = itemWidth / sliderWidth * 100;
var iPercentRightMargin = itemRightMargin / sliderWidth * 100;
var iPercentLeftMargin = itemLeftMargin / sliderWidth * 100;
var outerPercentWidth = iPercentWidth + iPercentLeftMargin + iPercentRightMargin;
var percentAsWidth = iPercentWidth / outerPercentWidth;
var percentAsRightMargin = iPercentRightMargin / outerPercentWidth;
var percentAsLeftMargin = iPercentLeftMargin / outerPercentWidth;
$itemsContain.css("width", parentWidth + "%");
$items.css("width", 100 / numItems * percentAsWidth + "%");
$items.css("margin-left", 100 / numItems * percentAsLeftMargin + "%");
$items.css("margin-right", 100 / numItems * percentAsRightMargin + "%");
}
updateWidths();
$(self).addClass(enhancedClass);
// if the nextprev option is set, add the nextprev nav
if (addNextPrev) {
var $nextprev = $('<ul class="snapper_nextprev"><li class="snapper_nextprev_item"><a href="#prev" class="snapper_nextprev_prev">Prev</a></li><li class="snapper_nextprev_item"><a href="#next" class="snapper_nextprev_next">Next</a></li></ul>');
var $nextprevContain = $(".snapper_nextprev_contain", self);
if (!$nextprevContain.length) {
$nextprevContain = $(self);
}
$nextprev.appendTo($nextprevContain);
}
// This click binding will allow deep-linking to slides without causing the page to scroll to the carousel container
// this also supports click handling for generated next/prev links
$("a", this).bind("click", function (e) {
clearTimeout(autoTimeout);
var slideID = $(this).attr("href");
if ($(this).is(".snapper_nextprev_next")) {
e.preventDefault();
return arrowNavigate(true);
} else if ($(this).is(".snapper_nextprev_prev")) {
e.preventDefault();
return arrowNavigate(false);
}
// internal links to slides
else if (slideID.indexOf("#") === 0 && slideID.length > 1) {
e.preventDefault();
var $slide = $(slideID, self);
if ($slide.length) {
goto($slider[0], $slide[0].offsetLeft);
if (useDeepLinking && "replaceState" in w.history) {
w.history.replaceState({}, document.title, slideID);
}
}
}
});
// arrow key bindings for next/prev
$(this).bind("keydown", function (e) {
if (e.keyCode === 37 || e.keyCode === 38) {
clearTimeout(autoTimeout);
e.preventDefault();
e.stopImmediatePropagation();
arrowNavigate(false);
}
if (e.keyCode === 39 || e.keyCode === 40) {
clearTimeout(autoTimeout);
e.preventDefault();
e.stopImmediatePropagation();
arrowNavigate(true);
}
});
var snapScrollCancelled = false;
// snap to nearest slide. Useful after a scroll stops, for polyfilling snap points
function snapScroll() {
if (isTouched) {
snapScrollCancelled = true;
return;
}
var currScroll = $slider[0].scrollLeft;
var width = $itemsContain.width();
var itemWidth = $items[1] ? $items[1].offsetLeft : outerWidth($items.eq(0));
var roundedScroll = Math.round(currScroll / itemWidth) * itemWidth;
var maxScroll = width - $slider.width();
if (roundedScroll > maxScroll) {
roundedScroll = maxScroll;
}
if (currScroll !== roundedScroll) {
if (snapSupported) {
snapEvent($slider[0], roundedScroll);
snapEvent($slider[0], roundedScroll, "after");
} else {
goto($slider[0], roundedScroll);
}
} else {
goto($slider[0], roundedScroll);
}
snapScrollCancelled = false;
}
// retain snapping on resize (necessary even in scroll-snap supporting browsers currently, unfortunately)
var startSlide;
var afterResize;
function snapStay() {
var currScroll = $slider[0].scrollLeft;
var numItems = $items.length;
var width = $itemsContain.width();
if (startSlide === undefined) {
startSlide = Math.round(currScroll / width * numItems);
}
if (afterResize) {
clearTimeout(afterResize);
}
afterResize = setTimeout(function () {
updateWidths();
goto($slider[0], $items[startSlide].offsetLeft, true);
startSlide = afterResize = undefined;
}, 50);
}
$(w).bind("resize", snapStay);
// next/prev links or arrows should loop back to the other end when an extreme is reached
function arrowNavigate(forward) {
var currScroll = $slider[0].scrollLeft;
var width = $itemsContain.width();
var itemWidth = outerWidth($slider);
var maxScroll = width - itemWidth - 5;
if (forward) {
if (currScroll >= maxScroll) {
$self.addClass(pluginName + "-looping");
return first();
} else {
return next();
}
} else {
if (currScroll === 0) {
$self.addClass(pluginName + "-looping");
return last();
} else {
return prev();
}
}
}
// advance slide one full scrollpane's width forward
function next() {
goto($slider[0], $slider[0].scrollLeft + $itemsContain.width() / numItems, false, function () {
$slider.trigger(pluginName + ".after-next");
});
}
// advance slide one full scrollpane's width backwards
function prev() {
goto($slider[0], $slider[0].scrollLeft - $itemsContain.width() / numItems, false, function () {
$slider.trigger(pluginName + ".after-prev");
});
}
// go to first slide
function first() {
goto($slider[0], 0);
}
// go to last slide
function last() {
goto($slider[0], $itemsContain.width() - $slider.width());
}
// update thumbnail state on pane scroll
if ($nav.length) {
// function for scrolling to the xy of the active thumbnail
function scrollNav(elem, x, y) {
if (typeof w.toss !== "undefined") {
w.toss(elem, { left: x, top: y });
} else {
elem.scrollLeft = x;
elem.scrollTop = y;
}
}
var lastActiveItem;
function activeItem(force) {
var currTime = new Date().getTime();
if (!force && lastActiveItem && currTime - lastActiveItem < 200) {
return;
}
lastActiveItem = currTime;
var currScroll = $slider[0].scrollLeft;
var width = outerWidth($itemsContain);
var navWidth = outerWidth($nav);
var navHeight = outerHeight($nav);
var activeIndex = Math.round(currScroll / width * numItems) || 0;
var childs = $nav.find("a").removeClass(navSelectedClass);
var activeChild = childs.eq(activeIndex).addClass(navSelectedClass);
var thumbX = activeChild[0].offsetLeft - navWidth / 2;
var thumbY = activeChild[0].offsetTop - navHeight / 2;
scrollNav($navInner[0], thumbX, thumbY);
}
// set active item on init
activeItem();
$slider.bind("scroll", activeItem);
}
// apply snapping after scroll, in browsers that don't support CSS scroll-snap
var scrollStop;
var scrolling;
var lastScroll = 0;
$slider.bind("scroll", function (e) {
lastScroll = new Date().getTime();
scrolling = true;
});
setInterval(function () {
if (scrolling && lastScroll <= new Date().getTime() - 150) {
snapScroll();
if (activeItem) {
activeItem(true);
}
scrolling = false;
}
}, 150);
var isTouched = false;
// if a touch event is fired on the snapper we know the user is trying to
// interact with it and we should disable the auto play
$slider.bind("touchstart", function () {
clearTimeout(autoTimeout);
isTouched = true;
});
$slider.bind("touchend", function () {
isTouched = false;
if (snapScrollCancelled && !scrolling) {
snapScroll();
scrolling = false;
}
});
// if the `data-autoplay` attribute is assigned a natural number value
// use it to make the slides cycle until there is a user interaction
function autoplay(autoTiming) {
if (autoTiming) {
// autoTimeout is cleared in each user interaction binding
autoTimeout = setTimeout(function () {
var timeout = getAutoplayInterval();
if (timeout) {
arrowNavigate(true);
autoplay(timeout);
}
}, autoTiming);
}
}
autoplay(getAutoplayInterval());
$self.attr("data-" + pluginName + "-enhanced", true);
});
return innerResult !== undefined ? innerResult : result;
};
})(this, jQuery);
;(function (w) {
var enlarge = function () {
var $ = w.jQuery;
var pluginName = "enlarge";
$.fn[pluginName] = function (options) {
var pluginArgs = arguments;
// options
var o = $(this).data("options") || {
button: true,
hoverZoomWithoutClick: true,
delay: 300,
flyout: {
width: 200,
height: 200
},
placement: "inline",
magnification: 3
};
if (typeof options !== "string") {
// extend with passed options
o = $.extend(o, options);
$(this).data("options", o);
}
var internalResult;
var result = this.each(function () {
var $element = $(this);
var self = this;
var testimg = w.document.createElement("img");
var srcsetSupported = "srcset" in testimg;
var srcsetSizesSupported = srcsetSupported && "sizes" in testimg;
var $anchor = $(this).find("a");
if (!$anchor.length) {
throw new Error(pluginName + ": requires an anchor element with `href` for the enlarged image source");
}
// find image within container
var initialImg = $element.find("img")[0];
var targetImg = initialImg;
var imgOriginalSrc = targetImg.src;
var srcset = $(targetImg).attr("srcset");
var imgOriginalSizes = $(targetImg).attr("sizes");
var imgZoomSrc = $anchor[0].href;
var initialText = $anchor[0].innerText;
var zoomClass = pluginName + "-zoomed";
var delayClass = pluginName + "-delay";
var $contain = $(targetImg).closest(".enlarge_contain");
var $zoomContain = $contain;
var $parentPane = $(targetImg).closest(".enlarge_pane") || $element;
var $zoomParent = $(this).data("zoomParent") || $parentPane;
$(this).data("zoomParent", $zoomParent);
var zoomed = $element.data("zoomed") || false;
$element.data("zoomed", zoomed);
$element.data("lockedZoom", $element.data("lockedZoom") || false);
var lockZoomClass = pluginName + "-locked";
if (!$contain.length) {
throw new Error(pluginName + ": requires an element above the image marked with the class `enlarge_contain`");
}
// this allows for methods and changing options in subsequent calls to the plugin
if (typeof options === "string") {
var args = Array.prototype.slice.call(pluginArgs, 1);
switch (options) {
case "in":
if (!$element.data("zoomed")) {
standardToggleZoom();
}
break;
case "out":
if ($element.data("zoomed")) {
standardToggleZoom();
}
break;
case "isZoomed":
internalResult = $element.data("zoomed");
break;
case "updateOptions":
$element.data("updateOptions")(args[0]);
break;
}
return;
}
// to toggle between inline and flyout modes, we change the elements that
// the targetImg, zoomParent, and zoomContain vars refer to
function updatePlacement() {
if (o.placement === "inline") {
targetImg = initialImg;
$zoomParent = $parentPane;
$element.data("zoomParent", $zoomParent);
$zoomContain = $contain;
} else {
targetImg = $flyout.find("img")[0];
$zoomParent = $zoomContain = $flyout;
$element.data("zoomParent", $zoomParent);
}
}
// this positions the loupe or side flyout element either according to mouse/touch coordinates
// or the sides of the viewer specified
function positionFlyout() {
// set flyout width and height
$flyout.css({
"width": o.flyout.width + "px",
"height": o.flyout.height + "px",
top: "",
left: "",
"margin-left": "",
"margin-top": ""
});
// set negative left or right value to match width
var flyoutSide = o.placement.match(/left|right/);
if (flyoutSide) {
$flyout.css(flyoutSide[0], -o.flyout.width + "px");
}
// if loupe mode, center offset
var loupe = o.placement.match(/loupe/);
if (loupe) {
// loupe
$flyout.css({
"margin-left": -o.flyout.width / 2 + "px",
"margin-top": -o.flyout.height / 2 + "px"
});
}
// add class to specify positioning spot for static css to apply
$flyout[0].className = $flyout[0].className.replace(/enlarge_flyout\-[^$\s]+/, ' ');
$flyout.addClass("enlarge_flyout-" + o.placement);
}
function disable() {
if (o.disabled) {
$element.addClass("enlarge_disabled");
} else {
$element.removeClass("enlarge_disabled");
}
}
disable();
// this allows for subsequent calls to the plugin to pass an updateOptions method and object,
// which will pass through to the existing viewer on that element
$element.data("updateOptions", function (opts) {
o = $.extend(o, opts);
$(this).data("options", o);
updatePlacement();
positionFlyout();
hoverEnabled = o.hoverZoomWithoutClick;
if (o.image && o.image.sizes) {
imgOriginalSizes = o.image.sizes;
toggleImgSrc();
}
disable();
if (o.disabled && $element.data("zoomed")) {
standardToggleZoom();
}
});
// loader div holds a new image while its new source is loading
// we insert this into the dom so that srcset/sizes can calculate a best source
function addLoader() {
$contain.append('<i class="enlarge_loader"><i></i></i>');
}
// zoom state toggle boolean
function toggleZoomState() {
zoomed = !$element.data("zoomed");
$element.data("zoomed", zoomed);
}
// toggle the image source bigger or smaller
// ideally, this toggles the sizes attribute and allows the browser to select a new source from srcset
// if srcset isn't supported or sizes attribute is not provided, the link href is used for the larger source
function toggleImgSrc(after) {
after = after || function () {};
if (!zoomed) {
targetImg.sizes = imgOriginalSizes;
if (!srcsetSizesSupported) {
targetImg.src = imgOriginalSrc;
}
after();
} else {
// if the zooming is disabled do not replace with the larger source
// NOTE we don't prevent switching to the original source because we
// always want to allow the plugin to back out of the zoomed state
// when disabled
if (o.disabled) {
after();return false;
}
var zoomimg = new Image();
zoomimg.className = "enlarge_img-loading";
zoomimg.onload = function () {
targetImg.sizes = zoomimg.sizes;
if (!srcsetSizesSupported || !srcset) {
targetImg.src = imgZoomSrc;
}
$(zoomimg).remove();
after();
};
zoomimg.sizes = imgZoomWidth() + "px";
if (!srcsetSizesSupported || !srcset) {
zoomimg.src = imgZoomSrc;
} else if (srcset) {
zoomimg.srcset = srcset;
}
$(zoomimg).insertBefore(targetImg);
}
}
// scroll to the center of the zoomed image
function scrollToCenter() {
var pw = $zoomContain.width();
var ph = $zoomContain.height();
var w = targetImg.offsetWidth;
var h = targetImg.offsetHeight;
$zoomContain[0].scrollLeft = w / 2 - pw / 2;
$zoomContain[0].scrollTop = h / 2 - ph / 2;
}
// lock zoom mode allows for scrolling around normally without a cursor-follow behavior
function toggleLockZoom() {
if (!$element.data("lockedZoom")) {
// NOTE we allow the image to zoom out if functionality gets disabled
// when it's in a zoomed state
if (o.disabled) {
return false;
}
$parentPane.add($zoomParent).addClass(lockZoomClass);
$element.data("lockedZoom", lockedZoom = true);
$zoomContain.attr("tabindex", "0");
$zoomContain[0].focus();
} else {
$parentPane.add($zoomParent).removeClass(lockZoomClass);
$element.data("lockedZoom", lockedZoom = false);
$zoomContain.removeAttr("tabindex");
}
}
function imgZoomWidth() {
return $parentPane[0].offsetWidth * o.magnification;
}
// toggle magnification of image
function toggleImgZoom() {
if ($element.data("zoomed")) {
// NOTE we allow the image to zoom out if functionality gets disabled
// when it's in a zoomed state
if (o.disabled) {
return false;
}
if (o.placement === "inline") {
$contain.add($parentPane).css({ "width": $parentPane[0].offsetWidth + "px", "height": parseFloat(getComputedStyle($parentPane[0]).height) + "px" });
}
$zoomParent.addClass(zoomClass);
$(targetImg).css("width", imgZoomWidth() + "px");
$(self).trigger(pluginName + ".after-zoom-in");
} else {
$zoomParent.removeClass(zoomClass);
if (o.placement === "inline") {
$contain.add($parentPane).css({ "width": "", "height": "" });
}
$(targetImg).css("width", "");
$(self).trigger(pluginName + ".after-zoom-out");
}
}
function forceInlineMode() {
var oldO = o.placement;
if (oldO !== "inline") {
function resetPlacement() {
o.placement = oldO;
updatePlacement();
$(self).unbind(pluginName + ".after-zoom-out", resetPlacement);
}
$(self).bind(pluginName + ".after-zoom-out", resetPlacement);
o.placement = "inline";
updatePlacement();
}
}
// lock zoom mode toggle
function standardToggleZoom() {
// NOTE if the current is zoomed out and it's disabled prevent toggling
if (o.disabled && !$element.data("zoomed")) {
return false;
}
toggleZoomState();
toggleImgSrc(function () {
toggleLockZoom();
toggleImgZoom();
scrollToCenter();
});
}
var trackingOn;
var trackingTimer;
var mouseEntered;
var touchStarted;
var hoverEnabled = o.hoverZoomWithoutClick;
var lastMouseMove;
// mouseenter or touchstart handler for dragging image
function startTrackingDelay(e) {
if (e.type === "touchstart") {
touchStarted = true;
}
if (touchStarted && e.type === "mouseenter" || e.type === "mouseenter" && !touchStarted && !hoverEnabled || $element.data("lockedZoom") || mouseEntered) {
return;
}
mouseEntered = true;
$contain.addClass(delayClass);
trackingTimer = setTimeout(function () {
$contain.removeClass(delayClass);
toggleZoomState();
toggleImgSrc(function () {
toggleImgZoom();
trackingOn = true;
if (lastMouseMove) {
scrollWithMouse(lastMouseMove);
} else {
scrollWithMouse(e);
}
});
}, o.delay);
}
// mouseleave or touchend after a drag
function stopTrackingDelay(e) {
$contain.removeClass(delayClass);
clearTimeout(trackingTimer);
trackingOn = false;
if (o.hoverZoomWithoutClick === false && !touchStarted) {
hoverEnabled = false;
}
if (touchStarted && e.type === "mouseleave") {
touchStarted = false;
}
}
// mousemove or touch-drag image placement
function scrollWithMouse(e) {
// if tracking's not on yet, ignore. This allows the delay to work
if (trackingOn) {
// if the move was touch-started, and the event is mousemove, it can be ignored
// (mouse events fire along with touch events and we just want the touch)
if (touchStarted && e.type === "mousemove") {
return;
}
// normalize ev to touch or mouse
var ev = e.touches ? e.touches[0] : e;
e.preventDefault();
var x = ev.clientX - $contain[0].getBoundingClientRect().left;
var y = ev.clientY - $contain[0].getBoundingClientRect().top;
if (o.placement.match(/loupe/)) {
// offset the loupe a little differently for touch so that it's not directly beneath a finger
var mLeft = (e.touches ? -o.flyout.width / 1.3 : -o.flyout.width / 2) + "px";
var mTop = (e.touches ? -o.flyout.height / 1.3 : -o.flyout.height / 2) + "px";
requestAnimationFrame(function () {
$flyout.css({
top: y + "px",
left: x + "px",
"margin-left": mLeft,
"margin-top": mTop
});
});
}
var containWidth = $contain[0].offsetWidth;
var containHeight = $contain[0].offsetHeight;
var containScrollWidth = targetImg.offsetWidth;
var containScrollHeight = targetImg.offsetHeight;
var zoomContainWidth = $zoomContain[0].offsetWidth;
var zoomContainHeight = $zoomContain[0].offsetHeight;
var widthFactor = containWidth / zoomContainWidth;
var heightFactor = containHeight / zoomContainHeight;
$zoomContain[0].scrollLeft = x / containWidth * (containScrollWidth - zoomContainWidth);
$zoomContain[0].scrollLeft += (x / containWidth - 0.5) * zoomContainWidth;
$zoomContain[0].scrollTop = y / containHeight * (containScrollHeight - zoomContainHeight);
$zoomContain[0].scrollTop += (y / containHeight - 0.5) * zoomContainHeight;
} else {
lastMouseMove = e;
}
}
// add flyout for flyout and loupe modes
// flyout is always present, yet hidden when inline mode is active
var $flyout = $('<div class="enlarge_flyout"></div>').append($contain.clone());
$flyout.insertAfter($parentPane);
// add loader div
addLoader();
updatePlacement();
positionFlyout();
// clicking the magnify anchor toggles lock-zoom mode
$anchor.bind("keydown", function (e) {
if (e.keyCode === 13 || e.keyCode === 32) {
forceInlineMode();
}
// spacebar triggers click too
if (e.keyCode === 32) {
e.preventDefault(); // don't scroll the new focused area
$(this).trigger("click");
}
}).bind("click", function (e) {
e.preventDefault();
standardToggleZoom();
});
// on resize, if in lock zoom mode, un zoom
$(w).bind("resize", function (e) {
if ($element.data("lockedZoom")) {
standardToggleZoom();
}
});
// on click-out on the page, if in locked zoom mode, zoom out
$(w.document).bind("mouseup", function (e) {
if ($element.data("lockedZoom") && !$(e.target).closest($parentPane).length) {
standardToggleZoom();
}
});
// mouse hover and touch-drag gestures for a cursor-tracked zoom behavior
$(initialImg).bind("mouseenter touchstart", startTrackingDelay).bind("mousemove touchmove", scrollWithMouse).bind("mouseleave touchend", function (e) {
mouseEntered = false;
if (zoomed && !$element.data("lockedZoom")) {
toggleZoomState();
toggleImgSrc(function () {
toggleImgZoom();
});
}
stopTrackingDelay(e);
})
// tapping the image should trigger a lock zoom
// click will not fire after a touch-drag so it works as a tap for our needs here
.bind("click", function (e) {
e.preventDefault();
// if the click was started with a touchstart event,
// and placement is inline
// toggle the locked zoom mode
if (touchStarted && o.placement === "inline") {
standardToggleZoom();
}
if (o.hoverZoomWithoutClick === false && !touchStarted) {
hoverEnabled = !hoverEnabled;
if (hoverEnabled) {
$(this).trigger("mouseenter");
} else {
$(this).trigger("mouseleave");
}
}
});
// keyboard handling for arrows in zoom mode
$(this).bind("keydown keyup", function (e) {
if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) {
e.stopImmediatePropagation();
if (!$element.data("lockedZoom")) {
e.preventDefault();
}
} else if (e.type === "keyup" && $(this).data("lockedZoom") && e.keyCode === 27) {
//esc or backspace closes zoom
standardToggleZoom();
$anchor[0].focus();
e.stopImmediatePropagation();
}
});
// on scroll, zoom out
$parentPane.bind("scroll", function () {
if ($element.data("zoomed")) {
toggleZoomState();
toggleImgSrc(function () {
if ($element.data("lockedZoom")) {
toggleLockZoom();
}
toggleImgZoom();
});
}
});
});
return internalResult !== undefined ? internalResult : result;
};
};
if (typeof module !== "undefined") {
module.exports = enlarge;
} else {
enlarge();
}
})(typeof global !== "undefined" ? global : this);
(function (exports) {
var Image = {
defaultConfig: {
widths: ["320", "640", "800", "1024", "2048", "5000"],
sizes: "100vw",
policy: undefined,
widthParam: "imwidth"
}
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.Image = Image;
})(typeof exports === 'undefined' ? window : exports);
(function (exports, $) {
/**
* 360 degree viewer
* @class
* @alias Akamai.Spin360
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*/
var Spin360 = Akamai.Util.component("Spin360", function (element, options) {
this._comp = new Akamai.Tau(element, this._options);
});
// srcset stuff
Akamai.Sourceable.extendStatic(Spin360);
// Used in preflight to "rename" events based on the child component events
Spin360._componentEventMapping = {
"tau.auto-rotate-start": "play",
"tau.auto-rotate-stop": "pause"
};
Spin360.prototype._updateOptions = function (options) {
if (!options) {
this._options = this._originalOptions;
}
this._options = Akamai.Util.extend(true, this._options, options);
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
Akamai.Util.setDataAttrOptions(this._$el, Spin360.defaultOptions, this._options, "Spin360");
// TODO actually update the options
};
Spin360._renderImg = function (url, options) {
return "\n\t\t\t<img src=\"" + Spin360._fallbackSrc(url, options.images) + "\"\n\t\t\t\tsrcset=\"" + Spin360._srcset(url, options.images) + "\"\n\t\t\t\tsizes=\"" + options.images.sizes + "\" />\n\t\t";
};
Spin360.render = function (json, options) {
var finalOptions = Akamai.Util.options(Spin360.defaultOptions, options);
// TODO fix the string problem in shoestring
return Akamai.Util.trim("\n\t\t\t<div class=\"tau\" data-akamai-spin360 title=\"" + json.alt + "\">\n\t\t\t\t" + Akamai.Util.map(json.urls, function (url) {
return Spin360._renderImg(url, finalOptions);
}).join("\n") + "\n\t\t\t</div>\n\t\t");
};
/**
* Defines the global default options for all Spin360s on the page
* @static
* @property {Boolean} autoplay.enabled - Enable autoplay (default: false)
* @property {Integer} autoplay.delay - Delay in milliseconds after initialization before spinning begins (default: 1000)
* @property {Boolean} controls.arrows - Render controls to spin left and right (default: false)
* @property {Boolean} controls.play - Render controls to enable and disable automatic spinning (default: false)
* @property {String} controls.text.left - Left rotation control title and text (default: "Rotate Left")
* @property {String} controls.text.right - Right rotation control title and text (default: "Rotate Right")
* @property {String} controls.text.play - Spin control title and text (default: "Spin Object")
* @property {Array} images.widths - list of available widths for an image (to be combined with image.widthParam), Default: ["320","640","800","1024","2048","5000"]
* @property {String} images.sizes - value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw`, `200px`, `(min-width:1000px) 500px, 100vw`.
* @property {String} images.policy - query param value for policy, appended to &policy= per image url when specified. Values: `foo`. Default: undefined.
* @property {String} images.widthParam - query string name to use for setting each url width. Default urls will be ?imwidth=320 for example. Values: `imwidth` (default), `w`, `width`, etc.
* @property {Integer} interval - The full rotation interval in milliseconds, determines physics, (default: 3000)
* @property {Boolean} reverse - Reverse the direction of the spin (default: false)
* @property {Float} sensitivity - The speed at which the object rotates relative to user input (default: 1). At the default value of `1` the object will complete a full 360 rotation when you drag across the entire width of the spin360 component. To require less effort to complete a rotation, change this value to a higher number. For example, setting `sensitivity: 2` would complete a full 360 rotation by dragging halfway (ex. from the center to the edge).
*/
Spin360.defaultOptions = {
autoplay: {
enabled: false,
delay: 1000
},
controls: {
arrows: false,
play: false,
text: {
left: "Rotate Left",
right: "Rotate Right",
play: "Spin Object"
}
},
// unsupported, the number of frame images to create using the configured
// template
frames: 72,
images: Akamai.Image.defaultConfig,
interval: 3000,
reverse: false,
sensitivity: 1,
// unsupported, template used to generate urls when only one image is
// present in the 360 viewer
template: undefined
};
/**
* Goto to a particular frame of the spining image
* @method
* @param {Integer} index - the frame to advance to
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Spin360.prototype.goto = function (index, callback) {
this._comp.goto(index);
// NOTE goto must come before the callback because the callback will be
// used to trigger the `next` and `previous` events. The order should be
// maintained consistently as `goto` -> `next`/`previous`
this._trigger("goto");
if (callback) {
callback();
}
};
/**
* Return the current frame index
* @method
* @returns {Integer}
*/
Spin360.prototype.getIndex = function () {
return this._comp.index;
};
// Extend Carousel with Advanceable interface
Akamai.Advanceable.extend(Spin360);
/**
* Go to the next frame
* @method
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Spin360.prototype.next;
// required for JSDocs
/**
* Go to the previous frame
* @method
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Spin360.prototype.previous;
// required for JSDocs
/**
* Begin the automatic rotation of the images
* @todo support passing in an interval or speed?
* @method
* @fires Akamai.Spin360#akamai-spin360-play
* @returns {undefined}
*/
Spin360.prototype.play = function () {
this._comp.autoRotate();
};
/**
* Stop the automatic rotation of the images
* @method
* @fires Akamai.Spin360#akamai-spin360-pause
* @returns {undefined}
*/
Spin360.prototype.pause = function () {
this._comp.stopAutoRotate();
};
Spin360.prototype.getElement = function () {
return this._$el[0];
};
Spin360.States = {
Playing: 0,
Paused: 1
};
/**
* Return the current state of the spin360
* @example <caption>Spin360 states</caption>
* Akamai.Spin360.States = {
* Playing: 0,
* Paused: 1
* };
*
* @example <caption>Conditioning on states</caption>
* if( spin360.getState() === Akamai.Spin360.States.Paused ) {
* spin360.play()
* }
* @method
* @returns {Akamai.Spin360.State}
*/
Spin360.prototype.getState = function () {
// TODO expose using method in Tau
return !!this._comp.autoInterval ? Spin360.States.Playing : Spin360.States.Paused;
};
/**
* Triggered when the spin360 has stoped automatically spinning
* {@link Akamai.Spin360#pause}.
*
* @event Akamai.Spin360#akamai-spin360-stop-spin
*/
/**
* Triggered when the spin360 has started automatically spinning
* {@link Akamai.Spin360#play}.
*
* @event Akamai.Spin360#akamai-spin360-start-spin
*/
/**
* Triggered when initialization finishes
* {@link Akamai.Spin360}.
*
* @event Akamai.Spin360#akamai-spin360-init
*/
exports.Akamai = exports.Akamai || {};
exports.Akamai.Spin360 = Spin360;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function () {
function MagnifierImpl(element, options) {
throw new Error('Not allowed to instantiate MagnifierImpl');
}
MagnifierImpl.prototype.updateOptions = function (options) {
throw new Error('magnifier updateOptions not implemented');
};
MagnifierImpl.prototype.zoomIn = function () {
throw new Error('magnifier zoomIn not implemented');
};
MagnifierImpl.prototype.zoomOut = function () {
throw new Error('magnifier zoomOut not implemented');
};
MagnifierImpl.prototype.isMagnified = function () {
throw new Error('magnifier isMagnified not implemented');
};
MagnifierImpl.prototype.toggleZoom = function () {
throw new Error('magnifier toggleZoom not implemented');
};
MagnifierImpl.prototype.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
throw new Error('magnifier render not implemented');
};
var ex = typeof exports === 'undefined' ? window : exports;
ex.Akamai = ex.Akamai || {};
ex.Akamai.MagnifierImpl = MagnifierImpl;
})();
(function () {
function MagnifierImplEnlarge(element, options) {
this._$el = $(element);
this._$el.enlarge(options);
}
MagnifierImplEnlarge.prototype = Object.create(Akamai.MagnifierImpl.prototype);
MagnifierImplEnlarge.prototype.constructor = MagnifierImplEnlarge;
MagnifierImplEnlarge.prototype.updateOptions = function (options) {
this._$el.enlarge("updateOptions", options);
};
MagnifierImplEnlarge.prototype.zoomIn = function () {
this._$el.enlarge("in");
};
MagnifierImplEnlarge.prototype.zoomOut = function () {
this._$el.enlarge("out");
};
MagnifierImplEnlarge.prototype.isMagnified = function () {
return this._$el.enlarge("isZoomed");
};
MagnifierImplEnlarge.prototype.toggleZoom = function () {
if (this.isMagnified()) {
this.zoomOut();
} else {
this.zoomIn();
}
};
MagnifierImplEnlarge.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
return "\n\t\t\t<div " + dataAttr + ">\n\t\t\t\t<div class=\"enlarge_contain\">\n\t\t\t\t\t<img src=\"" + fallbackSrc + "\"\n\t\t\t\t\t\tsrcset=\"" + srcSet + "\"\n\t\t\t\t\t\tsizes=\"" + options.image.sizes + "\">\n\t\t\t\t</div>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"enlarge_btn\"\n\t\t\t\t\ttitle=\"" + options.buttonText + "\">\n\t\t\t\t\t" + options.buttonText + "\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t";
};
var ex = typeof exports === 'undefined' ? window : exports;
ex.Akamai = ex.Akamai || {};
ex.Akamai.MagnifierImplEnlarge = MagnifierImplEnlarge;
})();
(function () {
function ClickTracker(element, handler) {
var self = this;
self.element = element;
self.handler = handler;
this.reset();
}
ClickTracker.prototype.onDown = function (e) {
if (e.target == this.element) {
if (this.isDown) {
this.reset();
} else {
this.down.x = e.clientX;
this.down.y = e.clientY;
this.down.timestamp = new Date();
this.isDown = true;
}
}
};
ClickTracker.prototype.onMove = function () {
this.isMoved = true;
};
ClickTracker.prototype.onUp = function (e) {
if (this.isDown) {
this.up.x = e.clientX;
this.up.y = e.clientY;
this.up.timestamp = new Date();
if (!this.isMoved && this.down.x === this.up.x && this.down.y === this.up.y && this.up.timestamp.getTime() - this.down.timestamp.getTime() <= 500) {
this.handler();
}
}
this.reset();
};
ClickTracker.prototype.reset = function (x, y) {
this.down = {
x: -Infinity,
y: -Infinity,
timestamp: new Date(0)
};
this.isDown = false;
this.isMoved = false;
this.up = {
x: Infinity,
y: Infinity,
timestamp: new Date()
};
};
function MagnifierImplScroller(element, options) {
this._$el = $(element);
this._options = options;
this._container = this._$el.get(0);
this._content = this._$el.children().get(0);
this._imgCast = $(this._content).find('div').get(0);
this._zoomInBtn = this._$el.find("a.scroller-zoom-in").get(0);
this._zoomOutBtn = this._$el.find("a.scroller-zoom-out").get(0);
this._magnification = 1;
var self = this;
// Initialize Scroller
this.scroller = new Scroller(this._renderer(), {
zooming: options.enabled,
minZoom: 1,
maxZoom: options.magnification,
animationDuration: options.animationDuration
});
var rect = this._container.getBoundingClientRect();
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
this._installEventHandlers();
this._updateButtonStates();
setTimeout(function () {
self._onResize();
});
}
MagnifierImplScroller.prototype = Object.create(Akamai.MagnifierImpl.prototype);
MagnifierImplScroller.prototype.constructor = MagnifierImplScroller;
MagnifierImplScroller.prototype.updateOptions = function (options) {
var rect = this._container.getBoundingClientRect();
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
this._onResize();
this._zoomBy(1 / this._options.magnification, false);
};
MagnifierImplScroller.prototype.zoomIn = function () {
this._zoomBy(this._options.incrementalZoomFactor);
};
MagnifierImplScroller.prototype.cyclicZoom = function () {
var self = this;
if (this._magnification >= this._options.magnification) {
this._zoomBy(1 / this._options.magnification);
} else {
this.zoomIn();
}
};
MagnifierImplScroller.prototype.zoomOut = function () {
this._zoomBy(1 / this._options.incrementalZoomFactor);
};
MagnifierImplScroller.prototype.isMagnified = function () {
return this._magnification > 1;
};
MagnifierImplScroller.prototype.toggleZoom = function () {
throw new Error('zoom toggle behavior is undefined in scroller mode');
};
MagnifierImplScroller.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
return "\n\t\t\t<div " + dataAttr + ">\n\t\t\t\t<div>\n\t\t\t\t\t<img src=\"" + fallbackSrc + "\"\n\t\t\t\t\tsrcset=\"" + srcSet + "\"\n\t\t\t\t\tsizes=\"" + options.image.sizes + "\">\n\t\t\t\t\t<div></div>\n\t\t\t\t</div>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"scroller-zoom-in\"\n\t\t\t\t\ttitle=\"" + options.zoomInBtnText + "\">\n\t\t\t\t\t" + options.zoomInBtnText + "\n\t\t\t\t</a>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"scroller-zoom-out\"\n\t\t\t\t\ttitle=\"" + options.zoomOutBtnText + "\">\n\t\t\t\t\t" + options.zoomOutBtnText + "\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t";
};
MagnifierImplScroller.prototype._zoomBy = function (magnification, animate) {
var self = this,
magnification = Number(magnification);
if (typeof animate === "undefined") {
animate = this._options.animateZoom;
}
if (magnification === 1 || // if we are magnifying by 1 OR
this._magnification === 1 && magnification < 1 || // fully zoomed out and still attempting a zoom out OR
this._magnification === this._options.magnification && magnification > 1) {
// fully zoomed in and still attempting a zoom in
return;
}
this.scroller.zoomTo(this._magnification * magnification, this._options.animateZoom);
setTimeout(function () {
self._onZoom();
}, this._options.animationDuration);
};
MagnifierImplScroller.prototype._onZoom = function () {
var oldMagnification = this._magnification;
this._magnification = Number(this.scroller.getValues().zoom.toFixed(2));
this._updateButtonStates();
if (oldMagnification > this._magnification) {
this._$el.trigger("scroller.after-zoom-out");
} else if (oldMagnification < this._magnification) {
this._$el.trigger("scroller.after-zoom-in");
}
};
MagnifierImplScroller.prototype._renderer = function () {
var docStyle = document.documentElement.style,
self = this;
var engine;
if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
engine = 'presto';
} else if ('MozAppearance' in docStyle) {
engine = 'gecko';
} else if ('WebkitAppearance' in docStyle) {
engine = 'webkit';
} else if (typeof navigator.cpuClass === 'string') {
engine = 'trident';
}
var vendorPrefix = {
trident: 'ms',
gecko: 'Moz',
webkit: 'Webkit',
presto: 'O'
}[engine];
var helperElem = document.createElement("div");
var undef;
var perspectiveProperty = vendorPrefix + "Perspective";
var transformProperty = vendorPrefix + "Transform";
if (helperElem.style[perspectiveProperty] !== undef) {
return function (left, top, zoom) {
self._content.style[transformProperty] = 'translate3d(' + -left + 'px,' + -top + 'px,0) scale(' + zoom + ')';
};
} else if (helperElem.style[transformProperty] !== undef) {
return function (left, top, zoom) {
self._content.style[transformProperty] = 'translate(' + -left + 'px,' + -top + 'px) scale(' + zoom + ')';
};
} else {
return function (left, top, zoom) {
self._content.style.marginLeft = left ? -left / zoom + 'px' : '';
self._content.style.marginTop = top ? -top / zoom + 'px' : '';
self._content.style.zoom = zoom || '';
};
}
};
MagnifierImplScroller.prototype._onResize = function () {
this.scroller.setDimensions(this._container.clientWidth, this._container.clientHeight, this._container.clientWidth, this._container.clientHeight);
};
MagnifierImplScroller.prototype._installEventHandlers = function () {
var self = this,
clickTracker = new ClickTracker(this._imgCast, function () {
self.cyclicZoom();
});
window.addEventListener("resize", function (e) {
self._onResize(e);
}, false);
this._zoomInBtn.addEventListener("click", function (e) {
e.preventDefault();
self.zoomIn();
}, false);
this._zoomOutBtn.addEventListener("click", function (e) {
e.preventDefault();
self.zoomOut();
}, false);
if ('ontouchstart' in window) {
self._container.addEventListener("touchstart", function (e) {
// Don't react if initial down happens on one of the zoom buttons
if (e.touches[0].target == self._$el.find('a.scroller-zoom-in').get(0) || e.touches[0].target == self._$el.find('a.scroller-zoom-out').get(0)) {
return;
}
// Don't react if initial down happens on a form element
if (e.touches[0] && e.touches[0].target && e.touches[0].target.tagName.match(/input|textarea|select/i)) {
return;
}
self.scroller.doTouchStart(e.touches, e.timeStamp);
for (var i = 0; i < e.touches.length; i++) {
clickTracker.onDown(e.touches[i]);
}
e.preventDefault();
}, false);
document.addEventListener("touchmove", function (e) {
self.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
clickTracker.onMove();
}, false);
document.addEventListener("touchend", function (e) {
self.scroller.doTouchEnd(e.timeStamp);
// handle any zoom that may have occured
self._onZoom();
for (var i = 0; i < e.changedTouches.length; i++) {
clickTracker.onUp(e.changedTouches[i]);
}
}, false);
document.addEventListener("touchcancel", function (e) {
self.scroller.doTouchEnd(e.timeStamp);
// handle any zoom that may have occured
self._onZoom();
for (var i = 0; i < e.changedTouches.length; i++) {
clickTracker.onUp(e.changedTouches[i]);
}
}, false);
} else {
self._container.addEventListener("mousedown", function (e) {
if (e.target.tagName.match(/input|textarea|select/i)) {
return;
}
if (self.scroller.__clientWidth === 0) {
self._onResize();
}
self.scroller.doTouchStart([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
clickTracker.onDown(e);
}, false);
document.addEventListener("mousemove", function (e) {
if (!clickTracker.isDown) {
return;
}
self.scroller.doTouchMove([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
clickTracker.onMove();
}, false);
document.addEventListener("mouseup", function (e) {
if (!clickTracker.isDown) {
return;
}
self.scroller.doTouchEnd(e.timeStamp);
clickTracker.onUp(e);
}, false);
// self._container.addEventListener(navigator.userAgent.indexOf("Firefox") > -1 ? "DOMMouseScroll" : "mousewheel", function(e) {
// e.preventDefault();
// self.scroller.doMouseZoom(e.detail ? (e.detail * -120) : e.wheelDelta, e.timeStamp, e.pageX, e.pageY);
// }, false);
}
};
MagnifierImplScroller.prototype._updateButtonStates = function () {
if (this._magnification === 1) {
$(this._$el).addClass('scroller-zoom-out-max');
$(this._$el).removeClass('scroller-zoom-in-max');
} else if (this._magnification === this._options.magnification) {
$(this._$el).addClass('scroller-zoom-in-max');
$(this._$el).removeClass('scroller-zoom-out-max');
} else {
$(this._$el).removeClass('scroller-zoom-in-max');
$(this._$el).removeClass('scroller-zoom-out-max');
}
};
var ex = typeof exports === 'undefined' ? window : exports;
ex.Akamai = ex.Akamai || {};
ex.Akamai.MagnifierImplScroller = MagnifierImplScroller;
})();
(function (exports, $) {
/**
* Image magnifier
* @class
* @alias Akamai.Magnifier
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*/
var Magnifier = Akamai.Util.component("Magnifier", function (element, options) {
// compat with enlarge `disabled` option
this._options.disabled = !this._options.enabled;
if (this._options.mode === Magnifier.MODE_ANIMATED_ZOOM) {
this._impl = new Akamai.MagnifierImplScroller(element, options);
} else {
this._impl = new Akamai.MagnifierImplEnlarge(element, options);
}
});
Magnifier.prototype._updateOptions = function (options) {
if (!options) {
this._options = this._originalOptions;
}
this._options = Akamai.Util.extend(true, this._options, options);
// compat with enlarge `disabled` option
this._options.disabled = !this._options.enabled;
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
Akamai.Util.setDataAttrOptions(this._$el, Magnifier.defaultOptions, this._options, "Magnifier");
this._impl.updateOptions(this._options);
};
// Used in preflight to "rename" events based on the child component events
Magnifier._componentEventMapping = {
"enlarge.after-zoom-in": "in",
"enlarge.after-zoom-out": "out",
"scroller.after-zoom-in": "in",
"scroller.after-zoom-out": "out"
};
Magnifier.MODE_HOVER_ZOOM = 'hoverzoom';
Magnifier.MODE_ANIMATED_ZOOM = 'animatedzoom';
/**
* Defines the global default options for all Magnifiers on the page
* @static
* @property {String} mode - Set what mode to run the magnifier in. There are two modes available: `hoverzoom` and `animatedzoom`. The `hoverzoom` mode provides the ability to magnify and pan the image by just hovering your mouse over the image. It also provides a `flyout` option where a clip of the zoomed in image is displayed on a separate widget floating somewhere around (configurable) the image. The `animatedzoom` mode only does inline magnification but provides smooth animation between magnification levels, it also allows for incremental zooming.
* @property {Boolean} button - Whether to show a button for toggling magnification (default: true)
* @property {Boolean} enabled - Enabled/disable magnification (default: true, breakpoints supported)
* @property {Integer} magnification - The scale factor to magnify the image: `2`, `3` (default), `4`, `4.5`, etc
* @property {Array} image.widths - List of available widths for an image (to be combined with image.widthParam) - (default: ["320","640","800","1024","2048","5000"])
* @property {String} image.sizes - Value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw` (default), `200px`, `(min-width:1000px) 500px, 100vw` - (default: `100vw`, breakpoints supported)
* @property {String} image.policy - Query param value for policy, appended to &policy= per image url when specified. Values: `foo`. (default: undefined)
* @property {Integer} delay - Only applicable in `hoverzoom` mode. The time delay in milliseconds between mouse hover and magnification (default: 300, breakpoints supported)
* @property {String} buttonText - Only applicable in `hoverzoom` mode. Text for the zoom button. Also used for its title attribute. (default: "Toggle Image Magnification")
* @property {Integer} flyout.width - Only applicable in `hoverzoom` mode. Width of the flyout image (default: 200)
* @property {Integer} flyout.height - Only applicable in `hoverzoom` mode. Height of the flyout image (default: 200)
* @property {Boolean} hoverZoomWithoutClick - Only applicable in `hoverzoom` mode. Zoom starts on mouse hover with no click needed (default: true; false will require a click to hover-zoom)
* @property {String} placement - Only applicable in `hoverzoom` mode. Placement of the magnified image: `inline` , `flyoutloupe`, `flyouttopleft`,`flyoutbottomleft` ,`flyouttopright` and `flyoutbottomright` - (default: inline, breakpoins supported)
* @property {Float} incrementalZoomFactor - Only applicable in `animatedzoom` mode. A number by which to incrementally zoom up until the specified `magnification`, default `3`. For example a `magnification` of `4` and an `incrementalZoomFactor` of `2` will zoom the image in `2` steps
* @property {Boolean} animateZoom - Only applicable in `animatedzoom` mode. Animates the magnification process. Default `true`
* @property {Integer} animationDuration - Only applicable in `animatedzoom` mode. If `animateZoom` is true, this specifies the length of the animation in milliseconds. Default `250`
* @property {String} zoomInBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom in button. Default `Zoom In`
* @property {String} zoomOutBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom out button. Default `Zoom Out`
*/
Magnifier.defaultOptions = {
// general options
mode: Magnifier.MODE_HOVER_ZOOM,
button: true,
enabled: true,
magnification: 3,
// image options
image: Akamai.Image.defaultConfig,
// hoverzoom options
delay: 300,
buttonText: "Toggle Image Magnification",
flyout: {
width: 200,
height: 200
},
hoverZoomWithoutClick: true,
placement: "inline",
// animatedzoom options
incrementalZoomFactor: 3,
animateZoom: true,
animationDuration: 250,
zoomInBtnText: 'Zoom In',
zoomOutBtnText: 'Zoom Out'
};
// srcset stuff
Akamai.Sourceable.extendStatic(Magnifier);
Magnifier.render = function (json, options) {
var finalOptions = Akamai.Util.options(Magnifier.defaultOptions, options);
var dataAttr = Magnifier._dataAttr;
var fallbackSrc = Magnifier._fallbackSrc(json.url, finalOptions.image);
var largestSrc = Magnifier._largestSrc(json.url, finalOptions.image);
var srcSet = Magnifier._srcset(json.url, finalOptions.image);
var sizes = finalOptions.image.sizes;
var impl = finalOptions.mode === Magnifier.MODE_ANIMATED_ZOOM ? Akamai.MagnifierImplScroller : Akamai.MagnifierImplEnlarge;
return impl.render(finalOptions, dataAttr, fallbackSrc, largestSrc, srcSet, sizes);
};
/**
* Enter magnifier mode
* @method
* @fires Akamai.Magnifier#akamai-magnifier-in
* @returns {undefined}
*/
Magnifier.prototype.in = function () {
this._impl.zoomIn();
};
/**
* Exit magnifier mode
* @method
* @fires Akamai.Magnifier#akamai-magnifier-out
* @returns {undefined}
*/
Magnifier.prototype.out = function () {
this._impl.zoomOut();
};
/**
* Return the current state of the magnifier
* @method
* @returns {Boolean}
*/
Magnifier.prototype.isMagnified = function () {
return this._impl.isMagnified();
};
/**
* Toggle the state of the magnifier
* @method
* @fires Akamai.Magnifier#akamai-magnifier-in
* @fires Akamai.Magnifier#akamai-magnifier-out
* @returns {undefined}
*/
Magnifier.prototype.toggle = function () {
this._impl.toggleZoom();
};
/**
* Triggered when the magnifier has completed it transition to
* a new index due to user interaction or a call to {@link Akamai.Carouse#in}.
*
* @event Akamai.Magnifier#akamai-magnifier-in
*/
/**
* Triggered when the magnifier has completed it transition to
* a new index due to user interaction or a call to {@link Akamai.Carouse#out}.
*
* @event Akamai.Magnifier#akamai-magnifier-out
*/
/**
* Triggered when initialization finishes
* {@link Akamai.Magnifier}.
*
* @event Akamai.Magnifier#akamai-magnifier-init
*/
exports.Akamai = exports.Akamai || {};
exports.Akamai.Magnifier = Magnifier;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
var VideoImpl = Akamai.Util.component("VideoImpl", function (element, options) {
throw new Error('Not allowed to instantiate VideoImpl');
});
VideoImpl.render = function (json, options) {
throw new Error('VideoImpl.render not implemented');
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.VideoImpl = VideoImpl;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
var VideoImplPristine = Akamai.Util.component("VideoImplPristine", function (element, options) {});
VideoImplPristine.prototype = Object.create(Akamai.VideoImpl.prototype);
VideoImplPristine.prototype.constructor = VideoImplPristine;
VideoImplPristine.render = function (json, options) {
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
var url = json.url;
var poster = json.poster || "";
var mime = json.mime ? 'type="' + json.mime + '"' : '';
var attrs = [finalOptions.loop ? "loop" : "", finalOptions.autoplay ? "autoplay" : "", finalOptions.controls ? "controls" : "", finalOptions.muted ? "muted" : "", "playsinline"];
return Akamai.Util.trim("\n\t\t\t<div " + Akamai.Video._dataAttr + ">\n\t\t\t\t<video " + attrs.join(" ") + " poster=\"" + poster + "\" preload=\"metadata\">\n\t\t\t\t\t<source src=\"" + url + "\" " + mime + " />\n\t\t\t\t</video>\n\t\t\t</div>\n\t\t");
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.VideoImplPristine = VideoImplPristine;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
var VideoImplIm = Akamai.Util.component("VideoImplIm", function (element, options) {
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
VideoImplIm._validateOptions(finalOptions);
});
VideoImplIm.prototype = Object.create(Akamai.VideoImpl.prototype);
VideoImplIm.prototype.constructor = VideoImplIm;
VideoImplIm._validateOptions = function (options) {
if (!Array.isArray(options.sizes) || options.sizes.length !== 3) {
throw new Error('Akamai.Video sizes must have three entries');
}
if (options.sizes.some(function (size) {
return isNaN(size);
})) {
throw new Error('Akamai.VideoImplIm all sizes must be numbers');
}
};
VideoImplIm.render = function (json, options) {
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
VideoImplIm._validateOptions(finalOptions);
var url = json.url;
var poster = json.poster || "";
var width;
var viewPortWidth = window.innerWidth;
if (viewPortWidth < 992) {
width = finalOptions.sizes[0];
} else if (viewPortWidth < 1200) {
width = finalOptions.sizes[1];
} else {
width = finalOptions.sizes[2];
}
var attrs = [finalOptions.loop ? "loop" : "", finalOptions.autoplay ? "autoplay" : "", finalOptions.controls ? "controls" : "", finalOptions.muted ? "muted" : "", "playsinline"];
var joiner = url.indexOf('?') === -1 ? '?' : '&';
return Akamai.Util.trim("\n\t\t\t<div " + Akamai.Video._dataAttr + ">\n\t\t\t\t<video " + attrs.join(" ") + " poster=\"" + poster + "\" preload=\"metadata\">\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=vp9&imwidth=" + width + "\" type=\"video/webm\" />\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=h265&imwidth=" + width + "\" type=\"video/mp4; codecs=hevc\" />\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=h264&imwidth=" + width + "\" type=\"video/mp4\" />\n\t\t\t\t</video>\n\t\t\t</div>\n\t\t");
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.VideoImplIm = VideoImplIm;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
/**
* Video component
* @class
* @alias Akamai.Video
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*/
var Video = Akamai.Util.component("Video", function (element, options) {
// TODO
this._comp = this._$el.length && undefined;
this._$videoElement = this._$el.find("video");
this._videoElement = this._$videoElement[0];
if (!this._videoElement) {
throw new Error("Akamai.Video requires a child HTML Video element");
}
if (!this._videoElement.play || !this._videoElement.pause) {
this._unsupportedAPI = true;
this._unsupported();
return;
}
// get the initial state (could be autoplaying on render)
this._setState(this._videoElement.paused ? Video.States.Paused : Video.States.Playing);
// state bindings
this._$el.bind("akamai-video-play", function () {
this._setState(Video.States.Playing);
}.bind(this)).bind("akamai-video-pause", function () {
this._setState(Video.States.Paused);
}.bind(this));
this._createPlayButton();
});
Video.MODE_IM = 'im';
Video.MODE_PRISTINE = 'pristine';
Video._componentEventMapping = {
"play": {
to: "play",
selector: "video"
},
"pause": {
to: "pause",
selector: "video"
},
"seeked": {
to: "seek",
selector: "video"
}
};
Video.prototype._updateOptions = function (options) {
if (!options) {
this._options = this._originalOptions;
}
this._options = Akamai.Util.extend(true, this._options, options);
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
Akamai.Util.setDataAttrOptions(this._$el, Video.defaultOptions, this._options, "Video");
// TODO see Magnifier for example
};
/**
* Defines the global default options for all Spin360s on the page
* @static
* @property {Boolean} autoplay - Automatically play the video on load (default: false)
* @property {Boolean} controls - Display the video controls (default: true)
* @property {Boolean} loop - Restart the video when it reaches the end (default: false)
* @property {Boolean} muted - Mute the video (default: true)
* @property {String} mode - What video component to use. `im` will let you use any size pristine video and will automatically request & generate the right size when the page is loaded. `pristine` just passes through the original video into the video player and this is what will always play on the users device. Default: `pristine`
* @property {Array} sizes - Video sizes (widths) to use for different screen widths. Defaults: [1920 (large screens), 1280 (tablet), 854 (mobile), ]
*/
Video.defaultOptions = {
autoplay: false,
controls: true,
loop: false,
muted: true,
mode: Video.MODE_PRISTINE,
sizes: [854, 1280, 1920]
};
Video.render = function (json, options) {
var impl = options && options.mode === Video.MODE_IM ? Akamai.VideoImplIm : Akamai.VideoImplPristine;
return impl.render(json, options);
};
Video.States = {
Playing: 0,
Paused: 1
};
Video.prototype._unsupported = function () {
if (this._unsupportedAPI) {
Akamai.Util.log("Video: video API not supported", 'error');
}
return this._unsupportedAPI;
};
/**
* Play the video, idempotent
* @method
* @fires Akamai.Video#akamai-video-play
* @returns {undefined}
*/
Video.prototype.play = function () {
if (this._unsupported()) {
return;
}
this._videoElement.play();
};
/**
* Pause the video
* @method
* @fires Akamai.Video#akamai-video-pause
* @returns {undefined}
*/
Video.prototype.pause = function () {
if (this._unsupported()) {
return;
}
this._videoElement.pause();
};
/**
* Return the current state of the video
* @example <caption>Video states</caption>
* Akamai.Video.States = {
* Playing: 0,
* Paused: 1
* };
*
* @example <caption>Conditioning on states</caption>
* if( video.getState() === Akamai.Video.States.Paused ) {
* video.play()
* }
* @method
* @returns {Akamai.Video.State}
*/
Video.prototype.getState = function () {
return this._state;
};
Video.prototype._setState = function (value) {
this._state = value;
};
/**
* Seek to the input percentage.
* @method
* @fires Akamai.Video#akamai-video-seek
* @param {Integer} percent - value between 0 and 100 percent for seeking
* @returns {undefined}
*/
Video.prototype.seek = function (percent) {
if (percent < 0 || 100 < percent) {
throw new Error("seek takes a an integer between 0 and 100");
}
var newTime = percent / 100 * (this._videoElement.duration || 1);
this._videoElement.currentTime = newTime;
};
/**
* Set whether the video should loop or not
* @method
* @param {Boolean} value - The value true or false
* @returns {undefined}
*/
Video.prototype.setLoop = function (value) {
this._videoElement.loop = value;
};
Video.prototype.getElement = function () {
return this._$el[0];
};
// Borrowed from https://codepen.io/chrisnager/pen/jPrJgQ
Video.prototype._createPlayButton = function () {
var videoPlayButton;
var videoWrapper = this._$el[0];
var video = this._videoElement;
if (this._options.autoplay) {
return;
}
// TODO move to render?
videoWrapper.insertAdjacentHTML('beforeend', "\n\t\t\t<svg viewBox=\"0 0 200 200\" alt=\"Play video\">\n\t\t\t\t<circle cx=\"100\" cy=\"100\" r=\"90\" fill=\"none\" stroke-width=\"15\" stroke=\"#fff\"/>\n\t\t\t\t<polygon points=\"70, 55 70, 145 145, 100\" fill=\"#fff\"/>\n\t\t\t</svg>\n\t\t");
video.removeAttribute('controls');
videoPlayButton = this._$el.find('svg')[0];
videoPlayButton.addEventListener('click', function () {
video.play();
videoPlayButton.classList.add('is-hidden');
if (this._options.controls) {
video.setAttribute('controls', 'controls');
}
}.bind(this));
};
/**
* Triggered when the video has been played. See {@link Akamai.Video#play}.
*
* @event Akamai.Video#akamai-video-play
*/
/**
* Triggered when the video has been paused. See {@link Akamai.Video#pause}.
*
* @event Akamai.Video#akamai-video-pause
*/
/**
* Triggered when the video has seeked to a position. See {@link Akamai.Video#seek}.
*
* @event Akamai.Video#akamai-video-seek
*/
/**
* Triggered when initialization finishes
* {@link Akamai.Video}.
*
* @event Akamai.Video#akamai-video-init
*/
exports.Akamai = exports.Akamai || {};
exports.Akamai.Video = Video;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
/**
* Image carousel
* @class
* @alias Akamai.Carousel
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*/
var Carousel = Akamai.Util.component("Carousel", function (element, options) {
this._spins = Akamai.Spin360.createMany(this._$el[0], this._options.spin360);
this._magnifiers = Akamai.Magnifier.createMany(this._$el[0], this._options.magnifier);
this._videos = Akamai.Video.createMany(this._$el[0], this._options.video);
this._$el.snapper(this._options);
this._setAspectRatio();
this._bindAspectAttributes();
if (this._options.slideshow.autostart) {
setTimeout(function () {
this.startSlideshow();
}.bind(this), this._options.slideshow.interval);
}
// NOTE !! the following two bindings must happen in order, the
// autoplayables binding relies on the attribut
// add item attrs and maintain the active item attributes to determine the
// active item state
this._activeItemAttributes();
this._$el.bind("akamai-carousel-goto", this._activeItemAttributes.bind(this));
// TODO pause autoplay videos that are not visible
this._handleAutoplayables();
this._$el.bind("akamai-carousel-goto", this._handleAutoplayables.bind(this));
this._$el.bind("tau.touch-tracking-start", function () {
this._$el.find(".snapper_pane").addClass("no-scroll");
}.bind(this)).bind("tau.touch-tracking-stop", function () {
this._$el.find(".snapper_pane").removeClass("no-scroll");
}.bind(this));
// when child components are doing things, stop the carousel from
// automatically advancing
this._$el.bind(this.constructor._stopSlideshowEvents.join(" "), this.stopSlideshow.bind(this));
});
Carousel._stopSlideshowEvents = ["akamai-magnifier-in", "akamai-magnifier-out", "akamai-spin360-goto", "akamai-video-play", "akamai-video-seek"];
// clearly there should be an autoplayable interface
Carousel.prototype._handleAutoplayables = function () {
var isParentActive = function (comp) {
return !!$(comp.getElement()).closest("[" + this.constructor.activeItemAttr + "]").length;
}.bind(this);
this._videos.concat(this._spins).forEach(function (comp) {
if (isParentActive(comp) && comp._carouselWasPlaying) {
comp.play();
} else {
if (comp.getState() == comp.constructor.States.Playing) {
comp._carouselWasPlaying = true;
comp.pause();
} else {
comp._carouselWasPlaying = false;
}
}
}.bind(this));
};
// Unique counter for IDs
Carousel.counter = 0;
Carousel.prototype._updateOptions = function (options) {
if (!options) {
this._options = this._originalOptions;
}
// update the current options
this._options = Akamai.Util.extend(true, this._options, options);
// update the options for each of the subcomponents
var update = function (comp, name) {
comp._updateOptions(this._options[name]);
}.bind(this);
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
Akamai.Util.setDataAttrOptions(this._$el, Carousel.defaultOptions, this._options, "Carousel");
this._$el.snapper("updateOptions", options);
this._spins.forEach(function (c) {
update(c, "spin360");
});
this._magnifiers.forEach(function (c) {
update(c, "magnifier");
});
this._videos.forEach(function (c) {
update(c, "video");
});
this._setAspectRatio();
};
// Used in preflight to "rename" events based on the child component events
Carousel._componentEventMapping = {
"snapper.after-snap": "goto",
"snapper.snap": "snap",
"snapper.after-next": "next",
"snapper.after-prev": "previous"
};
/**
* Defines the global default options for all Carousels on the page
* @static
* @property {Boolean} arrows - Show carousel controls - (default: true)
* @property {Number} aspectratio - Specify a percentage-based height for the carousel, relative to the width. Values: `false`, `100`, `45.6`, `78` - (default `false`, breakpoints supported)
* @property {Integer} slideshow.interval - Time in milliseconds between slide advances - (default: 4000, breakpoints supported)
* @property {Integer} slideshow.autostart - Start the slideshow on instantiation - (default: false)
* @property {String} thumbnail.placement - Placement relative to the slide container: `left`, `right`, `bottom` (default: `bottom`, breakpoints supported)
* @property {String} thumbnail.type - Type of thumbnail: `dots`, `none`, `images` - (default: `images`, breakpoints supported)
* @property {String} thumbnail.policy - String to use for query parameter, ex: "&policy=" (default: undefined)
* @property {String} thumbnail.sizes - Sizes attribute value to use if thumbnail policy is set. Values: `300px`, `200px`, `(min-width:1000px) 300px, 100px` - (default: `300px`)
* @property {Object} images - Defaults to the Magnifier image option's settings
*/
Carousel.defaultOptions = {
arrows: true,
aspectratio: false,
slideshow: {
interval: 4000,
autostart: false
},
thumbnail: {
placement: "bottom",
type: "images",
policy: undefined,
sizes: "300px"
},
images: Akamai.Image.defaultConfig
};
Carousel.renderMapping = {
image: Akamai.Magnifier,
spin360: Akamai.Spin360,
video: Akamai.Video
};
Carousel._typeMapping = {
image: "magnifier"
};
Carousel._uniqueItemId = function (item, i) {
return "akamai-carousel-" + Carousel.counter + "-" + item.type + "-" + i;
};
// srcset stuff
Akamai.Sourceable.extendStatic(Carousel);
Carousel._renderItem = function (item, i, options) {
var mappedType = Carousel._typeMapping[item.type] || item.type;
if (!Carousel.renderMapping[item.type]) {
throw new Error("\n\t\t\t\titem type " + item.type + " at index " + i + " must be 'image', 'spin360', or 'video'\n\t\t\t");
}
return "\n\t\t\t<div class=\"snapper_item\" id=\"" + Carousel._uniqueItemId(item, i) + "\">\n\t\t\t\t" + Carousel.renderMapping[item.type].render(item, options[mappedType]) + "\n\t\t\t</div>\n\t\t";
};
Carousel._renderThumbnailAnchor = function (item, data, i, options) {
var thumbUrl = "";
var altText = item.alt || "";
// use the video poster, the canonical url, or the first in a sequence
if (item.type === 'video') {
thumbUrl = item.poster || data.reduce(function (acc, i) {
return acc || Carousel._thumbUrl(i);
}, "");
} else {
thumbUrl = Carousel._thumbUrl(item);
}
// TODO this sucks
options.images.policy = options.thumbnail.policy;
var src = Carousel._fallbackSrc(thumbUrl, options.images);
var srcset = Carousel._srcset(thumbUrl, options.images);
var sizes = options.images.sizes;
// if there's a thumbnail policy, the thumbnails will be fresh image requests,
// so they should have better sizes attribute values
if (options.thumbnail.policy) {
sizes = options.thumbnail.sizes;
}
var attrs = [Carousel._dataAttr + "-thumb-type=\"" + item.type + "\"", item.type == "video" && !item.poster ? "${Carousel._dataAttr}-thumb-noposter" : ""];
return "\n\t\t\t<a href=\"#" + Carousel._uniqueItemId(item, i) + "\" " + attrs.join(" ") + ">\n\t\t\t\t<img src=\"" + src + "\" srcset=\"" + srcset + "\" sizes=\"" + sizes + "\" alt=\"" + altText + "\" title=\"Scroll to " + item.type + " " + i + "\" />\n\t\t\t</a>\n\t\t";
};
Carousel._thumbUrl = function (item) {
return item.type === 'video' ? item.poster : item.url || item.urls && item.urls[0];
};
Carousel._renderThumbnails = function (data, options) {
if (data.length <= 1) {
return "";
};
return "\n\t\t\t<div class=\"snapper_nav\">\n\t\t\t\t<div class=\"snapper_nav_inner\">\n\t\t\t\t\t" + Akamai.Util.map(data, function (item, i) {
return Carousel._renderThumbnailAnchor(item, data, i, options);
}).join("\n") + "\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t";
};
Carousel.prototype._setAspectAttributes = function ($el) {
var orientationAttr = Carousel._dataAttr + "-aspectratio-orientation";
if ($el.height() > $el.width()) {
$el.attr(orientationAttr, "portrait");
} else if ($el.height() < $el.width()) {
$el.attr(orientationAttr, "landscape");
} else {
if ($el.parent().height() >= $el.parent().width()) {
$el.attr(orientationAttr, "landscape");
} else {
$el.attr(orientationAttr, "portrait");
}
}
};
Carousel.prototype._setAspectRatio = function () {
var value = this._options.aspectratio || 0;
var $items = this._$el.find(".snapper_item");
$items.css("padding-top", value / $items.length + "%");
};
Carousel.prototype._bindAspectAttributes = function () {
if (this._options.aspectratio === false) {
return;
}
var value = this._options.aspectratio || 0;
var self = this;
var $items = this._$el.find(".snapper_item");
$items.each(function () {
var loadBound;
var $item = $(this);
var $loadable = $item.find("img, video").eq(0);
$loadable.bind("load loadedmetadata", loadBinding = function () {
clearTimeout(loadBound);
if ($item.is("[" + Akamai.Spin360._dataAttr + "]")) {
// TODO it's not always a canvas, sometimes it's a collection of
// images based on settings
self._setAspectAttributes($item.find("canvas"));
} else {
self._setAspectAttributes($loadable);
}
});
loadBound = setTimeout(loadBinding, 5000);
});
$items.eq(0).find("img,video").eq(0).bind("load loadedmetadata", function () {
self._trigger("first-media-load");
});
};
Carousel.render = function (data, options) {
options = options || {};
// establish the extended default options for the carousel
var carouselOptions = Akamai.Util.options(Carousel.defaultOptions, options);
var templateAttrs = ["" + Carousel._dataAttr, Carousel._dataAttr + "-item-count=\"" + (data || []).length + "\"", "data-snapper-deeplinking=\"false\"", carouselOptions.arrows ? "data-snapper-nextprev" : ""];
Carousel.counter++;
// TODO can we do something about the `enlarge_pane` class here, seems it
// could be conditioned on at least one regular image type in the json
// TODO options should dictate `data-snapper-nextprev`
return Akamai.Util.trim("\n\t\t\t<div " + templateAttrs.join(" ") + " class=\"snapper\">\n\t\t\t\t<div class=\"snapper_nextprev_contain\">\n\t\t\t\t\t<div class=\"snapper_pane_crop\">\n\t\t\t\t\t\t<div class=\"snapper_pane enlarge_pane\">\n\t\t\t\t\t\t\t<div class=\"snapper_items\">\n\t\t\t\t\t\t\t\t" + Akamai.Util.map(data, function (item, i) {
return Carousel._renderItem(item, i, carouselOptions);
}).join("\n") + "\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t" + Carousel._renderThumbnails(data, carouselOptions) + "\n\t\t\t</div>\n\t\t");
};
/**
* Instantiate Carousels by looking for children matching
* [data-akamai-carousel] in `element` param
* @method
* @static
* @param {HTMLElement} element - the element to search in for
* @returns {Akamai.Carousel[]}
*/
/**
* @method
* @returns {Akamai.Spin360[]}
*/
Carousel.prototype.getSpin360s = function () {
return this._spins;
};
/**
* @method
* @returns {Akamai.Magnifier[]}
*/
Carousel.prototype.getMagnifiers = function () {
return this._magnifiers;
};
/**
* @method
* @returns {Akamai.Video[]}
*/
Carousel.prototype.getVideos = function () {
return this._videos;
};
/**
* Go to a particular slide.
*
* Note that the state of the DOM on the page and thus the index of the
* carousel will not be up-to-date until the {@link
* Akamai.Carousel#akamai-carousel-goto} event has been fired. That is,
* calling this method and the calling `getIndex` will not necessarily result
* in an updated index value. Either bind to the event or supply a callback.
*
* @method
* @fires Akamai.Carousel#akamai-carousel-goto
* @param {Integer} index - The zero-based slide index to go to
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Carousel.prototype.goto = function (index, callback) {
this._$el.snapper("goto", index, callback);
};
Carousel._itemAttr = Carousel._dataAttr + "-item";
Carousel._activeItemAttr = Carousel._itemAttr + "-active";
/**
* Add an attribute to all carousel items,
* and additionally maintain an active attribute on the active carousel item
*
* @method
* @returns {undefined}
*/
Carousel.prototype._activeItemAttributes = function () {
var attrAllItems = this.constructor._itemAttr;
var attrActiveItem = this.constructor._activeItemAttr;
this._$el.find(".snapper_item").attr(attrAllItems, true).removeAttr(attrActiveItem).eq(this.getIndex()).attr(attrActiveItem, true);
};
/**
* Return the current slide index
* @method
* @returns {undefined}
*/
Carousel.prototype.getIndex = function () {
return this._$el.snapper("getIndex");
};
// Extend Carousel with Advanceabl interface
Akamai.Advanceable.extend(Carousel);
/**
* Advance to the next item
* @method
* @fires Akamai.Carousel#akamai-carousel-next
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Carousel.prototype.next;
// required for JSDocs, implementation in Advanceable
/**
* Retreat to the previous item
* @method
* @fires Akamai.Carousel#akamai-carousel-previous
* @param {Function?} callback - callback invoked after the action has completed in the DOM
* @returns {undefined}
*/
Carousel.prototype.previous;
// required for JSDocs, implementation in Advanceable
/**
* Start automatic advancement of the carousel items
* @method
* @fires Akamai.Carousel#akamai-carousel-start-slideshow
* @returns {undefined}
*/
Carousel.prototype.startSlideshow = function () {
this._$el.one("mousedown touchstart", this.stopSlideshow.bind(this));
this.next(function () {
this._trigger("start-slideshow");
this._slideshowTimer = setTimeout(function () {
this.startSlideshow();
// TODO remove || when default options are added
}.bind(this), this._options.slideshow.interval);
}.bind(this));
};
/**
* Start automatic advancement of the carousel items
* @method
* @fires Akamai.Carousel#akamai-carousel-stop-slideshow
* @returns {undefined}
*/
Carousel.prototype.stopSlideshow = function () {
clearTimeout(this._slideshowTimer);
this._slideshowTimer = undefined;
this._trigger("stop-slideshow");
};
/**
* Triggered when the carousel has completed it transition to
* a new index due to user interaction or a call to {@link Akamai.Carousel#goto}.
*
* @event Akamai.Carousel#akamai-carousel-goto
*/
/**
* Triggered when the carousel has completed it transition to
* a new index due to user interaction or a call to {@link Akamai.Carousel#next}.
*
* @event Akamai.Carousel#akamai-carousel-next
*/
/**
* Triggered when the carousel has completed it transition to
* a new index due to user interaction or a call to {@link Akamai.Carousel#previous}.
*
* @event Akamai.Carousel#akamai-carousel-previous
*/
/**
* Triggered when the carousel has started the slide show due to a call to
* {@link Akamai.Carousel#startSlideshow}.
*
* @event Akamai.Carousel#akamai-carousel-start-slideshow
*/
/**
* Triggered when the carousel has stoped the slide show due to a call to
* {@link Akamai.Carousel#stopSlideshow}.
*
* @event Akamai.Carousel#akamai-carousel-stop-slideshow
*/
/**
* Triggered when initialization finishes
* {@link Akamai.Carousel}.
*
* @event Akamai.Carousel#akamai-carousel-init
*/
Carousel.prototype.refresh = function () {
this._$el.snapper("updateWidths");
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.Carousel = Carousel;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
/**
* Image Fullscreen
* @class
* @alias Akamai.Fullscreen
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*/
// TODO JSDocs
// TODO Tests
var Fullscreen = Akamai.Util.component("Fullscreen", function (element, options) {
if (this._options.enabled) {
this._init();
}
});
Fullscreen.prototype._init = function () {
this._$fullscreen = this._$el;
// parent container does not allow widths to be set (Firefox, fullscreen), so well adjust the children
this.addButton();
this._fullscreenApiKeys = Fullscreen._keys();
if (this._options.native) {
this._useNativeApi = !!this._fullscreenApiKeys;
} else {
this._useNativeApi = false;
}
// Fullscreen API is disabled and the fallback behavior is active
this._fullscreenFallbackEnabled = false;
this._$placeholder = $("<div>");
this._isFullscreen = false;
this.addEvents();
};
Fullscreen.classes = {
btn: "akamai-fullscreen-btn",
btnContainer: "akamai-fullscreen-btncontainer",
enterBtn: "akamai-fullscreen-btn-enter",
exitBtn: "akamai-fullscreen-btn-exit",
active: "akamai-fullscreen-active",
fallback: "akamai-fullscreen-fallback"
};
Fullscreen.attr = {
width: "data-akamai-fullscreen-width",
enterBtn: "data-akamai-fullscreen-btn",
exitBtn: "data-akamai-fullscreen-exit-btn"
};
Fullscreen.defaultOptions = {
enabled: false,
native: false
};
Fullscreen.prototype.addButton = function () {
this._$widthAdjust = this._$fullscreen.children().filter(".focused");
if (!this._$widthAdjust.length) {
this._$widthAdjust = this._$fullscreen.children().eq(0);
}
if (this._$widthAdjust.attr(Fullscreen.attr.width) === null) {
this._$widthAdjust.attr(Fullscreen.attr.width, "");
}
if (this._$widthAdjust.find("[" + Fullscreen.attr.enterBtn + "]").length) {
return;
}
// buttons parent
this._$buttonContainer = this._$widthAdjust;
this._$buttonContainer.addClass(Fullscreen.classes.btnContainer);
this._$buttonContainer.append(this.render());
};
Fullscreen.prototype.addEvents = function () {
this._$fullscreen.on("click", function (e) {
var $target = $(e.target);
if (!$target.is("[" + Fullscreen.attr.enterBtn + "]")) {
return;
}
this.enter();
e.preventDefault();
}.bind(this));
this._$fullscreen.on("click", function (e) {
var $target = $(e.target);
if (!$target.is("[" + Fullscreen.attr.exitBtn + "]")) {
return;
}
this.exit();
e.preventDefault();
}.bind(this));
if (this._useNativeApi) {
document.addEventListener(this._fullscreenApiKeys.onchange, function () {
// exiting fullscreen using native method (ESC or menu option)
if (!document[this._fullscreenApiKeys.element]) {
this._exit();
}
}.bind(this), false);
}
// ESC to close
$(document).on("keydown", function (e) {
var code = e.keyCode || e.which;
if (code === 27) {
this._exit();
}
}.bind(this));
};
Fullscreen.prototype.render = function () {
return Akamai.Util.trim("\n\t\t\t<button " + Fullscreen.attr.enterBtn + " class=\"" + Fullscreen.classes.btn + " " + Fullscreen.classes.enterBtn + " icon-fullscreen\">Full Screen</button>\n\t\t\t<button " + Fullscreen.attr.exitBtn + " class=\"" + Fullscreen.classes.btn + " " + Fullscreen.classes.exitBtn + " icon-close-light\">Exit Full Screen</button>\n\t\t");
};
Fullscreen._keyLookup = [{
enter: "requestFullscreen",
exit: "exitFullscreen",
element: "fullscreenElement",
onchange: "fullscreenchange"
}, {
enter: "webkitRequestFullscreen",
exit: "webkitExitFullscreen",
element: "webkitFullscreenElement",
onchange: "webkitfullscreenchange"
}, {
enter: "webkitRequestFullScreen",
exit: "webkitCancelFullScreen",
element: "webkitCurrentFullScreenElement",
onchange: "webkitfullscreenchange"
}, {
enter: "mozRequestFullScreen",
exit: "mozCancelFullScreen",
element: "mozFullScreenElement",
onchange: "mozfullscreenchange"
}, {
enter: "msRequestFullscreen",
exit: "msExitFullscreen",
element: "msFullscreenElement",
onchange: "MSFullscreenChange"
}];
Fullscreen._keys = function (el) {
var keys = Fullscreen._keyLookup;
var el = document.body;
for (var j = 0, k = keys.length; j < k; j++) {
if (keys[j].enter in el) {
return keys[j];
}
}
};
Fullscreen.prototype._maximizePlacement = function () {
this._fullscreenFallbackEnabled = true;
this._$fullscreen.addClass(Fullscreen.classes.fallback);
this._$placeholder.insertAfter(this._$fullscreen);
this._$fullscreen.appendTo(document.body);
};
Fullscreen.prototype._restorePlacement = function () {
if (!this._fullscreenFallbackEnabled) {
return;
}
this._fullscreenFallbackEnabled = false;
this._$fullscreen.removeClass(Fullscreen.classes.fallback);
this._$fullscreen.insertAfter(this._$placeholder);
this._$placeholder.remove();
};
Fullscreen.prototype._adjustWidth = function () {
this._$fullscreen.css("width", "auto !important");
// wish this could go into the change event above, but alas the dimensions dont measure right
var widthSmall = this._$fullscreen.width();
var heightSmall = this._$fullscreen.height();
var viewportWidth = this._useNativeApi ? window.screen.width : Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var viewportHeight = this._useNativeApi ? window.screen.height : Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
// calculate the maximum width we can use to fill the viewportHeight
var widthBig = widthSmall * viewportHeight / heightSmall;
// if the width is bigger than the maximum, just set to 100% (wont fill the entire height, but thats ok)
if (widthBig > viewportWidth) {
widthBig = "100%";
}
this._$widthAdjust.width(widthBig);
};
Fullscreen.prototype._revertWidth = function () {
this._$widthAdjust.css("width", "");
};
/**
* Enter full screen mode
* @method
* @returns {undefined}
*/
Fullscreen.prototype.enter = function () {
var activeIndex = this._$fullscreen[0].shoestringData.Viewer._carousels[0].getIndex();
this._$fullscreen.addClass(Fullscreen.classes.active);
if (this._useNativeApi) {
this._$fullscreen[0][this._fullscreenApiKeys.enter]();
this._adjustWidth();
} else {
this._adjustWidth();
this._maximizePlacement();
}
this._isFullscreen = true;
this._$fullscreen[0].shoestringData.Viewer._carousels[0].goto(activeIndex);
this._trigger("enter");
};
Fullscreen.prototype._exit = function () {
var activeIndex = this._$fullscreen[0].shoestringData.Viewer._carousels[0].getIndex();
this._$fullscreen.removeClass(Fullscreen.classes.active);
if (!this._useNativeApi) {
this._restorePlacement();
}
this._revertWidth();
this._isFullscreen = false;
this._$fullscreen[0].shoestringData.Viewer._carousels[0].goto(activeIndex);
this._trigger("exit");
};
/**
* Exits full screen mode
* @method
* @returns {undefined}
*/
Fullscreen.prototype.exit = function () {
if (this._useNativeApi) {
// note some close behavior happens in the onchange event handler above
document[this._fullscreenApiKeys.exit]();
}
this._exit();
};
Fullscreen.prototype.isFullscreen = function () {
return this._isFullscreen;
};
exports.Akamai = exports.Akamai || {};
exports.Akamai.Fullscreen = Fullscreen;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
/**
* Akamai Viewer omni-component
* @class
* @alias Akamai.Viewer
* @param {HTMLElement} element - the DOM element representing the component markup
* @param {Object} options - configuration options
*
* @example <caption>Instatiation</caption>
* var element = document.querySelector( "[data-akamai-viewer]" );
* var viewer = Akamai.Viewer( element );
*/
var Viewer = Akamai.Util.component("Viewer", true, function (element, options) {
// try to update the carousel and magnifier sizes options to the width of
// the viewer for more accurate srcset selection
this._updateSizesOptions(true);
this._onResizeComplete(this._updateSizesOptions.bind(this));
// move all the flattened component options (spin, zoom, video) onto the carousel
this._options.carousel = Viewer._extendCarouselOptions(this._options);
this._withData(function (data) {
if (data) {
// check all of the urls agains the hostname and whitelist
this._checkJSONUrls(data);
// prevent XSS / injection attacks by escaping string values
data = Akamai.Util.escapeJSONVals(data);
// store escaped data for later reference
this._options.items.data = data;
}
this._tagMapping = {};
// TODO if the element is empty and/or options has `items` render into element
if (this._$el.children().length == 0 && data) {
this._tagMapping = this.constructor._tagSplit(data, this._options.items);
this._$el.append(this.constructor.render(data, this._options, true));
}
// intantiate all child carousels
this._carousels = Akamai.Carousel.createMany(element, this._options.carousel);
this._fullscreen = new Akamai.Fullscreen(element, this._options.fullscreen);
// on enter and exit of fullscreen resolve the breakpoints and update options
this._$el.bind("akamai-fullscreen-enter akamai-fullscreen-exit", function () {
this._updateOptions(this._resolveBreakpointOptions());
}.bind(this));
// use the breakpoints to set match media listeners
this._setBreakpoints();
// have to trigger init explicitly for async constructors
this._trigger("init");
}.bind(this), function (msg) {
throw new Error(msg);
});
});
Viewer.prototype._updateSizesOptions = function (localUpdateOnly) {
// update sizes option to something more specific if possible
var elWidth = this._$el.width();
if (elWidth && elWidth > 0) {
elWidth += "px";
this._options = Akamai.Util.extend(true, this._options, {
carousel: {
images: {
sizes: elWidth
}
}
});
}
if (!localUpdateOnly) {
this._updateOptions(this._options);
}
};
Viewer.prototype._withData = function (after, fail) {
if (!this._options.items.uri) {
if (this._exceedsLimit(this._options.items.data, fail)) {
return;
}
after(this._options.items.data);
} else if (typeof this._options.items.uri !== 'undefined' && this._options.items.uri.length > 0) {
$.get(this._options.items.uri, function (data) {
// make sure the data gets sorted out regardless of `get` impl
data = typeof data === "string" ? JSON.parse(data) : data;
if (this._exceedsLimit(data.items, fail)) {
return;
}
after(data.items);
}.bind(this));
}
};
// value in kibibytes
Viewer.prototype._exceedsLimit = function (data, fail) {
// TODO some tests don't provide data
if (!data) {
return false;
}
// 1 unicode character = 4 bytes
// length = # of chars
// kibibyte = 1024 bytes
// # of chars * 4 / 1024
var kb = JSON.stringify(data).length * 4 / 1024;
if (kb > this._options.items.limit) {
fail = fail || function () {};
fail("JSON data size exceeds " + this._options.items.limit + " KiB. Limit can be configured with items.limit Akamai.Viewer options.");
return true;
}
return false;
};
Viewer.prototype._setBreakpoints = function () {
this._breakpoints = this._reduceBreakpoints(this._options.breakpoints || {});
this._fullscreenBreakpoints = this._reduceBreakpoints(this._options.fullscreenBreakpoints || {});
this._bindBreakpoints();
};
Viewer.prototype._reduceBreakpoints = function (breakpoints) {
var bps = [];
for (var bp in breakpoints) {
if (breakpoints.hasOwnProperty(bp)) {
try {
bps.push(parseInt(bp, 10));
} catch (e) {
Akamai.Util.log(e, 'error');
}
}
}
bps.sort(function (a, b) {
return b < a;
});
return bps;
};
Viewer.prototype._bindBreakpoints = function () {
[this._breakpoints, this._fullscreenBreakpoints].forEach(function (bps) {
// bind using the minwidth and the next breakpoint as the maxwidth
// adds a range from 0 to the first breakpoint and from the last
// breakpoint to a very large number
if (!bps.length) {
return;
}
for (var i = -1; i < bps.length; i++) {
this._bindMatchMedia(bps[i], bps[i + 1]);
}
}.bind(this));
};
// bind a callback to run after resize completes
Viewer.prototype._onResizeComplete = function (callback) {
var cbtimer;
var self = this;
window.addEventListener("resize", function () {
clearTimeout(cbtimer);
cbtimer = setTimeout(callback, 500);
});
};
Viewer.prototype._bindMatchMedia = function (minWidth, maxWidth) {
minWidth = minWidth || 0;
// large number so we can use one media query template
maxWidth = maxWidth || 1000000000;
if (window.matchMedia) {
// bind using the minwidth and maxwidth so we get the events at the boundaries
// so we can asses which set of options applies.
var query = "(min-width: " + minWidth + "px) and (max-width: " + maxWidth + "px)";
// create a media list to bind to
var initList = window.matchMedia(query);
if (initList && initList.addListener) {
// handle a match on instantiation
this._mediaMatch(initList, minWidth, true);
// bind for later changes in whether the media query matches
initList.addListener(function (list) {
this._mediaMatch(list, minWidth);
}.bind(this));
}
}
};
Viewer.prototype._mediaMatch = function (list, minWidth, ignoreDefault) {
// If there is a match for the breakpoint (we're in the bp range)
// Then set the options based on that breakpoint
// Else if there is not a match and the client width is below the
// breakpoint that's being disabled, then use the original options
if (list.matches) {
var resolvedOptions = this._resolveBreakpointOptions(minWidth);
this._updateOptions(resolvedOptions);
}
};
// TODO a ton of duplication with bindMatchmedia
Viewer.prototype._getCurrentMinWidth = function (bps) {
// bind using the minwidth and the next breakpoint as the maxwidth
// adds a range from 0 to the first breakpoint and from the last
// breakpoint to a very large number
for (var i = -1; i < bps.length; i++) {
minWidth = bps[i] || 0;
maxWidth = bps[i + 1] || 100000000;
if (window.matchMedia) {
// bind using the minwidth and maxwidth so we get the events at the boundaries
// so we can asses which set of options applies.
var query = "(min-width: " + minWidth + "px) and (max-width: " + maxWidth + "px)";
// create a media list to bind to
var initList = window.matchMedia(query);
if (initList && initList.matches) {
return minWidth;
}
}
}
return false;
};
Viewer.prototype._resolveBreakpointOptions = function (minWidth) {
// TODO bind on the reduce callback was not working
var self = this;
var bps, bpConfig;
if (this._fullscreen.isFullscreen()) {
bps = this._fullscreenBreakpoints;
bpConfig = self._options.fullscreenBreakpoints;
} else {
bps = this._breakpoints;
bpConfig = self._options.breakpoints;
}
if (!minWidth) {
minWidth = self._getCurrentMinWidth(bps);
}
// There may be no matching breakpoints when this method is called to
// resolve the current options. If that's the case then we need to default
// to the top level options
if (!bps.length) {
return self._originalOptions;
}
return bps.reduce(function (acc, bp) {
var cloned = Akamai.Util.extend(true, {}, acc);
var ptions;
if (bp <= minWidth) {
cloned = Akamai.Util.extend(true, cloned, bpConfig[bp]);
}
return cloned;
}, Akamai.Util.extend(true, {}, this._originalOptions));
};
Viewer._extendCarouselOptions = function (options) {
// clone the carousel options for the given breakpoint
var clonedCarousel = Akamai.Util.extend(true, {}, options.carousel || {});
// TODO shared code with constructor
// extend the cloned options with the breakpoint options so that all the
// child component config is attached to the carousel config (also happens
// in the constructor )
return Akamai.Util.extend(true, clonedCarousel, {
magnifier: options.magnifier,
spin360: options.spin360,
video: options.video,
fullscreen: options.fullscreen
});
};
Viewer.prototype._updateOptions = function (options) {
this._options = Akamai.Util.extend(true, this._options, options);
// extend the carousel options for the given breakpoint
var carouselOptions = Viewer._extendCarouselOptions(options);
// push the final options down to the dom element so that CSS that keys off
// of the attributes can apply when JS config is used
Akamai.Util.setDataAttrOptions(this._$el, Viewer.defaultOptions, options, "Viewer");
// pass the new options down to all child carousels
this._carousels.forEach(function (comp) {
comp._updateOptions(carouselOptions);
}.bind(this));
};
Viewer.prototype._checkJSONUrls = function (obj) {
var hostnames = this._options.items.hostnames;
return Akamai.Util.mapJSONVals(obj, function (val, key) {
if (key === "url" && !this._urlHostnameMatch(val)) {
throw new Error("The URL `" + val + "` does not match this page's hostname or the whitelist defined in Akamai.Viewer option `hostnames` which is:\n\n" + (hostnames.length ? hostnames.join("\n") : "No hostnames") + "\n");
}
return val;
}.bind(this));
};
Viewer.prototype._urlHostnameMatch = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser.hostname === "" || parser.hostname === window.location.hostname || this._options.items.hostnames.indexOf(parser.hostname) >= 0;
};
/**
* Defines the global default options for all Viewers on the page
* @static
* @property {Object} breakpoints - configuration changes for child components at breakpoints (no default)
* @property {Object} fullscreenBreakpoints - configuration changes for child components at breakpoints when in fullscreen mode (no default)
* @property {Object[]} items.data - array of items from the Akamai JSON (default: undefined)
* @property {String} items.defaultTag - (default: "akamai-untagged")
* @property {String[]} items.hostnames - whitelist of URL hostnames to check for in JSON, (default: empty array)
* @property {Integer} items.limit - size limit of JSON data in kibibytes (default: 100)
* @property {String} items.renderAll - (default: false)
* @property {String[]} items.tags - Set of tags to match against the Akamai JSON data (default: undefined)
* @property {String} items.uri - URI at which to retrieve the Akamai JSON (default: undefined)
* @property {Object} carousel - child {@link Akamai.Carousel} options
* @property {Object} magnifier - child {@link Akamai.Magnifier} options
* @property {Object} spin360 - child {@link Akamai.Spin360} options
* @property {Object} video - child {@link Akamai.Video} options
* @property {Object} fullscreen - child {@link Akamai.Fullscreen} options
*/
Viewer.defaultOptions = {
breakpoints: {},
items: {
data: undefined,
defaultTag: "akamai-untagged",
hostnames: [],
limit: 100,
renderAll: false,
tags: undefined,
uri: undefined
},
carousel: Akamai.Carousel.defaultOptions,
magnifier: Akamai.Magnifier.defaultOptions,
spin360: Akamai.Spin360.defaultOptions,
video: Akamai.Video.defaultOptions,
fullscreen: Akamai.Fullscreen.defaultOptions
};
Viewer._tagSplit = function (data, options) {
var items = options;
// set all items without a tag to the default
data = data.map(function (item) {
item.tags = item.tags && item.tags.length ? item.tags : [items.defaultTag];
return item;
});
// if the tags option was set, filter items out that don't match
if (items.tags) {
data = items.data.filter(function (item) {
return item.tags.reduce(function (acc, tag) {
return acc || items.tags.indexOf(tag) >= 0;
}, false);
});
}
// from the filtered get the first (may be default tag)
var def = data[0].tags[0];
// create a mapping from tags to items in the set
var tagMapping = data.reduce(function (acc, item) {
(item.tags || []).forEach(function (tag) {
acc[tag] = acc[tag] || [];
acc[tag].push(item);
});
return acc;
}, {});
// set the first (default) object to the first tag
tagMapping[Viewer._firstRenderTag] = tagMapping[def];
return tagMapping;
};
Viewer._firstRenderTag = "akamai-first-render";
Viewer._tagAttr = Viewer._dataAttr + "-tag";
Viewer._renderTag = function (data, options) {
// get the first item and it's tag
// NOTE this assumes that the data has been normalized to have a default tag
var tag = data[0].tags[0];
// TODO shoestring doesn't treat html correctly unless the leading `<` has
// no whitepsace in front of it:
// shoestring/issues/94
return Akamai.Util.trim("\n\t\t\t<div " + Viewer._tagAttr + "=\"" + tag + "\">\n\t\t\t\t" + Akamai.Carousel.render(data, options) + "\n\t\t\t</div>\n\t\t");
};
/**
* Takes standard options including an `items` attribute and renders HTML
* that conforms to the component expectations
* @method
* @static
* @params {Object[]} json - Akamai JSON data
* @params {Object} options - options object with an `items` attribute, see default options
* @params {Boolean} options.items - configuration for how the items should be
* rendered, see {@link Akamai.Viewer.defaultOptions} default options items sub-configuration
* @returns { String }
*/
Viewer.render = function (json, options, unwrapped) {
// NOTE we do not extend the default options here because it has no bearing
// on the rendering of the Viewer markup or child markup. This is contrast
// to the carousel which does extend the passed options with defaults so the
// markup can make use of them
var data = Viewer._tagSplit(json, options.items);
var carouselOptions = Viewer._extendCarouselOptions(options);
var childMarkup;
if (options.items.renderAll) {
childMarkup = data.map(function (acc, datum) {
return Viewer._renderTag(datum, carouselOptions);
}).join(" ");
} else {
childMarkup = Viewer._renderTag(data[Viewer._firstRenderTag], carouselOptions);
}
var wrapped = "\n\t\t\t<div " + Viewer._dataAttr + ">\n\t\t\t\t" + childMarkup + "\n\t\t\t</div>\n\t\t";
return unwrapped ? childMarkup : wrapped;
};
/**
* Accessor for child carousel components.
* @method
* @returns { Akamai.Carousel[] }
*/
Viewer.prototype.getCarousels = function () {
return this._carousels;
};
/**
* Set the visible carousel based on the tag. Note the method activity does
* not complete until all the images from the relevant viewer have loaded, use
* the `akamai-viewer-switch-tag` event
* @method
* @fires Akamai.Viewer#akamai-viewer-switch-tag
* @params {String} tag - The tag corresponding to the desired carousel
* @returns {undefined}
*/
Viewer.prototype.switchTag = function (tag) {
var tags = this.getTags();
// if the passed tag isn't in the data set return early and log an error in
// the console
if (tags.indexOf(tag) == -1) {
Akamai.Util.log("tag: " + tag + " is not present in the data for this viewer", 'error');
return;
}
var selector = "[" + this.constructor._tagAttr + "=\"" + tag + "\"]";
// try to find an existing element with the tag
var $taggedViewer = this._$el.find(selector);
// if there's no element append the newly rendered tag markup
if (!$taggedViewer.length) {
// make sure the config takes into account the current breakpoint
var options = this._resolveBreakpointOptions();
var carouselOptions = Viewer._extendCarouselOptions(options);
// create the markup that will be inserted
var $markup = $(this.constructor._renderTag(this._tagMapping[tag], carouselOptions, true));
var $imgs = $markup.find("img");
var imgCount = $imgs.length;
var loaded = 0;
// hide the new carousel initially
$markup.css("display", "none");
$imgs.bind("load", function () {
if (++loaded !== imgCount) {
return;
}
// TODO namespace?
$imgs.unbind("load");
var carousels = Akamai.Carousel.createMany($markup[0], carouselOptions);
carousels.forEach(function (c) {
// TODO figure out why the carousel constructor doesn't apply the
// active index class we want on instantiation, likely due to the fact
// that the carousel is hidden so the "getIndex" calc is broken
// mark the carousel item as active
c.goto(c.getIndex());
});
// create and store the new carousels (should be one)
this._carousels = this._carousels.concat(carousels);
this._showViewer($markup, carousels);
}.bind(this));
// append the new markup to the existing viewer
this._$el.append($markup);
} else {
this._showViewer($taggedViewer);
}
};
// TODO should be handled in CSS
Viewer.prototype._showViewer = function ($viewer, carousels) {
this._$el.find("[" + this.constructor._tagAttr + "]").css("display", "none").removeClass("focused");
$viewer.css("display", "block").addClass("focused");
if (carousels) {
carousels.map(function (c) {
c.refresh();
});
}
// TODO the placement here seems arbitrary, probably belongs in `_showViewer`
// TODO also sucks to be so tightly coupled
this._fullscreen.addButton();
this._trigger("switch-tag");
};
/**
* Accessor for tags derived from Akamai JSON data
* @method
* @returns { String[] }
*/
Viewer.prototype.getTags = function () {
if (this._tags) {
return this._tags;
};
var tags = [];
// map and store all the carousel tags
for (var tag in this._tagMapping) {
if (this._tagMapping.hasOwnProperty(tag) && tag !== Viewer._firstRenderTag) {
// otherwise grab the list of tags
tags.push(tag);
}
}
return this._tags = tags;
};
/**
* Triggered when the viewer switches tag views. This includes waiting for
* images to load for carousels dedicated to previously unviewed tags.
* {@link Akamai.Viewer#switchTag}.
*
* @event Akamai.Viewer#akamai-viewer-switch-tag
*/
/**
* Triggered when initialization finishes
* {@link Akamai.Viewer}.
*
* @event Akamai.Viewer#akamai-viewer-init
*/
exports.Akamai = exports.Akamai || {};
exports.Akamai.Viewer = Viewer;
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
(function (exports, $) {
$.fn.akamaiViewer = function (options) {
this.each(function (i, element) {
new Akamai.Viewer(element, options);
});
};
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
//# sourceMappingURL=akamai-viewer.unmin.js.map