origin33/app/html/js/docs.js

9184 lines
263 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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