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

8477 lines
232 KiB
Plaintext
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 `
<img src="${Spin360._fallbackSrc(url, options.images)}"
srcset="${Spin360._srcset(url, options.images)}"
sizes="${options.images.sizes}" />
`;
};
Spin360.render = function(json, options){
var finalOptions = Akamai.Util.options(Spin360.defaultOptions, options);
// TODO fix the string problem in shoestring
return Akamai.Util.trim(`
<div class="tau" data-akamai-spin360 title="${json.alt}">
${Akamai.Util
.map(json.urls, function(url) { return Spin360._renderImg(url, finalOptions); })
.join("\n")
}
</div>
`);
};
/**
* 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 `
<div ${dataAttr}>
<div class="enlarge_contain">
<img src="${fallbackSrc}"
srcset="${srcSet}"
sizes="${options.image.sizes}">
</div>
<a href="${largestSrc}"
class="enlarge_btn"
title="${options.buttonText}">
${options.buttonText}
</a>
</div>
`;
};
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 `
<div ${dataAttr}>
<div>
<img src="${fallbackSrc}"
srcset="${srcSet}"
sizes="${options.image.sizes}">
<div></div>
</div>
<a href="${largestSrc}"
class="scroller-zoom-in"
title="${options.zoomInBtnText}">
${options.zoomInBtnText}
</a>
<a href="${largestSrc}"
class="scroller-zoom-out"
title="${options.zoomOutBtnText}">
${options.zoomOutBtnText}
</a>
</div>
`;
};
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(`
<div ${Akamai.Video._dataAttr}>
<video ${attrs.join(" ")} poster="${poster}" preload="metadata">
<source src="${url}" ${mime} />
</video>
</div>
`);
};
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(`
<div ${Akamai.Video._dataAttr}>
<video ${attrs.join(" ")} poster="${poster}" preload="metadata">
<source src="${url}${joiner}imformat=vp9&imwidth=${width}" type="video/webm" />
<source src="${url}${joiner}imformat=h265&imwidth=${width}" type="video/mp4; codecs=hevc" />
<source src="${url}${joiner}imformat=h264&imwidth=${width}" type="video/mp4" />
</video>
</div>
`);
};
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', `
<svg viewBox="0 0 200 200" alt="Play video">
<circle cx="100" cy="100" r="90" fill="none" stroke-width="15" stroke="#fff"/>
<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>
</svg>
`);
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(`
item type ${item.type} at index ${i} must be 'image', 'spin360', or 'video'
`);
}
return `
<div class="snapper_item" id="${Carousel._uniqueItemId(item, i)}">
${Carousel.renderMapping[item.type].render(item, options[mappedType])}
</div>
`;
};
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 `
<a href="#${Carousel._uniqueItemId(item, i)}" ${attrs.join(" ")}>
<img src="${src}" srcset="${srcset}" sizes="${sizes}" alt="${altText}" title="Scroll to ${item.type} ${i}" />
</a>
`;
};
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 `
<div class="snapper_nav">
<div class="snapper_nav_inner">
${Akamai.Util.map(data, function(item, i){
return Carousel._renderThumbnailAnchor(item, data, i, options);
}).join("\n")}
</div>
</div>
`;
};
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(`
<div ${templateAttrs.join(" ")} class="snapper">
<div class="snapper_nextprev_contain">
<div class="snapper_pane_crop">
<div class="snapper_pane enlarge_pane">
<div class="snapper_items">
${Akamai.Util.map(data, function(item, i){
return Carousel._renderItem(item, i, carouselOptions);
}).join("\n")}
</div>
</div>
</div>
</div>
${Carousel._renderThumbnails(data, carouselOptions)}
</div>
`);
};
/**
* 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(`
<button ${Fullscreen.attr.enterBtn} class="${Fullscreen.classes.btn} ${Fullscreen.classes.enterBtn} icon-fullscreen">Full Screen</button>
<button ${Fullscreen.attr.exitBtn} class="${Fullscreen.classes.btn} ${Fullscreen.classes.exitBtn} icon-close-light">Exit Full Screen</button>
`);
};
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:
${hostnames.length ? hostnames.join("\n") : "No hostnames"}
`);
}
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(`
<div ${Viewer._tagAttr}="${tag}">
${Akamai.Carousel.render(data, options)}
</div>
`);
};
/**
* 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 = `
<div ${Viewer._dataAttr}>
${childMarkup}
</div>
`;
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);