8477 lines
232 KiB
Plaintext
8477 lines
232 KiB
Plaintext
|
/*! 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 = {
|
|||
|
'&': '&',
|
|||
|
'<': '<',
|
|||
|
'>': '>',
|
|||
|
'"': '"',
|
|||
|
"'": ''',
|
|||
|
'/': '/',
|
|||
|
'`': '`',
|
|||
|
'=': '='
|
|||
|
};
|
|||
|
|
|||
|
function escapeHTML(string) {
|
|||
|
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
|
|||
|
return entityMap[s];
|
|||
|
});
|
|||
|
}
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
var Advanceable = {
|
|||
|
extension: {
|
|||
|
next: function(callback){
|
|||
|
this.goto(this.getIndex() + 1, function(){
|
|||
|
if( callback ) { callback(); };
|
|||
|
this._trigger("next");
|
|||
|
}.bind(this));
|
|||
|
},
|
|||
|
|
|||
|
previous: function(callback){
|
|||
|
this.goto(this.getIndex() - 1, function(){
|
|||
|
if( callback ) { callback(); };
|
|||
|
this._trigger("previous");
|
|||
|
}.bind(this));
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
extend: function(constr) {
|
|||
|
if(!constr.prototype.goto || !constr.prototype.getIndex) {
|
|||
|
throw new Error("Advanceable mixin requires `goto` and `getIndex` methods");
|
|||
|
}
|
|||
|
|
|||
|
$.extend(constr.prototype, this.extension);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Advanceable = Advanceable;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
|
|||
|
// TODO this should be handled in an image component
|
|||
|
(function(exports, $){
|
|||
|
var Sourceable = {
|
|||
|
extension: {
|
|||
|
_srcArray: function(url, options, includeW ){
|
|||
|
var policy = options.policy || options.thumbnail && options.thumbnail.policy;
|
|||
|
var widthParam = options.widthParam;
|
|||
|
|
|||
|
return options.widths.map(function(w){
|
|||
|
var src = url + (url.indexOf("?") === -1 ? "?" : "&") + widthParam + "=" + w;
|
|||
|
|
|||
|
if( policy ){
|
|||
|
src += "&impolicy=" + policy;
|
|||
|
}
|
|||
|
|
|||
|
//note src width for srcset (eg ' 500w')
|
|||
|
if( includeW ){
|
|||
|
src += " " + w + "w";
|
|||
|
}
|
|||
|
|
|||
|
return src;
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
_srcset: function(url, options){
|
|||
|
return this._srcArray( url, options, true ).join(", ");
|
|||
|
},
|
|||
|
|
|||
|
_fallbackSrc: function(url, options){
|
|||
|
return this._srcArray( url, options )[ 0 ];
|
|||
|
},
|
|||
|
|
|||
|
_largestSrc: function(url, options){
|
|||
|
return this._srcArray( url, options ).pop();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
extendStatic: function(constr) {
|
|||
|
Akamai.Util.extend(false, constr, this.extension);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Sourceable = Sourceable;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
|
|||
|
(function( w, undefined ){
|
|||
|
|
|||
|
// requestAnimationFrame pfill
|
|||
|
var raf = (function(){
|
|||
|
return window.requestAnimationFrame ||
|
|||
|
window.webkitRequestAnimationFrame ||
|
|||
|
window.mozRequestAnimationFrame ||
|
|||
|
function( callback ){
|
|||
|
w.setTimeout(callback, 1000 / 60);
|
|||
|
};
|
|||
|
})();
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
/* toss scrolls and element with easing
|
|||
|
|
|||
|
// elem is the element to scroll
|
|||
|
// options hash:
|
|||
|
* left is the desired horizontal scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
|
|||
|
* top is the desired vertical scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
|
|||
|
* duration is the number of milliseconds the throw will take. Default is 100.
|
|||
|
* easing is an optional custom easing function. Default is w.toss.easing. Must follow the easing function signature
|
|||
|
|
|||
|
*/
|
|||
|
w.toss = function( elem, options ){
|
|||
|
toss.tossing( elem, false );
|
|||
|
var i = 0,
|
|||
|
sLeft = elem.scrollLeft,
|
|||
|
sTop = elem.scrollTop,
|
|||
|
// Toss defaults
|
|||
|
op = {
|
|||
|
top: "+0",
|
|||
|
left: "+0",
|
|||
|
duration: 200,
|
|||
|
easing: toss.easing,
|
|||
|
finished: function() {}
|
|||
|
},
|
|||
|
endLeft, endTop;
|
|||
|
|
|||
|
// Mixin based on predefined defaults
|
|||
|
if( options ){
|
|||
|
for( var j in op ){
|
|||
|
if( options[ j ] !== undefined ){
|
|||
|
op[ j ] = options[ j ];
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Convert relative values to ints
|
|||
|
// First the left val
|
|||
|
if( typeof op.left === "string" ){
|
|||
|
op.left = parseFloat( op.left );
|
|||
|
endLeft = op.left + sLeft;
|
|||
|
}
|
|||
|
else {
|
|||
|
endLeft = op.left;
|
|||
|
op.left = op.left - sLeft;
|
|||
|
}
|
|||
|
// Then the top val
|
|||
|
if( typeof op.top === "string" ){
|
|||
|
|
|||
|
op.top = parseFloat( op.top );
|
|||
|
endTop = op.top + sTop;
|
|||
|
}
|
|||
|
else {
|
|||
|
endTop = op.top;
|
|||
|
op.top = op.top - sTop;
|
|||
|
}
|
|||
|
|
|||
|
toss.tossing( elem, true );
|
|||
|
var startTime = new Date().getTime();
|
|||
|
var endTime = startTime + op.duration;
|
|||
|
var run = function(){
|
|||
|
var curTime = new Date().getTime();
|
|||
|
// if tossing is suddenly not true, return the callback
|
|||
|
if( !toss.tossing( elem ) ){
|
|||
|
if( op.finished ){
|
|||
|
op.finished();
|
|||
|
}
|
|||
|
}
|
|||
|
// if the time is still less than the end of duration, keep scrolling
|
|||
|
else if( curTime < endTime ){
|
|||
|
i = ( ( curTime - startTime ) / op.duration ) * op.duration;
|
|||
|
elem.scrollLeft = op.easing( i, sLeft, op.left, op.duration );
|
|||
|
elem.scrollTop = op.easing( i, sTop, op.top, op.duration );
|
|||
|
return raf( run );
|
|||
|
}
|
|||
|
// if time is up,
|
|||
|
else{
|
|||
|
elem.scrollLeft = endLeft;
|
|||
|
elem.scrollTop = endTop;
|
|||
|
if( op.finished ){
|
|||
|
op.finished();
|
|||
|
}
|
|||
|
toss.tossing( elem, false );
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
raf( run );
|
|||
|
|
|||
|
// Return the values, post-mixin, with end values specified
|
|||
|
return { top: endTop, left: endLeft, duration: op.duration, easing: op.easing };
|
|||
|
};
|
|||
|
|
|||
|
// tossing object keeps track of currently tossing elements. true during a programatic scroll
|
|||
|
var tossingElems = {};
|
|||
|
toss.tossing = function( elem, state ){
|
|||
|
if( state !== undefined ){
|
|||
|
tossingElems[ elem ] = state;
|
|||
|
}
|
|||
|
return tossingElems[ elem ];
|
|||
|
};
|
|||
|
|
|||
|
// Easing can use any of Robert Penner's equations (http://www.robertpenner.com/easing_terms_of_use.html). By default, toss includes ease-out-cubic
|
|||
|
// arguments: t = current iteration, b = initial value, c = end value, d = total iterations
|
|||
|
// use w.toss.easing to provide a custom function externally, or pass an easing function as a callback to the toss method
|
|||
|
toss.easing = function (t, b, c, d) {
|
|||
|
return c*((t=t/d-1)*t*t + 1) + b;
|
|||
|
};
|
|||
|
|
|||
|
//retain old api
|
|||
|
toss.toss = toss;
|
|||
|
|
|||
|
})( this );
|
|||
|
|
|||
|
;(function( w, $ ){
|
|||
|
var pluginName = "snapper";
|
|||
|
$.fn[ pluginName ] = function(optionsOrMethod){
|
|||
|
var pluginArgs = arguments;
|
|||
|
var scrollListening = true;
|
|||
|
|
|||
|
// css snap points feature test.
|
|||
|
// even if this test passes, several behaviors will still be polyfilled, such as snapping after resize, and animated advancing of slides with anchor links or next/prev links
|
|||
|
var testProp = "scroll-snap-type";
|
|||
|
// test latest spec first. then fallback older
|
|||
|
var snapSupported = w.CSS && w.CSS.supports && (w.CSS.supports(testProp, "x mandatory") || w.CSS.supports(testProp, "mandatory") || w.CSS.supports("-webkit-" + testProp, "mandatory") || w.CSS.supports("-ms-" + testProp, "mandatory") );
|
|||
|
|
|||
|
// get the snapper_item elements whose left offsets fall within the scroll pane. Returns a wrapped array.
|
|||
|
function itemsAtOffset( elem, offset ){
|
|||
|
var $childNodes = $( elem ).find( "." + pluginName + "_item" );
|
|||
|
var containWidth = $( elem ).width();
|
|||
|
var activeItems = [];
|
|||
|
|
|||
|
$childNodes.each(function( i ){
|
|||
|
if( this.offsetLeft >= offset - 5 && this.offsetLeft < offset + containWidth - 5 ){
|
|||
|
activeItems.push( this );
|
|||
|
}
|
|||
|
});
|
|||
|
return $( activeItems );
|
|||
|
}
|
|||
|
|
|||
|
function outerWidth( $elem ){
|
|||
|
return $elem.width() + parseFloat( $elem.css( "margin-left" ) ) + parseFloat( $elem.css( "margin-right" ) );
|
|||
|
}
|
|||
|
|
|||
|
function outerHeight( $elem ){
|
|||
|
return $elem.height() + parseFloat( $elem.css( "margin-bottom" ) ) + parseFloat( $elem.css( "margin-top" ) );
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
// snapEvent dispatches the "snapper.snap" event.
|
|||
|
// The snapper_item elements with left offsets that are inside the scroll viewport are listed in an array in the second callback argument's activeSlides property.
|
|||
|
// use like this: $( ".snapper" ).bind( "snapper.snap", function( event, data ){ console.log( data.activeSlides ); } );
|
|||
|
function snapEvent( elem, x, prefix ){
|
|||
|
prefix = prefix ? prefix + "-" : "";
|
|||
|
var activeSlides = itemsAtOffset( elem, x );
|
|||
|
$( elem ).trigger( pluginName + "." + prefix + "snap", { activeSlides: activeSlides } );
|
|||
|
}
|
|||
|
|
|||
|
// optional: include toss() in your page to get a smooth scroll, otherwise it'll just jump to the slide
|
|||
|
function goto( elem, x, nothrow, callback ){
|
|||
|
scrollListening = false;
|
|||
|
snapEvent( elem, x );
|
|||
|
|
|||
|
var after = function(){
|
|||
|
elem.scrollLeft = x;
|
|||
|
$(elem).closest( "." + pluginName ).removeClass( pluginName + "-looping" );
|
|||
|
$( elem ).trigger( pluginName + ".after-goto", {
|
|||
|
activeSlides: itemsAtOffset( elem, x )
|
|||
|
});
|
|||
|
|
|||
|
if( callback ){ callback(); };
|
|||
|
snapEvent( elem, x, "after" );
|
|||
|
scrollListening = true;
|
|||
|
};
|
|||
|
|
|||
|
// backport to old toss for compat
|
|||
|
if( !w.toss && w.overthrow ){
|
|||
|
w.toss = w.overthrow.toss;
|
|||
|
}
|
|||
|
if( typeof w.toss !== "undefined" && !nothrow ){
|
|||
|
w.toss( elem, { left: x, finished: after });
|
|||
|
}
|
|||
|
else {
|
|||
|
elem.scrollLeft = x;
|
|||
|
after();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
var result, innerResult;
|
|||
|
|
|||
|
// Loop through snapper elements and enhance/bind events
|
|||
|
result = this.each(function(){
|
|||
|
if( innerResult !== undefined ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var self = this;
|
|||
|
var $self = $( self );
|
|||
|
var addNextPrev = $self.is( "[data-" + pluginName + "-nextprev]" );
|
|||
|
var autoTimeout;
|
|||
|
var $slider = $( "." + pluginName + "_pane", self );
|
|||
|
var enhancedClass = pluginName + "-enhanced";
|
|||
|
var $itemsContain = $slider.find( "." + pluginName + "_items" );
|
|||
|
var $items = $itemsContain.children();
|
|||
|
$items.addClass( pluginName + "_item" );
|
|||
|
var numItems = $items.length;
|
|||
|
var $nav = $( "." + pluginName + "_nav", self );
|
|||
|
var navSelectedClass = pluginName + "_nav_item-selected";
|
|||
|
var useDeepLinking = $self.attr( "data-snapper-deeplinking" ) !== "false";
|
|||
|
|
|||
|
if( typeof optionsOrMethod === "string" ){
|
|||
|
var args = Array.prototype.slice.call(pluginArgs, 1);
|
|||
|
var index;
|
|||
|
var itemWidth = ( $itemsContain.width() / numItems );
|
|||
|
|
|||
|
switch(optionsOrMethod) {
|
|||
|
case "goto":
|
|||
|
index = args[0] % numItems;
|
|||
|
|
|||
|
// width / items * index to make sure it goes
|
|||
|
offset = itemWidth * index;
|
|||
|
goto( $slider[ 0 ], offset, false, function(){
|
|||
|
// snap the scroll to the right position
|
|||
|
snapScroll();
|
|||
|
|
|||
|
// invoke the callback if it was supplied
|
|||
|
if( typeof args[1] === "function" ){
|
|||
|
args[1]();
|
|||
|
}
|
|||
|
});
|
|||
|
break;
|
|||
|
case "getIndex":
|
|||
|
// NOTE make the scroll left value large enough to overcome
|
|||
|
// subpixel widths
|
|||
|
innerResult = Math.floor(($slider[ 0 ].scrollLeft + 1)/ itemWidth);
|
|||
|
break;
|
|||
|
case "updateWidths":
|
|||
|
updateWidths();
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// avoid double enhance activities
|
|||
|
if( $self.attr("data-" + pluginName + "-enhanced") ) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// NOTE all state manipulation has to come after method invocation to
|
|||
|
// avoid monkeying with the DOM when it's unwarranted
|
|||
|
var $navInner = $nav.find( "." + pluginName + "_nav_inner" );
|
|||
|
if( !$navInner.length ){
|
|||
|
$navInner = $( '<div class="'+ pluginName + '_nav_inner"></div>' ).append( $nav.children() ).appendTo( $nav );
|
|||
|
}
|
|||
|
|
|||
|
// give the pane a tabindex for arrow key handling
|
|||
|
$slider.attr("tabindex", "0");
|
|||
|
|
|||
|
function getAutoplayInterval() {
|
|||
|
var autoTiming = $self.attr( "data-autoplay" ) || $self.attr( "data-snapper-autoplay" );
|
|||
|
var parseError = false;
|
|||
|
|
|||
|
if( autoTiming ) {
|
|||
|
try {
|
|||
|
autoTiming = parseInt(autoTiming, 10);
|
|||
|
} catch(e) {
|
|||
|
parseError = true;
|
|||
|
}
|
|||
|
|
|||
|
// if NaN or there was an error throw an exception
|
|||
|
if( !autoTiming || parseError ) {
|
|||
|
var msg = "Snapper: `data-autoplay` must have an natural number value.";
|
|||
|
throw new Error(msg);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return autoTiming;
|
|||
|
}
|
|||
|
|
|||
|
// this function updates the widths of the items within the slider, and their container.
|
|||
|
// It factors in margins and converts those to values that make sense when all items are placed in a long row
|
|||
|
function updateWidths(){
|
|||
|
var itemsContainStyle = $itemsContain.attr( "style" );
|
|||
|
$itemsContain.attr( "style", "" );
|
|||
|
var itemStyle = $items.eq(0).attr( "style" );
|
|||
|
$items.eq(0).attr( "style", "" );
|
|||
|
var sliderWidth = $slider.width();
|
|||
|
var itemWidth = $items.eq(0).width();
|
|||
|
var computed = w.getComputedStyle( $items[ 0 ], null );
|
|||
|
var itemLeftMargin = parseFloat( computed.getPropertyValue( "margin-left" ) );
|
|||
|
var itemRightMargin = parseFloat( computed.getPropertyValue( "margin-right" ) );
|
|||
|
var outerItemWidth = itemWidth + itemLeftMargin + itemRightMargin;
|
|||
|
$items.eq(0).attr( "style", itemStyle );
|
|||
|
$itemsContain.attr( "style", itemsContainStyle );
|
|||
|
var parentWidth = numItems / Math.round(sliderWidth / outerItemWidth) * 100;
|
|||
|
var iPercentWidth = itemWidth / sliderWidth * 100;
|
|||
|
var iPercentRightMargin = itemRightMargin / sliderWidth * 100;
|
|||
|
var iPercentLeftMargin = itemLeftMargin / sliderWidth * 100;
|
|||
|
var outerPercentWidth = iPercentWidth + iPercentLeftMargin + iPercentRightMargin;
|
|||
|
var percentAsWidth = iPercentWidth / outerPercentWidth;
|
|||
|
var percentAsRightMargin = iPercentRightMargin / outerPercentWidth;
|
|||
|
var percentAsLeftMargin = iPercentLeftMargin / outerPercentWidth;
|
|||
|
$itemsContain.css( "width", parentWidth + "%");
|
|||
|
$items.css( "width", 100 / numItems * percentAsWidth + "%" );
|
|||
|
$items.css( "margin-left", 100 / numItems * percentAsLeftMargin + "%" );
|
|||
|
$items.css( "margin-right", 100 / numItems * percentAsRightMargin + "%" );
|
|||
|
}
|
|||
|
|
|||
|
updateWidths();
|
|||
|
$( self ).addClass( enhancedClass );
|
|||
|
|
|||
|
// if the nextprev option is set, add the nextprev nav
|
|||
|
if( addNextPrev ){
|
|||
|
var $nextprev = $( '<ul class="snapper_nextprev"><li class="snapper_nextprev_item"><a href="#prev" class="snapper_nextprev_prev">Prev</a></li><li class="snapper_nextprev_item"><a href="#next" class="snapper_nextprev_next">Next</a></li></ul>' );
|
|||
|
var $nextprevContain = $( ".snapper_nextprev_contain", self );
|
|||
|
if( !$nextprevContain.length ){
|
|||
|
$nextprevContain = $( self );
|
|||
|
}
|
|||
|
$nextprev.appendTo( $nextprevContain );
|
|||
|
}
|
|||
|
|
|||
|
// This click binding will allow deep-linking to slides without causing the page to scroll to the carousel container
|
|||
|
// this also supports click handling for generated next/prev links
|
|||
|
$( "a", this ).bind( "click", function( e ){
|
|||
|
clearTimeout(autoTimeout);
|
|||
|
var slideID = $( this ).attr( "href" );
|
|||
|
|
|||
|
if( $( this ).is( ".snapper_nextprev_next" ) ){
|
|||
|
e.preventDefault();
|
|||
|
return arrowNavigate( true );
|
|||
|
}
|
|||
|
else if( $( this ).is( ".snapper_nextprev_prev" ) ){
|
|||
|
e.preventDefault();
|
|||
|
return arrowNavigate( false );
|
|||
|
}
|
|||
|
// internal links to slides
|
|||
|
else if( slideID.indexOf( "#" ) === 0 && slideID.length > 1 ){
|
|||
|
e.preventDefault();
|
|||
|
|
|||
|
var $slide = $( slideID, self );
|
|||
|
if( $slide.length ){
|
|||
|
goto( $slider[ 0 ], $slide[ 0 ].offsetLeft );
|
|||
|
if( useDeepLinking && "replaceState" in w.history ){
|
|||
|
w.history.replaceState( {}, document.title, slideID );
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// arrow key bindings for next/prev
|
|||
|
$( this )
|
|||
|
.bind( "keydown", function( e ){
|
|||
|
if( e.keyCode === 37 || e.keyCode === 38 ){
|
|||
|
clearTimeout(autoTimeout);
|
|||
|
e.preventDefault();
|
|||
|
e.stopImmediatePropagation();
|
|||
|
arrowNavigate( false );
|
|||
|
}
|
|||
|
if( e.keyCode === 39 || e.keyCode === 40 ){
|
|||
|
clearTimeout(autoTimeout);
|
|||
|
e.preventDefault();
|
|||
|
e.stopImmediatePropagation();
|
|||
|
arrowNavigate( true );
|
|||
|
}
|
|||
|
} );
|
|||
|
|
|||
|
var snapScrollCancelled = false;
|
|||
|
|
|||
|
// snap to nearest slide. Useful after a scroll stops, for polyfilling snap points
|
|||
|
function snapScroll(){
|
|||
|
if(isTouched){
|
|||
|
snapScrollCancelled = true;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var currScroll = $slider[ 0 ].scrollLeft;
|
|||
|
var width = $itemsContain.width();
|
|||
|
var itemWidth = $items[ 1 ] ? $items[ 1 ].offsetLeft : outerWidth( $items.eq( 0 ) );
|
|||
|
var roundedScroll = Math.round(currScroll/itemWidth)*itemWidth;
|
|||
|
var maxScroll = width - $slider.width();
|
|||
|
if( roundedScroll > maxScroll ){
|
|||
|
roundedScroll = maxScroll;
|
|||
|
}
|
|||
|
if( currScroll !== roundedScroll ){
|
|||
|
if( snapSupported ){
|
|||
|
snapEvent( $slider[ 0 ], roundedScroll );
|
|||
|
snapEvent( $slider[ 0 ], roundedScroll, "after" );
|
|||
|
}
|
|||
|
else {
|
|||
|
goto( $slider[ 0 ], roundedScroll );
|
|||
|
}
|
|||
|
} else {
|
|||
|
goto( $slider[ 0 ], roundedScroll );
|
|||
|
}
|
|||
|
|
|||
|
snapScrollCancelled = false;
|
|||
|
}
|
|||
|
|
|||
|
// retain snapping on resize (necessary even in scroll-snap supporting browsers currently, unfortunately)
|
|||
|
var startSlide;
|
|||
|
var afterResize;
|
|||
|
function snapStay(){
|
|||
|
var currScroll = $slider[ 0 ].scrollLeft;
|
|||
|
var numItems = $items.length;
|
|||
|
var width = $itemsContain.width();
|
|||
|
if( startSlide === undefined ){
|
|||
|
startSlide = Math.round( currScroll / width * numItems );
|
|||
|
}
|
|||
|
if( afterResize ){
|
|||
|
clearTimeout( afterResize );
|
|||
|
}
|
|||
|
afterResize = setTimeout( function(){
|
|||
|
updateWidths();
|
|||
|
goto( $slider[ 0 ], $items[ startSlide ].offsetLeft, true );
|
|||
|
startSlide = afterResize = undefined;
|
|||
|
}, 50 );
|
|||
|
}
|
|||
|
$( w ).bind( "resize", snapStay );
|
|||
|
|
|||
|
// next/prev links or arrows should loop back to the other end when an extreme is reached
|
|||
|
function arrowNavigate( forward ){
|
|||
|
var currScroll = $slider[ 0 ].scrollLeft;
|
|||
|
var width = $itemsContain.width();
|
|||
|
var itemWidth = outerWidth( $slider );
|
|||
|
var maxScroll = width - itemWidth - 5;
|
|||
|
if( forward ){
|
|||
|
if( currScroll >= maxScroll ){
|
|||
|
$self.addClass( pluginName + "-looping" );
|
|||
|
return first();
|
|||
|
}
|
|||
|
else {
|
|||
|
return next();
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
if( currScroll === 0 ){
|
|||
|
$self.addClass( pluginName + "-looping" );
|
|||
|
return last();
|
|||
|
}
|
|||
|
else {
|
|||
|
return prev();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// advance slide one full scrollpane's width forward
|
|||
|
function next(){
|
|||
|
goto( $slider[ 0 ], $slider[ 0 ].scrollLeft + ( $itemsContain.width() / numItems ), false, function(){
|
|||
|
$slider.trigger( pluginName + ".after-next" );
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// advance slide one full scrollpane's width backwards
|
|||
|
function prev(){
|
|||
|
goto( $slider[ 0 ], $slider[ 0 ].scrollLeft - ( $itemsContain.width() / numItems ), false, function(){
|
|||
|
$slider.trigger( pluginName + ".after-prev" );
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// go to first slide
|
|||
|
function first(){
|
|||
|
goto( $slider[ 0 ], 0 );
|
|||
|
}
|
|||
|
|
|||
|
// go to last slide
|
|||
|
function last(){
|
|||
|
goto( $slider[ 0 ], $itemsContain.width() - $slider.width() );
|
|||
|
}
|
|||
|
|
|||
|
// update thumbnail state on pane scroll
|
|||
|
if( $nav.length ){
|
|||
|
// function for scrolling to the xy of the active thumbnail
|
|||
|
function scrollNav(elem, x, y){
|
|||
|
if( typeof w.toss !== "undefined" ){
|
|||
|
w.toss( elem, { left: x, top:y });
|
|||
|
}
|
|||
|
else {
|
|||
|
elem.scrollLeft = x;
|
|||
|
elem.scrollTop = y;
|
|||
|
}
|
|||
|
}
|
|||
|
var lastActiveItem;
|
|||
|
function activeItem( force ){
|
|||
|
var currTime = new Date().getTime();
|
|||
|
if( !force && lastActiveItem && currTime - lastActiveItem < 200 ){
|
|||
|
return;
|
|||
|
}
|
|||
|
lastActiveItem = currTime;
|
|||
|
var currScroll = $slider[ 0 ].scrollLeft;
|
|||
|
var width = outerWidth( $itemsContain );
|
|||
|
var navWidth = outerWidth( $nav );
|
|||
|
var navHeight = outerHeight( $nav );
|
|||
|
var activeIndex = Math.round( currScroll / width * numItems ) || 0;
|
|||
|
var childs = $nav.find( "a" ).removeClass( navSelectedClass );
|
|||
|
var activeChild = childs.eq( activeIndex ).addClass( navSelectedClass );
|
|||
|
|
|||
|
var thumbX = activeChild[ 0 ].offsetLeft - (navWidth/2);
|
|||
|
var thumbY = activeChild[ 0 ].offsetTop - (navHeight/2);
|
|||
|
|
|||
|
scrollNav( $navInner[ 0 ], thumbX, thumbY );
|
|||
|
}
|
|||
|
|
|||
|
// set active item on init
|
|||
|
activeItem();
|
|||
|
|
|||
|
$slider.bind( "scroll", activeItem );
|
|||
|
}
|
|||
|
|
|||
|
// apply snapping after scroll, in browsers that don't support CSS scroll-snap
|
|||
|
var scrollStop;
|
|||
|
var scrolling;
|
|||
|
var lastScroll = 0;
|
|||
|
|
|||
|
|
|||
|
$slider.bind( "scroll", function(e){
|
|||
|
lastScroll = new Date().getTime();
|
|||
|
scrolling = true;
|
|||
|
});
|
|||
|
|
|||
|
setInterval(function(){
|
|||
|
if( scrolling && lastScroll <= new Date().getTime() - 150) {
|
|||
|
snapScroll();
|
|||
|
if( activeItem ){
|
|||
|
activeItem( true );
|
|||
|
}
|
|||
|
scrolling = false;
|
|||
|
}
|
|||
|
}, 150);
|
|||
|
|
|||
|
var isTouched = false;
|
|||
|
|
|||
|
// if a touch event is fired on the snapper we know the user is trying to
|
|||
|
// interact with it and we should disable the auto play
|
|||
|
$slider.bind("touchstart", function(){
|
|||
|
clearTimeout(autoTimeout);
|
|||
|
|
|||
|
isTouched = true;
|
|||
|
});
|
|||
|
|
|||
|
$slider.bind("touchend", function(){
|
|||
|
isTouched = false;
|
|||
|
|
|||
|
if(snapScrollCancelled && !scrolling){
|
|||
|
snapScroll();
|
|||
|
scrolling = false;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// if the `data-autoplay` attribute is assigned a natural number value
|
|||
|
// use it to make the slides cycle until there is a user interaction
|
|||
|
function autoplay( autoTiming ) {
|
|||
|
if( autoTiming ){
|
|||
|
// autoTimeout is cleared in each user interaction binding
|
|||
|
autoTimeout = setTimeout(function(){
|
|||
|
var timeout = getAutoplayInterval();
|
|||
|
if( timeout ) {
|
|||
|
arrowNavigate(true);
|
|||
|
autoplay( timeout );
|
|||
|
}
|
|||
|
}, autoTiming);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
autoplay( getAutoplayInterval() );
|
|||
|
$self.attr("data-" + pluginName + "-enhanced", true);
|
|||
|
});
|
|||
|
|
|||
|
return (innerResult !== undefined ? innerResult : result);
|
|||
|
};
|
|||
|
}( this, jQuery ));
|
|||
|
|
|||
|
;(function( w ){
|
|||
|
var enlarge = function(){
|
|||
|
var $ = w.jQuery;
|
|||
|
var pluginName = "enlarge";
|
|||
|
$.fn[ pluginName ] = function( options ){
|
|||
|
var pluginArgs = arguments;
|
|||
|
|
|||
|
// options
|
|||
|
var o = $(this).data("options") || {
|
|||
|
button: true,
|
|||
|
hoverZoomWithoutClick: true,
|
|||
|
delay: 300,
|
|||
|
flyout: {
|
|||
|
width: 200,
|
|||
|
height: 200
|
|||
|
},
|
|||
|
placement: "inline",
|
|||
|
magnification: 3
|
|||
|
};
|
|||
|
|
|||
|
if( typeof options !== "string" ) {
|
|||
|
// extend with passed options
|
|||
|
o = $.extend( o, options );
|
|||
|
$(this).data("options", o);
|
|||
|
}
|
|||
|
|
|||
|
var internalResult;
|
|||
|
|
|||
|
var result = this.each(function(){
|
|||
|
var $element = $(this);
|
|||
|
|
|||
|
var self = this;
|
|||
|
var testimg = w.document.createElement( "img" );
|
|||
|
var srcsetSupported = "srcset" in testimg;
|
|||
|
var srcsetSizesSupported = srcsetSupported && "sizes" in testimg;
|
|||
|
var $anchor = $( this ).find( "a" );
|
|||
|
|
|||
|
if( !$anchor.length ){
|
|||
|
throw new Error(pluginName + ": requires an anchor element with `href` for the enlarged image source");
|
|||
|
}
|
|||
|
|
|||
|
// find image within container
|
|||
|
var initialImg = $element.find( "img" )[ 0 ];
|
|||
|
var targetImg = initialImg;
|
|||
|
var imgOriginalSrc = targetImg.src;
|
|||
|
var srcset = $( targetImg ).attr( "srcset" );
|
|||
|
var imgOriginalSizes = $( targetImg ).attr( "sizes" );
|
|||
|
var imgZoomSrc = $anchor[ 0 ].href;
|
|||
|
var initialText = $anchor[ 0 ].innerText;
|
|||
|
var zoomClass = pluginName + "-zoomed";
|
|||
|
var delayClass = pluginName + "-delay";
|
|||
|
var $contain = $( targetImg ).closest( ".enlarge_contain" );
|
|||
|
var $zoomContain = $contain;
|
|||
|
var $parentPane = $( targetImg ).closest( ".enlarge_pane" ) || $element;
|
|||
|
|
|||
|
var $zoomParent = $(this).data("zoomParent") || $parentPane;
|
|||
|
$(this).data("zoomParent", $zoomParent);
|
|||
|
|
|||
|
var zoomed = $element.data("zoomed") || false;
|
|||
|
$element.data("zoomed", zoomed);
|
|||
|
|
|||
|
$element.data("lockedZoom", $element.data("lockedZoom") || false);
|
|||
|
|
|||
|
var lockZoomClass = pluginName + "-locked";
|
|||
|
|
|||
|
if( !$contain.length ){
|
|||
|
throw new Error(pluginName + ": requires an element above the image marked with the class `enlarge_contain`");
|
|||
|
}
|
|||
|
|
|||
|
// this allows for methods and changing options in subsequent calls to the plugin
|
|||
|
if( typeof options === "string" ) {
|
|||
|
var args = Array.prototype.slice.call(pluginArgs, 1);
|
|||
|
|
|||
|
switch(options){
|
|||
|
case "in":
|
|||
|
if( !$element.data("zoomed") ){
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
break;
|
|||
|
case "out":
|
|||
|
if( $element.data("zoomed") ){
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
break;
|
|||
|
case "isZoomed":
|
|||
|
internalResult = $element.data("zoomed");
|
|||
|
break;
|
|||
|
case "updateOptions":
|
|||
|
$element.data( "updateOptions" )( args[ 0 ] );
|
|||
|
break;
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// to toggle between inline and flyout modes, we change the elements that
|
|||
|
// the targetImg, zoomParent, and zoomContain vars refer to
|
|||
|
function updatePlacement(){
|
|||
|
if( o.placement === "inline" ){
|
|||
|
targetImg = initialImg;
|
|||
|
$zoomParent = $parentPane;
|
|||
|
$element.data("zoomParent", $zoomParent);
|
|||
|
$zoomContain = $contain;
|
|||
|
} else {
|
|||
|
targetImg = $flyout.find( "img" )[ 0 ];
|
|||
|
$zoomParent = $zoomContain = $flyout;
|
|||
|
$element.data("zoomParent", $zoomParent);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// this positions the loupe or side flyout element either according to mouse/touch coordinates
|
|||
|
// or the sides of the viewer specified
|
|||
|
function positionFlyout(){
|
|||
|
|
|||
|
// set flyout width and height
|
|||
|
$flyout.css({
|
|||
|
"width": o.flyout.width + "px",
|
|||
|
"height": o.flyout.height + "px",
|
|||
|
top: "",
|
|||
|
left: "",
|
|||
|
"margin-left": "",
|
|||
|
"margin-top": ""
|
|||
|
});
|
|||
|
|
|||
|
// set negative left or right value to match width
|
|||
|
var flyoutSide = o.placement.match( /left|right/ );
|
|||
|
|
|||
|
if( flyoutSide ){
|
|||
|
$flyout.css( flyoutSide[ 0 ], -o.flyout.width + "px" );
|
|||
|
}
|
|||
|
// if loupe mode, center offset
|
|||
|
var loupe = o.placement.match( /loupe/ );
|
|||
|
|
|||
|
if( loupe ) {
|
|||
|
// loupe
|
|||
|
$flyout.css({
|
|||
|
"margin-left": ( -o.flyout.width / 2 ) + "px",
|
|||
|
"margin-top": ( -o.flyout.height / 2 ) + "px"
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// add class to specify positioning spot for static css to apply
|
|||
|
$flyout[ 0 ].className = $flyout[ 0 ].className.replace( /enlarge_flyout\-[^$\s]+/, ' ' );
|
|||
|
$flyout.addClass( "enlarge_flyout-" + o.placement );
|
|||
|
}
|
|||
|
|
|||
|
function disable(){
|
|||
|
if(o.disabled){
|
|||
|
$element.addClass("enlarge_disabled");
|
|||
|
} else {
|
|||
|
$element.removeClass("enlarge_disabled");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
disable();
|
|||
|
|
|||
|
// this allows for subsequent calls to the plugin to pass an updateOptions method and object,
|
|||
|
// which will pass through to the existing viewer on that element
|
|||
|
$element.data( "updateOptions", function( opts ){
|
|||
|
o = $.extend( o, opts );
|
|||
|
$(this).data("options", o);
|
|||
|
|
|||
|
updatePlacement();
|
|||
|
positionFlyout();
|
|||
|
hoverEnabled = o.hoverZoomWithoutClick;
|
|||
|
if( o.image && o.image.sizes ){
|
|||
|
imgOriginalSizes = o.image.sizes;
|
|||
|
toggleImgSrc();
|
|||
|
}
|
|||
|
|
|||
|
disable();
|
|||
|
|
|||
|
if( o.disabled && $element.data("zoomed") ) {
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// loader div holds a new image while its new source is loading
|
|||
|
// we insert this into the dom so that srcset/sizes can calculate a best source
|
|||
|
function addLoader(){
|
|||
|
$contain.append( '<i class="enlarge_loader"><i></i></i>' );
|
|||
|
}
|
|||
|
|
|||
|
// zoom state toggle boolean
|
|||
|
function toggleZoomState(){
|
|||
|
zoomed = !$element.data("zoomed");
|
|||
|
$element.data("zoomed", zoomed);
|
|||
|
}
|
|||
|
|
|||
|
// toggle the image source bigger or smaller
|
|||
|
// ideally, this toggles the sizes attribute and allows the browser to select a new source from srcset
|
|||
|
// if srcset isn't supported or sizes attribute is not provided, the link href is used for the larger source
|
|||
|
function toggleImgSrc(after){
|
|||
|
after = after || function(){};
|
|||
|
|
|||
|
if( !zoomed ){
|
|||
|
targetImg.sizes = imgOriginalSizes;
|
|||
|
if( !srcsetSizesSupported ){
|
|||
|
targetImg.src = imgOriginalSrc;
|
|||
|
}
|
|||
|
after();
|
|||
|
} else {
|
|||
|
// if the zooming is disabled do not replace with the larger source
|
|||
|
// NOTE we don't prevent switching to the original source because we
|
|||
|
// always want to allow the plugin to back out of the zoomed state
|
|||
|
// when disabled
|
|||
|
if( o.disabled ) { after(); return false; }
|
|||
|
|
|||
|
var zoomimg = new Image();
|
|||
|
zoomimg.className = "enlarge_img-loading";
|
|||
|
zoomimg.onload = function(){
|
|||
|
targetImg.sizes = zoomimg.sizes;
|
|||
|
if( !srcsetSizesSupported || !srcset ){
|
|||
|
targetImg.src = imgZoomSrc;
|
|||
|
}
|
|||
|
$( zoomimg ).remove();
|
|||
|
|
|||
|
after();
|
|||
|
};
|
|||
|
|
|||
|
zoomimg.sizes = imgZoomWidth() + "px";
|
|||
|
|
|||
|
if( !srcsetSizesSupported || !srcset ){
|
|||
|
zoomimg.src = imgZoomSrc;
|
|||
|
} else if (srcset) {
|
|||
|
zoomimg.srcset = srcset;
|
|||
|
}
|
|||
|
|
|||
|
$( zoomimg ).insertBefore( targetImg );
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// scroll to the center of the zoomed image
|
|||
|
function scrollToCenter(){
|
|||
|
var pw = $zoomContain.width();
|
|||
|
var ph = $zoomContain.height();
|
|||
|
var w = targetImg.offsetWidth;
|
|||
|
var h = targetImg.offsetHeight;
|
|||
|
$zoomContain[ 0 ].scrollLeft = ( w / 2 ) - ( pw / 2 );
|
|||
|
$zoomContain[ 0 ].scrollTop = ( h / 2 ) - ( ph / 2 );
|
|||
|
}
|
|||
|
|
|||
|
// lock zoom mode allows for scrolling around normally without a cursor-follow behavior
|
|||
|
function toggleLockZoom(){
|
|||
|
if( !$element.data("lockedZoom") ){
|
|||
|
// NOTE we allow the image to zoom out if functionality gets disabled
|
|||
|
// when it's in a zoomed state
|
|||
|
if(o.disabled) { return false; }
|
|||
|
|
|||
|
$parentPane.add( $zoomParent ).addClass( lockZoomClass );
|
|||
|
$element.data("lockedZoom", lockedZoom = true);
|
|||
|
$zoomContain.attr( "tabindex", "0" );
|
|||
|
$zoomContain[ 0 ].focus();
|
|||
|
}
|
|||
|
else {
|
|||
|
$parentPane.add( $zoomParent ).removeClass( lockZoomClass );
|
|||
|
$element.data("lockedZoom", lockedZoom = false);
|
|||
|
$zoomContain.removeAttr( "tabindex" );
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function imgZoomWidth(){
|
|||
|
return $parentPane[0].offsetWidth * o.magnification;
|
|||
|
}
|
|||
|
|
|||
|
// toggle magnification of image
|
|||
|
function toggleImgZoom(){
|
|||
|
if( $element.data("zoomed") ){
|
|||
|
// NOTE we allow the image to zoom out if functionality gets disabled
|
|||
|
// when it's in a zoomed state
|
|||
|
if(o.disabled) { return false; }
|
|||
|
|
|||
|
if( o.placement === "inline" ){
|
|||
|
$contain.add( $parentPane ).css( { "width": $parentPane[0].offsetWidth + "px", "height": parseFloat( getComputedStyle( $parentPane[0] ).height ) + "px" } );
|
|||
|
}
|
|||
|
$zoomParent.addClass( zoomClass );
|
|||
|
$( targetImg ).css( "width", imgZoomWidth() + "px" );
|
|||
|
|
|||
|
$(self).trigger( pluginName + ".after-zoom-in");
|
|||
|
}
|
|||
|
else{
|
|||
|
$zoomParent.removeClass( zoomClass );
|
|||
|
if( o.placement === "inline" ){
|
|||
|
$contain.add( $parentPane ).css( { "width": "", "height": "" } );
|
|||
|
}
|
|||
|
$( targetImg ).css( "width", "" );
|
|||
|
|
|||
|
$(self).trigger( pluginName + ".after-zoom-out");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function forceInlineMode(){
|
|||
|
var oldO = o.placement;
|
|||
|
if( oldO !== "inline" ){
|
|||
|
function resetPlacement(){
|
|||
|
o.placement = oldO;
|
|||
|
updatePlacement();
|
|||
|
$(self).unbind( pluginName + ".after-zoom-out", resetPlacement );
|
|||
|
}
|
|||
|
$(self).bind( pluginName + ".after-zoom-out", resetPlacement );
|
|||
|
o.placement = "inline";
|
|||
|
updatePlacement();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// lock zoom mode toggle
|
|||
|
function standardToggleZoom(){
|
|||
|
// NOTE if the current is zoomed out and it's disabled prevent toggling
|
|||
|
if(o.disabled && !$element.data("zoomed")) { return false; }
|
|||
|
toggleZoomState();
|
|||
|
toggleImgSrc(function(){
|
|||
|
toggleLockZoom();
|
|||
|
toggleImgZoom();
|
|||
|
scrollToCenter();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
var trackingOn;
|
|||
|
var trackingTimer;
|
|||
|
var mouseEntered;
|
|||
|
var touchStarted;
|
|||
|
var hoverEnabled = o.hoverZoomWithoutClick;
|
|||
|
var lastMouseMove;
|
|||
|
|
|||
|
// mouseenter or touchstart handler for dragging image
|
|||
|
function startTrackingDelay(e){
|
|||
|
if( e.type === "touchstart" ){
|
|||
|
touchStarted = true;
|
|||
|
}
|
|||
|
if( touchStarted && e.type === "mouseenter" ||
|
|||
|
e.type === "mouseenter" && !touchStarted && !hoverEnabled ||
|
|||
|
$element.data("lockedZoom") ||
|
|||
|
mouseEntered ){
|
|||
|
return;
|
|||
|
}
|
|||
|
mouseEntered = true;
|
|||
|
$contain.addClass( delayClass );
|
|||
|
trackingTimer = setTimeout( function(){
|
|||
|
$contain.removeClass( delayClass );
|
|||
|
toggleZoomState();
|
|||
|
toggleImgSrc(function(){
|
|||
|
toggleImgZoom();
|
|||
|
trackingOn = true;
|
|||
|
if (lastMouseMove) {
|
|||
|
scrollWithMouse(lastMouseMove);
|
|||
|
} else {
|
|||
|
scrollWithMouse(e);
|
|||
|
}
|
|||
|
});
|
|||
|
}, o.delay );
|
|||
|
}
|
|||
|
|
|||
|
// mouseleave or touchend after a drag
|
|||
|
function stopTrackingDelay( e ){
|
|||
|
$contain.removeClass( delayClass );
|
|||
|
clearTimeout( trackingTimer );
|
|||
|
trackingOn = false;
|
|||
|
if( o.hoverZoomWithoutClick === false && !touchStarted ){
|
|||
|
hoverEnabled = false;
|
|||
|
}
|
|||
|
if( touchStarted && e.type === "mouseleave" ){
|
|||
|
touchStarted = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// mousemove or touch-drag image placement
|
|||
|
function scrollWithMouse( e ){
|
|||
|
// if tracking's not on yet, ignore. This allows the delay to work
|
|||
|
if( trackingOn ){
|
|||
|
// if the move was touch-started, and the event is mousemove, it can be ignored
|
|||
|
// (mouse events fire along with touch events and we just want the touch)
|
|||
|
if( touchStarted && e.type === "mousemove" ){
|
|||
|
return;
|
|||
|
}
|
|||
|
// normalize ev to touch or mouse
|
|||
|
var ev = e.touches ? e.touches[ 0 ] : e;
|
|||
|
e.preventDefault();
|
|||
|
var x = ev.clientX - $contain[ 0 ].getBoundingClientRect().left;
|
|||
|
var y = ev.clientY - $contain[ 0 ].getBoundingClientRect().top;
|
|||
|
|
|||
|
if( o.placement.match( /loupe/ ) ) {
|
|||
|
// offset the loupe a little differently for touch so that it's not directly beneath a finger
|
|||
|
var mLeft = ( e.touches ? -o.flyout.width / 1.3 : -o.flyout.width / 2 ) + "px";
|
|||
|
var mTop = ( e.touches ? -o.flyout.height / 1.3 : -o.flyout.height / 2 ) + "px";
|
|||
|
requestAnimationFrame(function(){
|
|||
|
$flyout.css( {
|
|||
|
top: y + "px",
|
|||
|
left: x + "px",
|
|||
|
"margin-left": mLeft,
|
|||
|
"margin-top": mTop
|
|||
|
} );
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
var containWidth = $contain[ 0 ].offsetWidth;
|
|||
|
var containHeight = $contain[ 0 ].offsetHeight;
|
|||
|
var containScrollWidth = targetImg.offsetWidth;
|
|||
|
var containScrollHeight = targetImg.offsetHeight;
|
|||
|
var zoomContainWidth = $zoomContain[ 0 ].offsetWidth;
|
|||
|
var zoomContainHeight = $zoomContain[ 0 ].offsetHeight;
|
|||
|
var widthFactor = containWidth / zoomContainWidth;
|
|||
|
var heightFactor = containHeight / zoomContainHeight;
|
|||
|
|
|||
|
$zoomContain[ 0 ].scrollLeft = (( x / containWidth ) * ( containScrollWidth - zoomContainWidth ));
|
|||
|
$zoomContain[0].scrollLeft += ((x / containWidth) - 0.5) * zoomContainWidth;
|
|||
|
$zoomContain[ 0 ].scrollTop = (( y / containHeight ) * ( containScrollHeight - zoomContainHeight )) ;
|
|||
|
$zoomContain[0].scrollTop += ((y / containHeight) - 0.5) * zoomContainHeight;
|
|||
|
} else {
|
|||
|
lastMouseMove = e;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// add flyout for flyout and loupe modes
|
|||
|
// flyout is always present, yet hidden when inline mode is active
|
|||
|
var $flyout = $( '<div class="enlarge_flyout"></div>' ).append( $contain.clone() );
|
|||
|
$flyout.insertAfter( $parentPane );
|
|||
|
|
|||
|
// add loader div
|
|||
|
addLoader();
|
|||
|
updatePlacement();
|
|||
|
positionFlyout();
|
|||
|
|
|||
|
// clicking the magnify anchor toggles lock-zoom mode
|
|||
|
$anchor
|
|||
|
.bind( "keydown", function(e){
|
|||
|
if( e.keyCode === 13 || e.keyCode === 32 ){
|
|||
|
forceInlineMode();
|
|||
|
}
|
|||
|
// spacebar triggers click too
|
|||
|
if( e.keyCode === 32 ){
|
|||
|
e.preventDefault(); // don't scroll the new focused area
|
|||
|
$( this ).trigger( "click" );
|
|||
|
}
|
|||
|
})
|
|||
|
.bind( "click", function( e ){
|
|||
|
e.preventDefault();
|
|||
|
standardToggleZoom();
|
|||
|
});
|
|||
|
|
|||
|
// on resize, if in lock zoom mode, un zoom
|
|||
|
$( w )
|
|||
|
.bind( "resize", function( e ){
|
|||
|
if( $element.data("lockedZoom") ){
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// on click-out on the page, if in locked zoom mode, zoom out
|
|||
|
$( w.document )
|
|||
|
.bind( "mouseup", function( e ){
|
|||
|
if( $element.data("lockedZoom") && !$( e.target ).closest( $parentPane ).length ){
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// mouse hover and touch-drag gestures for a cursor-tracked zoom behavior
|
|||
|
$( initialImg )
|
|||
|
.bind( "mouseenter touchstart", startTrackingDelay )
|
|||
|
.bind( "mousemove touchmove", scrollWithMouse )
|
|||
|
.bind( "mouseleave touchend", function( e ){
|
|||
|
mouseEntered = false;
|
|||
|
if( zoomed && !$element.data("lockedZoom") ){
|
|||
|
toggleZoomState();
|
|||
|
toggleImgSrc(function(){
|
|||
|
toggleImgZoom();
|
|||
|
});
|
|||
|
}
|
|||
|
stopTrackingDelay( e );
|
|||
|
})
|
|||
|
// tapping the image should trigger a lock zoom
|
|||
|
// click will not fire after a touch-drag so it works as a tap for our needs here
|
|||
|
.bind( "click", function( e ){
|
|||
|
e.preventDefault();
|
|||
|
// if the click was started with a touchstart event,
|
|||
|
// and placement is inline
|
|||
|
// toggle the locked zoom mode
|
|||
|
if( touchStarted && o.placement === "inline" ){
|
|||
|
standardToggleZoom();
|
|||
|
}
|
|||
|
|
|||
|
if( o.hoverZoomWithoutClick === false && !touchStarted ){
|
|||
|
hoverEnabled = !hoverEnabled;
|
|||
|
if( hoverEnabled ){
|
|||
|
$( this ).trigger( "mouseenter" );
|
|||
|
}
|
|||
|
else {
|
|||
|
$( this ).trigger( "mouseleave" );
|
|||
|
}
|
|||
|
}
|
|||
|
} );
|
|||
|
|
|||
|
// keyboard handling for arrows in zoom mode
|
|||
|
$(this).bind( "keydown keyup", function( e ){
|
|||
|
if( e.keyCode === 37 ||
|
|||
|
e.keyCode === 38 ||
|
|||
|
e.keyCode === 39 ||
|
|||
|
e.keyCode === 40 ){
|
|||
|
e.stopImmediatePropagation();
|
|||
|
if( !$element.data("lockedZoom") ){
|
|||
|
e.preventDefault();
|
|||
|
}
|
|||
|
} else if( e.type === "keyup" && $(this).data("lockedZoom") && e.keyCode === 27 ){ //esc or backspace closes zoom
|
|||
|
standardToggleZoom();
|
|||
|
$anchor[0].focus();
|
|||
|
e.stopImmediatePropagation();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// on scroll, zoom out
|
|||
|
$parentPane.bind( "scroll", function(){
|
|||
|
if( $element.data("zoomed") ){
|
|||
|
toggleZoomState();
|
|||
|
toggleImgSrc(function(){
|
|||
|
if( $element.data("lockedZoom") ){
|
|||
|
toggleLockZoom();
|
|||
|
}
|
|||
|
toggleImgZoom();
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
return internalResult !== undefined ? internalResult : result;
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
if( typeof module !== "undefined" ){
|
|||
|
module.exports = enlarge;
|
|||
|
} else {
|
|||
|
enlarge();
|
|||
|
}
|
|||
|
}( typeof global !== "undefined" ? global : this ));
|
|||
|
|
|||
|
(function(exports){
|
|||
|
var Image = {
|
|||
|
defaultConfig: {
|
|||
|
widths: [
|
|||
|
"320",
|
|||
|
"640",
|
|||
|
"800",
|
|||
|
"1024",
|
|||
|
"2048",
|
|||
|
"5000"
|
|||
|
],
|
|||
|
|
|||
|
sizes: "100vw",
|
|||
|
policy: undefined,
|
|||
|
widthParam: "imwidth"
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Image = Image;
|
|||
|
})(typeof exports === 'undefined'? window : exports);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* 360 degree viewer
|
|||
|
* @class
|
|||
|
* @alias Akamai.Spin360
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Spin360 = Akamai.Util.component("Spin360", function(element, options){
|
|||
|
this._comp = new Akamai.Tau(element, this._options);
|
|||
|
});
|
|||
|
|
|||
|
// srcset stuff
|
|||
|
Akamai.Sourceable.extendStatic(Spin360);
|
|||
|
|
|||
|
// Used in preflight to "rename" events based on the child component events
|
|||
|
Spin360._componentEventMapping = {
|
|||
|
"tau.auto-rotate-start": "play",
|
|||
|
"tau.auto-rotate-stop": "pause"
|
|||
|
};
|
|||
|
|
|||
|
Spin360.prototype._updateOptions = function(options){
|
|||
|
if( !options ) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Spin360.defaultOptions, this._options, "Spin360");
|
|||
|
|
|||
|
// TODO actually update the options
|
|||
|
};
|
|||
|
|
|||
|
Spin360._renderImg = function(url, options){
|
|||
|
return `
|
|||
|
<img src="${Spin360._fallbackSrc(url, options.images)}"
|
|||
|
srcset="${Spin360._srcset(url, options.images)}"
|
|||
|
sizes="${options.images.sizes}" />
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
Spin360.render = function(json, options){
|
|||
|
var finalOptions = Akamai.Util.options(Spin360.defaultOptions, options);
|
|||
|
|
|||
|
// TODO fix the string problem in shoestring
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<div class="tau" data-akamai-spin360 title="${json.alt}">
|
|||
|
${Akamai.Util
|
|||
|
.map(json.urls, function(url) { return Spin360._renderImg(url, finalOptions); })
|
|||
|
.join("\n")
|
|||
|
}
|
|||
|
</div>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Spin360s on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} autoplay.enabled - Enable autoplay (default: false)
|
|||
|
* @property {Integer} autoplay.delay - Delay in milliseconds after initialization before spinning begins (default: 1000)
|
|||
|
* @property {Boolean} controls.arrows - Render controls to spin left and right (default: false)
|
|||
|
* @property {Boolean} controls.play - Render controls to enable and disable automatic spinning (default: false)
|
|||
|
* @property {String} controls.text.left - Left rotation control title and text (default: "Rotate Left")
|
|||
|
* @property {String} controls.text.right - Right rotation control title and text (default: "Rotate Right")
|
|||
|
* @property {String} controls.text.play - Spin control title and text (default: "Spin Object")
|
|||
|
* @property {Array} images.widths - list of available widths for an image (to be combined with image.widthParam), Default: ["320","640","800","1024","2048","5000"]
|
|||
|
* @property {String} images.sizes - value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw`, `200px`, `(min-width:1000px) 500px, 100vw`.
|
|||
|
* @property {String} images.policy - query param value for policy, appended to &policy= per image url when specified. Values: `foo`. Default: undefined.
|
|||
|
* @property {String} images.widthParam - query string name to use for setting each url width. Default urls will be ?imwidth=320 for example. Values: `imwidth` (default), `w`, `width`, etc.
|
|||
|
* @property {Integer} interval - The full rotation interval in milliseconds, determines physics, (default: 3000)
|
|||
|
* @property {Boolean} reverse - Reverse the direction of the spin (default: false)
|
|||
|
* @property {Float} sensitivity - The speed at which the object rotates relative to user input (default: 1). At the default value of `1` the object will complete a full 360 rotation when you drag across the entire width of the spin360 component. To require less effort to complete a rotation, change this value to a higher number. For example, setting `sensitivity: 2` would complete a full 360 rotation by dragging halfway (ex. from the center to the edge).
|
|||
|
*/
|
|||
|
Spin360.defaultOptions = {
|
|||
|
autoplay: {
|
|||
|
enabled: false,
|
|||
|
delay: 1000
|
|||
|
},
|
|||
|
|
|||
|
controls: {
|
|||
|
arrows: false,
|
|||
|
play: false,
|
|||
|
text: {
|
|||
|
left: "Rotate Left",
|
|||
|
right: "Rotate Right",
|
|||
|
play: "Spin Object"
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// unsupported, the number of frame images to create using the configured
|
|||
|
// template
|
|||
|
frames: 72,
|
|||
|
images: Akamai.Image.defaultConfig,
|
|||
|
interval: 3000,
|
|||
|
reverse: false,
|
|||
|
|
|||
|
sensitivity: 1,
|
|||
|
|
|||
|
// unsupported, template used to generate urls when only one image is
|
|||
|
// present in the 360 viewer
|
|||
|
template: undefined
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Goto to a particular frame of the spining image
|
|||
|
* @method
|
|||
|
* @param {Integer} index - the frame to advance to
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.goto = function(index, callback){
|
|||
|
this._comp.goto(index);
|
|||
|
// NOTE goto must come before the callback because the callback will be
|
|||
|
// used to trigger the `next` and `previous` events. The order should be
|
|||
|
// maintained consistently as `goto` -> `next`/`previous`
|
|||
|
this._trigger("goto");
|
|||
|
if(callback){ callback(); }
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current frame index
|
|||
|
* @method
|
|||
|
* @returns {Integer}
|
|||
|
*/
|
|||
|
Spin360.prototype.getIndex = function(){
|
|||
|
return this._comp.index;
|
|||
|
};
|
|||
|
|
|||
|
// Extend Carousel with Advanceable interface
|
|||
|
Akamai.Advanceable.extend(Spin360);
|
|||
|
|
|||
|
/**
|
|||
|
* Go to the next frame
|
|||
|
* @method
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.next;
|
|||
|
// required for JSDocs
|
|||
|
|
|||
|
/**
|
|||
|
* Go to the previous frame
|
|||
|
* @method
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.previous;
|
|||
|
// required for JSDocs
|
|||
|
|
|||
|
/**
|
|||
|
* Begin the automatic rotation of the images
|
|||
|
* @todo support passing in an interval or speed?
|
|||
|
* @method
|
|||
|
* @fires Akamai.Spin360#akamai-spin360-play
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.play = function(){
|
|||
|
this._comp.autoRotate();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Stop the automatic rotation of the images
|
|||
|
* @method
|
|||
|
* @fires Akamai.Spin360#akamai-spin360-pause
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.pause = function(){
|
|||
|
this._comp.stopAutoRotate();
|
|||
|
};
|
|||
|
|
|||
|
Spin360.prototype.getElement = function(){
|
|||
|
return this._$el[0];
|
|||
|
};
|
|||
|
|
|||
|
Spin360.States = {
|
|||
|
Playing: 0,
|
|||
|
Paused: 1
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the spin360
|
|||
|
* @example <caption>Spin360 states</caption>
|
|||
|
* Akamai.Spin360.States = {
|
|||
|
* Playing: 0,
|
|||
|
* Paused: 1
|
|||
|
* };
|
|||
|
*
|
|||
|
* @example <caption>Conditioning on states</caption>
|
|||
|
* if( spin360.getState() === Akamai.Spin360.States.Paused ) {
|
|||
|
* spin360.play()
|
|||
|
* }
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Spin360.State}
|
|||
|
*/
|
|||
|
Spin360.prototype.getState = function(){
|
|||
|
// TODO expose using method in Tau
|
|||
|
return !!this._comp.autoInterval ? Spin360.States.Playing : Spin360.States.Paused;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the spin360 has stoped automatically spinning
|
|||
|
* {@link Akamai.Spin360#pause}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-stop-spin
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the spin360 has started automatically spinning
|
|||
|
* {@link Akamai.Spin360#play}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-start-spin
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Spin360}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Spin360 = Spin360;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(){
|
|||
|
function MagnifierImpl(element, options){
|
|||
|
throw new Error('Not allowed to instantiate MagnifierImpl');
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImpl.prototype.updateOptions = function(options){
|
|||
|
throw new Error('magnifier updateOptions not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.zoomIn = function(){
|
|||
|
throw new Error('magnifier zoomIn not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.zoomOut = function(){
|
|||
|
throw new Error('magnifier zoomOut not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.isMagnified = function(){
|
|||
|
throw new Error('magnifier isMagnified not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.toggleZoom = function(){
|
|||
|
throw new Error('magnifier toggleZoom not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.render = function(options, dataAttr, fallbackSrc, largestSrc, srcSet){
|
|||
|
throw new Error('magnifier render not implemented');
|
|||
|
};
|
|||
|
|
|||
|
var ex = (typeof exports === 'undefined') ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImpl = MagnifierImpl;
|
|||
|
})();
|
|||
|
(function(){
|
|||
|
function MagnifierImplEnlarge(element, options){
|
|||
|
this._$el = $(element);
|
|||
|
this._$el.enlarge(options);
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype = Object.create(Akamai.MagnifierImpl.prototype);
|
|||
|
MagnifierImplEnlarge.prototype.constructor = MagnifierImplEnlarge;
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.updateOptions = function(options){
|
|||
|
this._$el.enlarge( "updateOptions", options );
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.zoomIn = function(){
|
|||
|
this._$el.enlarge( "in" );
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.zoomOut = function(){
|
|||
|
this._$el.enlarge( "out" );
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.isMagnified = function(){
|
|||
|
return this._$el.enlarge( "isZoomed" );
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.toggleZoom = function(){
|
|||
|
if(this.isMagnified()){
|
|||
|
this.zoomOut();
|
|||
|
} else {
|
|||
|
this.zoomIn();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.render = function(options, dataAttr, fallbackSrc, largestSrc, srcSet){
|
|||
|
return `
|
|||
|
<div ${dataAttr}>
|
|||
|
<div class="enlarge_contain">
|
|||
|
<img src="${fallbackSrc}"
|
|||
|
srcset="${srcSet}"
|
|||
|
sizes="${options.image.sizes}">
|
|||
|
</div>
|
|||
|
<a href="${largestSrc}"
|
|||
|
class="enlarge_btn"
|
|||
|
title="${options.buttonText}">
|
|||
|
${options.buttonText}
|
|||
|
</a>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
var ex = (typeof exports === 'undefined') ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImplEnlarge = MagnifierImplEnlarge;
|
|||
|
})();
|
|||
|
(function(){
|
|||
|
function ClickTracker(element, handler){
|
|||
|
var self = this;
|
|||
|
self.element = element;
|
|||
|
self.handler = handler;
|
|||
|
this.reset();
|
|||
|
}
|
|||
|
|
|||
|
ClickTracker.prototype.onDown = function(e){
|
|||
|
if (e.target == this.element) {
|
|||
|
if (this.isDown) {
|
|||
|
this.reset();
|
|||
|
} else {
|
|||
|
this.down.x = e.clientX;
|
|||
|
this.down.y = e.clientY;
|
|||
|
this.down.timestamp = new Date();
|
|||
|
this.isDown = true;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.onMove = function(){
|
|||
|
this.isMoved = true;
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.onUp = function(e){
|
|||
|
if (this.isDown) {
|
|||
|
this.up.x = e.clientX;
|
|||
|
this.up.y = e.clientY;
|
|||
|
this.up.timestamp = new Date();
|
|||
|
|
|||
|
if (!this.isMoved && (this.down.x === this.up.x && this.down.y === this.up.y && (this.up.timestamp.getTime() - this.down.timestamp.getTime() <= 500))) {
|
|||
|
this.handler();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this.reset();
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.reset = function(x, y){
|
|||
|
this.down = {
|
|||
|
x: -Infinity,
|
|||
|
y: -Infinity,
|
|||
|
timestamp: new Date(0)
|
|||
|
};
|
|||
|
|
|||
|
this.isDown = false;
|
|||
|
this.isMoved = false;
|
|||
|
|
|||
|
this.up = {
|
|||
|
x: Infinity,
|
|||
|
y: Infinity,
|
|||
|
timestamp: new Date()
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
function MagnifierImplScroller(element, options){
|
|||
|
this._$el = $(element);
|
|||
|
this._options = options;
|
|||
|
this._container = this._$el.get(0);
|
|||
|
this._content = this._$el.children().get(0);
|
|||
|
this._imgCast = $(this._content).find('div').get(0);
|
|||
|
this._zoomInBtn = this._$el.find("a.scroller-zoom-in").get(0);
|
|||
|
this._zoomOutBtn = this._$el.find("a.scroller-zoom-out").get(0);
|
|||
|
this._magnification = 1;
|
|||
|
|
|||
|
var self = this;
|
|||
|
|
|||
|
// Initialize Scroller
|
|||
|
this.scroller = new Scroller(this._renderer(), {
|
|||
|
zooming: options.enabled,
|
|||
|
minZoom: 1,
|
|||
|
maxZoom: options.magnification,
|
|||
|
animationDuration: options.animationDuration
|
|||
|
});
|
|||
|
|
|||
|
var rect = this._container.getBoundingClientRect();
|
|||
|
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
|
|||
|
|
|||
|
this._installEventHandlers();
|
|||
|
this._updateButtonStates();
|
|||
|
setTimeout(function(){
|
|||
|
self._onResize();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImplScroller.prototype = Object.create(Akamai.MagnifierImpl.prototype);
|
|||
|
MagnifierImplScroller.prototype.constructor = MagnifierImplScroller;
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.updateOptions = function(options){
|
|||
|
var rect = this._container.getBoundingClientRect();
|
|||
|
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
|
|||
|
this._onResize();
|
|||
|
this._zoomBy(1 / this._options.magnification, false);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.zoomIn = function(){
|
|||
|
this._zoomBy(this._options.incrementalZoomFactor);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.cyclicZoom = function(){
|
|||
|
var self = this;
|
|||
|
if (this._magnification >= this._options.magnification) {
|
|||
|
this._zoomBy(1 / this._options.magnification);
|
|||
|
} else {
|
|||
|
this.zoomIn();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.zoomOut = function(){
|
|||
|
this._zoomBy(1 / this._options.incrementalZoomFactor);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.isMagnified = function(){
|
|||
|
return this._magnification > 1;
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.toggleZoom = function(){
|
|||
|
throw new Error('zoom toggle behavior is undefined in scroller mode');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.render = function(options, dataAttr, fallbackSrc, largestSrc, srcSet){
|
|||
|
return `
|
|||
|
<div ${dataAttr}>
|
|||
|
<div>
|
|||
|
<img src="${fallbackSrc}"
|
|||
|
srcset="${srcSet}"
|
|||
|
sizes="${options.image.sizes}">
|
|||
|
<div></div>
|
|||
|
</div>
|
|||
|
<a href="${largestSrc}"
|
|||
|
class="scroller-zoom-in"
|
|||
|
title="${options.zoomInBtnText}">
|
|||
|
${options.zoomInBtnText}
|
|||
|
</a>
|
|||
|
<a href="${largestSrc}"
|
|||
|
class="scroller-zoom-out"
|
|||
|
title="${options.zoomOutBtnText}">
|
|||
|
${options.zoomOutBtnText}
|
|||
|
</a>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._zoomBy = function(magnification, animate){
|
|||
|
var self = this, magnification = Number(magnification);
|
|||
|
|
|||
|
if (typeof(animate) === "undefined") {
|
|||
|
animate = this._options.animateZoom;
|
|||
|
}
|
|||
|
|
|||
|
if (magnification === 1 || // if we are magnifying by 1 OR
|
|||
|
(this._magnification === 1 && magnification < 1) || // fully zoomed out and still attempting a zoom out OR
|
|||
|
(this._magnification === this._options.magnification && magnification > 1)) { // fully zoomed in and still attempting a zoom in
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.scroller.zoomTo(this._magnification * magnification, this._options.animateZoom);
|
|||
|
|
|||
|
setTimeout(function(){
|
|||
|
self._onZoom();
|
|||
|
}, this._options.animationDuration);
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._onZoom = function(){
|
|||
|
var oldMagnification = this._magnification;
|
|||
|
|
|||
|
this._magnification = Number(this.scroller.getValues().zoom.toFixed(2));
|
|||
|
this._updateButtonStates();
|
|||
|
|
|||
|
if (oldMagnification > this._magnification) {
|
|||
|
this._$el.trigger("scroller.after-zoom-out");
|
|||
|
} else if (oldMagnification < this._magnification) {
|
|||
|
this._$el.trigger("scroller.after-zoom-in");
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._renderer = function(){
|
|||
|
var docStyle = document.documentElement.style, self = this;
|
|||
|
|
|||
|
var engine;
|
|||
|
if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
|
|||
|
engine = 'presto';
|
|||
|
} else if ('MozAppearance' in docStyle) {
|
|||
|
engine = 'gecko';
|
|||
|
} else if ('WebkitAppearance' in docStyle) {
|
|||
|
engine = 'webkit';
|
|||
|
} else if (typeof navigator.cpuClass === 'string') {
|
|||
|
engine = 'trident';
|
|||
|
}
|
|||
|
|
|||
|
var vendorPrefix = {
|
|||
|
trident: 'ms',
|
|||
|
gecko: 'Moz',
|
|||
|
webkit: 'Webkit',
|
|||
|
presto: 'O'
|
|||
|
}[engine];
|
|||
|
|
|||
|
var helperElem = document.createElement("div");
|
|||
|
var undef;
|
|||
|
|
|||
|
var perspectiveProperty = vendorPrefix + "Perspective";
|
|||
|
var transformProperty = vendorPrefix + "Transform";
|
|||
|
|
|||
|
if (helperElem.style[perspectiveProperty] !== undef) {
|
|||
|
return function(left, top, zoom) {
|
|||
|
self._content.style[transformProperty] = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
|
|||
|
};
|
|||
|
|
|||
|
} else if (helperElem.style[transformProperty] !== undef) {
|
|||
|
return function(left, top, zoom) {
|
|||
|
self._content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
|
|||
|
};
|
|||
|
|
|||
|
} else {
|
|||
|
return function(left, top, zoom) {
|
|||
|
self._content.style.marginLeft = left ? (-left/zoom) + 'px' : '';
|
|||
|
self._content.style.marginTop = top ? (-top/zoom) + 'px' : '';
|
|||
|
self._content.style.zoom = zoom || '';
|
|||
|
};
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._onResize = function(){
|
|||
|
this.scroller.setDimensions(this._container.clientWidth, this._container.clientHeight, this._container.clientWidth, this._container.clientHeight);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._installEventHandlers = function(){
|
|||
|
var self = this, clickTracker = new ClickTracker(this._imgCast, function(){
|
|||
|
self.cyclicZoom();
|
|||
|
});
|
|||
|
|
|||
|
window.addEventListener("resize", function(e){
|
|||
|
self._onResize(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
this._zoomInBtn.addEventListener("click", function(e) {
|
|||
|
e.preventDefault();
|
|||
|
self.zoomIn();
|
|||
|
}, false);
|
|||
|
|
|||
|
this._zoomOutBtn.addEventListener("click", function(e) {
|
|||
|
e.preventDefault();
|
|||
|
self.zoomOut();
|
|||
|
}, false);
|
|||
|
|
|||
|
if ('ontouchstart' in window) {
|
|||
|
self._container.addEventListener("touchstart", function(e) {
|
|||
|
// Don't react if initial down happens on one of the zoom buttons
|
|||
|
if (e.touches[0].target == self._$el.find('a.scroller-zoom-in').get(0) || e.touches[0].target == self._$el.find('a.scroller-zoom-out').get(0)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Don't react if initial down happens on a form element
|
|||
|
if (e.touches[0] && e.touches[0].target && e.touches[0].target.tagName.match(/input|textarea|select/i)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchStart(e.touches, e.timeStamp);
|
|||
|
|
|||
|
for (var i = 0; i < e.touches.length; i++) {
|
|||
|
clickTracker.onDown(e.touches[i]);
|
|||
|
}
|
|||
|
|
|||
|
e.preventDefault();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchmove", function(e) {
|
|||
|
self.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
|
|||
|
clickTracker.onMove();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchend", function(e) {
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
|
|||
|
// handle any zoom that may have occured
|
|||
|
self._onZoom();
|
|||
|
|
|||
|
for (var i = 0; i < e.changedTouches.length; i++) {
|
|||
|
clickTracker.onUp(e.changedTouches[i]);
|
|||
|
}
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchcancel", function(e) {
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
// handle any zoom that may have occured
|
|||
|
self._onZoom();
|
|||
|
|
|||
|
for (var i = 0; i < e.changedTouches.length; i++) {
|
|||
|
clickTracker.onUp(e.changedTouches[i]);
|
|||
|
}
|
|||
|
}, false);
|
|||
|
} else {
|
|||
|
self._container.addEventListener("mousedown", function(e) {
|
|||
|
if (e.target.tagName.match(/input|textarea|select/i)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (self.scroller.__clientWidth === 0) {
|
|||
|
self._onResize();
|
|||
|
}
|
|||
|
self.scroller.doTouchStart([{
|
|||
|
pageX: e.pageX,
|
|||
|
pageY: e.pageY
|
|||
|
}], e.timeStamp);
|
|||
|
|
|||
|
clickTracker.onDown(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("mousemove", function(e) {
|
|||
|
if (!clickTracker.isDown) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchMove([{
|
|||
|
pageX: e.pageX,
|
|||
|
pageY: e.pageY
|
|||
|
}], e.timeStamp);
|
|||
|
|
|||
|
clickTracker.onMove();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("mouseup", function(e) {
|
|||
|
if (!clickTracker.isDown) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
clickTracker.onUp(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
// self._container.addEventListener(navigator.userAgent.indexOf("Firefox") > -1 ? "DOMMouseScroll" : "mousewheel", function(e) {
|
|||
|
// e.preventDefault();
|
|||
|
// self.scroller.doMouseZoom(e.detail ? (e.detail * -120) : e.wheelDelta, e.timeStamp, e.pageX, e.pageY);
|
|||
|
// }, false);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._updateButtonStates = function(){
|
|||
|
if (this._magnification === 1) {
|
|||
|
$(this._$el).addClass('scroller-zoom-out-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-in-max');
|
|||
|
} else if (this._magnification === this._options.magnification) {
|
|||
|
$(this._$el).addClass('scroller-zoom-in-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-out-max');
|
|||
|
} else {
|
|||
|
$(this._$el).removeClass('scroller-zoom-in-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-out-max');
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
var ex = (typeof exports === 'undefined') ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImplScroller = MagnifierImplScroller;
|
|||
|
})();
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* Image magnifier
|
|||
|
* @class
|
|||
|
* @alias Akamai.Magnifier
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Magnifier = Akamai.Util.component("Magnifier", function(element, options){
|
|||
|
// compat with enlarge `disabled` option
|
|||
|
this._options.disabled = !this._options.enabled;
|
|||
|
if (this._options.mode === Magnifier.MODE_ANIMATED_ZOOM) {
|
|||
|
this._impl = new Akamai.MagnifierImplScroller(element, options);
|
|||
|
} else {
|
|||
|
this._impl = new Akamai.MagnifierImplEnlarge(element, options);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
Magnifier.prototype._updateOptions = function(options){
|
|||
|
if( !options ) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// compat with enlarge `disabled` option
|
|||
|
this._options.disabled = !this._options.enabled;
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Magnifier.defaultOptions, this._options, "Magnifier");
|
|||
|
|
|||
|
this._impl.updateOptions(this._options);
|
|||
|
};
|
|||
|
|
|||
|
// Used in preflight to "rename" events based on the child component events
|
|||
|
Magnifier._componentEventMapping = {
|
|||
|
"enlarge.after-zoom-in": "in",
|
|||
|
"enlarge.after-zoom-out": "out",
|
|||
|
"scroller.after-zoom-in": "in",
|
|||
|
"scroller.after-zoom-out": "out"
|
|||
|
};
|
|||
|
|
|||
|
Magnifier.MODE_HOVER_ZOOM = 'hoverzoom';
|
|||
|
Magnifier.MODE_ANIMATED_ZOOM = 'animatedzoom';
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Magnifiers on the page
|
|||
|
* @static
|
|||
|
* @property {String} mode - Set what mode to run the magnifier in. There are two modes available: `hoverzoom` and `animatedzoom`. The `hoverzoom` mode provides the ability to magnify and pan the image by just hovering your mouse over the image. It also provides a `flyout` option where a clip of the zoomed in image is displayed on a separate widget floating somewhere around (configurable) the image. The `animatedzoom` mode only does inline magnification but provides smooth animation between magnification levels, it also allows for incremental zooming.
|
|||
|
* @property {Boolean} button - Whether to show a button for toggling magnification (default: true)
|
|||
|
* @property {Boolean} enabled - Enabled/disable magnification (default: true, breakpoints supported)
|
|||
|
* @property {Integer} magnification - The scale factor to magnify the image: `2`, `3` (default), `4`, `4.5`, etc
|
|||
|
|
|||
|
* @property {Array} image.widths - List of available widths for an image (to be combined with image.widthParam) - (default: ["320","640","800","1024","2048","5000"])
|
|||
|
* @property {String} image.sizes - Value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw` (default), `200px`, `(min-width:1000px) 500px, 100vw` - (default: `100vw`, breakpoints supported)
|
|||
|
* @property {String} image.policy - Query param value for policy, appended to &policy= per image url when specified. Values: `foo`. (default: undefined)
|
|||
|
|
|||
|
* @property {Integer} delay - Only applicable in `hoverzoom` mode. The time delay in milliseconds between mouse hover and magnification (default: 300, breakpoints supported)
|
|||
|
* @property {String} buttonText - Only applicable in `hoverzoom` mode. Text for the zoom button. Also used for its title attribute. (default: "Toggle Image Magnification")
|
|||
|
* @property {Integer} flyout.width - Only applicable in `hoverzoom` mode. Width of the flyout image (default: 200)
|
|||
|
* @property {Integer} flyout.height - Only applicable in `hoverzoom` mode. Height of the flyout image (default: 200)
|
|||
|
* @property {Boolean} hoverZoomWithoutClick - Only applicable in `hoverzoom` mode. Zoom starts on mouse hover with no click needed (default: true; false will require a click to hover-zoom)
|
|||
|
* @property {String} placement - Only applicable in `hoverzoom` mode. Placement of the magnified image: `inline` , `flyoutloupe`, `flyouttopleft`,`flyoutbottomleft` ,`flyouttopright` and `flyoutbottomright` - (default: inline, breakpoins supported)
|
|||
|
|
|||
|
* @property {Float} incrementalZoomFactor - Only applicable in `animatedzoom` mode. A number by which to incrementally zoom up until the specified `magnification`, default `3`. For example a `magnification` of `4` and an `incrementalZoomFactor` of `2` will zoom the image in `2` steps
|
|||
|
* @property {Boolean} animateZoom - Only applicable in `animatedzoom` mode. Animates the magnification process. Default `true`
|
|||
|
* @property {Integer} animationDuration - Only applicable in `animatedzoom` mode. If `animateZoom` is true, this specifies the length of the animation in milliseconds. Default `250`
|
|||
|
* @property {String} zoomInBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom in button. Default `Zoom In`
|
|||
|
* @property {String} zoomOutBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom out button. Default `Zoom Out`
|
|||
|
*/
|
|||
|
Magnifier.defaultOptions = {
|
|||
|
// general options
|
|||
|
mode: Magnifier.MODE_HOVER_ZOOM,
|
|||
|
button: true,
|
|||
|
enabled: true,
|
|||
|
magnification: 3,
|
|||
|
|
|||
|
// image options
|
|||
|
image: Akamai.Image.defaultConfig,
|
|||
|
|
|||
|
// hoverzoom options
|
|||
|
delay: 300,
|
|||
|
buttonText: "Toggle Image Magnification",
|
|||
|
flyout: {
|
|||
|
width: 200,
|
|||
|
height: 200
|
|||
|
},
|
|||
|
hoverZoomWithoutClick: true,
|
|||
|
placement: "inline",
|
|||
|
|
|||
|
// animatedzoom options
|
|||
|
incrementalZoomFactor: 3,
|
|||
|
animateZoom: true,
|
|||
|
animationDuration: 250,
|
|||
|
zoomInBtnText: 'Zoom In',
|
|||
|
zoomOutBtnText: 'Zoom Out'
|
|||
|
};
|
|||
|
|
|||
|
// srcset stuff
|
|||
|
Akamai.Sourceable.extendStatic(Magnifier);
|
|||
|
|
|||
|
Magnifier.render = function(json, options){
|
|||
|
var finalOptions = Akamai.Util.options(Magnifier.defaultOptions, options);
|
|||
|
var dataAttr = Magnifier._dataAttr;
|
|||
|
var fallbackSrc = Magnifier._fallbackSrc(json.url, finalOptions.image);
|
|||
|
var largestSrc = Magnifier._largestSrc(json.url, finalOptions.image);
|
|||
|
var srcSet = Magnifier._srcset(json.url, finalOptions.image)
|
|||
|
var sizes = finalOptions.image.sizes;
|
|||
|
var impl = (finalOptions.mode === Magnifier.MODE_ANIMATED_ZOOM) ? Akamai.MagnifierImplScroller : Akamai.MagnifierImplEnlarge;
|
|||
|
return impl.render(finalOptions, dataAttr, fallbackSrc, largestSrc, srcSet, sizes);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Enter magnifier mode
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-in
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.in = function(){
|
|||
|
this._impl.zoomIn();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Exit magnifier mode
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-out
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.out = function(){
|
|||
|
this._impl.zoomOut();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the magnifier
|
|||
|
* @method
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
Magnifier.prototype.isMagnified = function(){
|
|||
|
return this._impl.isMagnified();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Toggle the state of the magnifier
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-in
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-out
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.toggle = function(){
|
|||
|
this._impl.toggleZoom();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the magnifier has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carouse#in}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-in
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the magnifier has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carouse#out}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-out
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Magnifier}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Magnifier = Magnifier;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
var VideoImpl = Akamai.Util.component("VideoImpl", function(element, options){
|
|||
|
throw new Error('Not allowed to instantiate VideoImpl');
|
|||
|
});
|
|||
|
|
|||
|
VideoImpl.render = function(json, options){
|
|||
|
throw new Error('VideoImpl.render not implemented');
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImpl = VideoImpl;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
var VideoImplPristine = Akamai.Util.component("VideoImplPristine", function(element, options){
|
|||
|
});
|
|||
|
|
|||
|
VideoImplPristine.prototype = Object.create(Akamai.VideoImpl.prototype);
|
|||
|
VideoImplPristine.prototype.constructor = VideoImplPristine;
|
|||
|
|
|||
|
VideoImplPristine.render = function(json, options){
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
|
|||
|
var url = json.url;
|
|||
|
var poster = json.poster || "";
|
|||
|
var mime = (json.mime) ? 'type="' + json.mime + '"' : '';
|
|||
|
|
|||
|
var attrs = [
|
|||
|
finalOptions.loop ? "loop" : "",
|
|||
|
finalOptions.autoplay ? "autoplay" : "",
|
|||
|
finalOptions.controls ? "controls" : "",
|
|||
|
finalOptions.muted ? "muted" : "",
|
|||
|
"playsinline"
|
|||
|
];
|
|||
|
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<div ${Akamai.Video._dataAttr}>
|
|||
|
<video ${attrs.join(" ")} poster="${poster}" preload="metadata">
|
|||
|
<source src="${url}" ${mime} />
|
|||
|
</video>
|
|||
|
</div>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImplPristine = VideoImplPristine;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
var VideoImplIm = Akamai.Util.component("VideoImplIm", function(element, options){
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
VideoImplIm._validateOptions(finalOptions);
|
|||
|
});
|
|||
|
|
|||
|
VideoImplIm.prototype = Object.create(Akamai.VideoImpl.prototype);
|
|||
|
VideoImplIm.prototype.constructor = VideoImplIm;
|
|||
|
|
|||
|
VideoImplIm._validateOptions = function(options){
|
|||
|
if (!Array.isArray(options.sizes) || options.sizes.length !== 3) {
|
|||
|
throw new Error('Akamai.Video sizes must have three entries');
|
|||
|
}
|
|||
|
|
|||
|
if (options.sizes.some(function (size) {
|
|||
|
return isNaN(size);
|
|||
|
})) {
|
|||
|
throw new Error('Akamai.VideoImplIm all sizes must be numbers');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
VideoImplIm.render = function(json, options){
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
VideoImplIm._validateOptions(finalOptions);
|
|||
|
|
|||
|
var url = json.url;
|
|||
|
var poster = json.poster || "";
|
|||
|
var width;
|
|||
|
|
|||
|
var viewPortWidth = window.innerWidth;
|
|||
|
|
|||
|
if (viewPortWidth < 992) {
|
|||
|
width = finalOptions.sizes[0];
|
|||
|
} else if (viewPortWidth < 1200) {
|
|||
|
width = finalOptions.sizes[1];
|
|||
|
} else {
|
|||
|
width = finalOptions.sizes[2];
|
|||
|
}
|
|||
|
|
|||
|
var attrs = [
|
|||
|
finalOptions.loop ? "loop" : "",
|
|||
|
finalOptions.autoplay ? "autoplay" : "",
|
|||
|
finalOptions.controls ? "controls" : "",
|
|||
|
finalOptions.muted ? "muted" : "",
|
|||
|
"playsinline"
|
|||
|
];
|
|||
|
|
|||
|
var joiner = url.indexOf('?') === -1 ? '?' : '&';
|
|||
|
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<div ${Akamai.Video._dataAttr}>
|
|||
|
<video ${attrs.join(" ")} poster="${poster}" preload="metadata">
|
|||
|
<source src="${url}${joiner}imformat=vp9&imwidth=${width}" type="video/webm" />
|
|||
|
<source src="${url}${joiner}imformat=h265&imwidth=${width}" type="video/mp4; codecs=hevc" />
|
|||
|
<source src="${url}${joiner}imformat=h264&imwidth=${width}" type="video/mp4" />
|
|||
|
</video>
|
|||
|
</div>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImplIm = VideoImplIm;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* Video component
|
|||
|
* @class
|
|||
|
* @alias Akamai.Video
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Video = Akamai.Util.component("Video", function(element, options){
|
|||
|
// TODO
|
|||
|
this._comp = this._$el.length && undefined;
|
|||
|
this._$videoElement = this._$el.find("video");
|
|||
|
this._videoElement = this._$videoElement[0];
|
|||
|
|
|||
|
if( !this._videoElement ){
|
|||
|
throw new Error( "Akamai.Video requires a child HTML Video element" );
|
|||
|
}
|
|||
|
|
|||
|
if( !this._videoElement.play || !this._videoElement.pause ){
|
|||
|
this._unsupportedAPI = true;
|
|||
|
this._unsupported();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// get the initial state (could be autoplaying on render)
|
|||
|
this._setState(this._videoElement.paused ? Video.States.Paused : Video.States.Playing);
|
|||
|
|
|||
|
// state bindings
|
|||
|
this._$el
|
|||
|
.bind("akamai-video-play", function(){
|
|||
|
this._setState(Video.States.Playing);
|
|||
|
}.bind(this))
|
|||
|
.bind("akamai-video-pause", function(){
|
|||
|
this._setState(Video.States.Paused);
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
this._createPlayButton();
|
|||
|
});
|
|||
|
|
|||
|
Video.MODE_IM = 'im';
|
|||
|
Video.MODE_PRISTINE = 'pristine';
|
|||
|
|
|||
|
Video._componentEventMapping = {
|
|||
|
"play": {
|
|||
|
to: "play",
|
|||
|
selector: "video"
|
|||
|
},
|
|||
|
|
|||
|
"pause": {
|
|||
|
to: "pause",
|
|||
|
selector: "video"
|
|||
|
},
|
|||
|
|
|||
|
"seeked": {
|
|||
|
to: "seek",
|
|||
|
selector: "video"
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._updateOptions = function(options){
|
|||
|
if( !options ) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Video.defaultOptions, this._options, "Video");
|
|||
|
|
|||
|
// TODO see Magnifier for example
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Spin360s on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} autoplay - Automatically play the video on load (default: false)
|
|||
|
* @property {Boolean} controls - Display the video controls (default: true)
|
|||
|
* @property {Boolean} loop - Restart the video when it reaches the end (default: false)
|
|||
|
* @property {Boolean} muted - Mute the video (default: true)
|
|||
|
* @property {String} mode - What video component to use. `im` will let you use any size pristine video and will automatically request & generate the right size when the page is loaded. `pristine` just passes through the original video into the video player and this is what will always play on the users device. Default: `pristine`
|
|||
|
* @property {Array} sizes - Video sizes (widths) to use for different screen widths. Defaults: [1920 (large screens), 1280 (tablet), 854 (mobile), ]
|
|||
|
*/
|
|||
|
Video.defaultOptions = {
|
|||
|
autoplay: false,
|
|||
|
controls: true,
|
|||
|
loop: false,
|
|||
|
muted: true,
|
|||
|
mode: Video.MODE_PRISTINE,
|
|||
|
sizes: [854, 1280, 1920]
|
|||
|
};
|
|||
|
|
|||
|
Video.render = function(json, options){
|
|||
|
var impl = (options && options.mode === Video.MODE_IM) ? Akamai.VideoImplIm : Akamai.VideoImplPristine;
|
|||
|
return impl.render(json, options);
|
|||
|
};
|
|||
|
|
|||
|
Video.States = {
|
|||
|
Playing: 0,
|
|||
|
Paused: 1
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._unsupported = function(){
|
|||
|
if( this._unsupportedAPI ){
|
|||
|
Akamai.Util.log("Video: video API not supported", 'error');
|
|||
|
}
|
|||
|
|
|||
|
return this._unsupportedAPI;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Play the video, idempotent
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-play
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.play = function(){
|
|||
|
if( this._unsupported() ) { return; }
|
|||
|
this._videoElement.play();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Pause the video
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-pause
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.pause = function(){
|
|||
|
if( this._unsupported() ) { return; }
|
|||
|
this._videoElement.pause();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the video
|
|||
|
* @example <caption>Video states</caption>
|
|||
|
* Akamai.Video.States = {
|
|||
|
* Playing: 0,
|
|||
|
* Paused: 1
|
|||
|
* };
|
|||
|
*
|
|||
|
* @example <caption>Conditioning on states</caption>
|
|||
|
* if( video.getState() === Akamai.Video.States.Paused ) {
|
|||
|
* video.play()
|
|||
|
* }
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Video.State}
|
|||
|
*/
|
|||
|
Video.prototype.getState = function(){
|
|||
|
return this._state;
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._setState = function(value){
|
|||
|
this._state = value;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Seek to the input percentage.
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-seek
|
|||
|
* @param {Integer} percent - value between 0 and 100 percent for seeking
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.seek = function(percent){
|
|||
|
if( percent < 0 || 100 < percent ) {
|
|||
|
throw new Error( "seek takes a an integer between 0 and 100" );
|
|||
|
}
|
|||
|
|
|||
|
var newTime = (percent / 100) * (this._videoElement.duration || 1);
|
|||
|
this._videoElement.currentTime = newTime;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Set whether the video should loop or not
|
|||
|
* @method
|
|||
|
* @param {Boolean} value - The value true or false
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.setLoop = function(value){
|
|||
|
this._videoElement.loop = value;
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype.getElement = function(){
|
|||
|
return this._$el[0];
|
|||
|
};
|
|||
|
|
|||
|
// Borrowed from https://codepen.io/chrisnager/pen/jPrJgQ
|
|||
|
Video.prototype._createPlayButton = function(){
|
|||
|
var videoPlayButton;
|
|||
|
var videoWrapper = this._$el[0];
|
|||
|
var video = this._videoElement;
|
|||
|
|
|||
|
if(this._options.autoplay){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// TODO move to render?
|
|||
|
videoWrapper.insertAdjacentHTML('beforeend', `
|
|||
|
<svg viewBox="0 0 200 200" alt="Play video">
|
|||
|
<circle cx="100" cy="100" r="90" fill="none" stroke-width="15" stroke="#fff"/>
|
|||
|
<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>
|
|||
|
</svg>
|
|||
|
`);
|
|||
|
|
|||
|
video.removeAttribute('controls');
|
|||
|
videoPlayButton = this._$el.find('svg')[0];
|
|||
|
|
|||
|
videoPlayButton.addEventListener('click', function(){
|
|||
|
video.play();
|
|||
|
videoPlayButton.classList.add('is-hidden');
|
|||
|
if( this._options.controls ) {
|
|||
|
video.setAttribute('controls', 'controls');
|
|||
|
}
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has been played. See {@link Akamai.Video#play}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-play
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has been paused. See {@link Akamai.Video#pause}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-pause
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has seeked to a position. See {@link Akamai.Video#seek}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-seek
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Video}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Video = Video;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* Image carousel
|
|||
|
* @class
|
|||
|
* @alias Akamai.Carousel
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Carousel = Akamai.Util.component("Carousel", function(element, options){
|
|||
|
this._spins = Akamai.Spin360.createMany(this._$el[0], this._options.spin360);
|
|||
|
this._magnifiers = Akamai.Magnifier.createMany(this._$el[0], this._options.magnifier);
|
|||
|
this._videos = Akamai.Video.createMany(this._$el[0], this._options.video);
|
|||
|
|
|||
|
this._$el.snapper(this._options);
|
|||
|
|
|||
|
this._setAspectRatio();
|
|||
|
this._bindAspectAttributes();
|
|||
|
|
|||
|
if( this._options.slideshow.autostart ){
|
|||
|
setTimeout(function(){
|
|||
|
this.startSlideshow();
|
|||
|
}.bind(this), this._options.slideshow.interval);
|
|||
|
}
|
|||
|
|
|||
|
// NOTE !! the following two bindings must happen in order, the
|
|||
|
// autoplayables binding relies on the attribut
|
|||
|
|
|||
|
// add item attrs and maintain the active item attributes to determine the
|
|||
|
// active item state
|
|||
|
this._activeItemAttributes();
|
|||
|
this._$el.bind( "akamai-carousel-goto", this._activeItemAttributes.bind(this));
|
|||
|
|
|||
|
// TODO pause autoplay videos that are not visible
|
|||
|
this._handleAutoplayables();
|
|||
|
this._$el.bind( "akamai-carousel-goto", this._handleAutoplayables.bind(this));
|
|||
|
|
|||
|
this._$el
|
|||
|
.bind( "tau.touch-tracking-start", function(){
|
|||
|
this._$el.find(".snapper_pane").addClass("no-scroll");
|
|||
|
}.bind(this))
|
|||
|
.bind( "tau.touch-tracking-stop", function(){
|
|||
|
this._$el.find(".snapper_pane").removeClass("no-scroll");
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
// when child components are doing things, stop the carousel from
|
|||
|
// automatically advancing
|
|||
|
this._$el.bind( this.constructor._stopSlideshowEvents.join(" "), this.stopSlideshow.bind(this));
|
|||
|
});
|
|||
|
|
|||
|
Carousel._stopSlideshowEvents = [
|
|||
|
"akamai-magnifier-in",
|
|||
|
"akamai-magnifier-out",
|
|||
|
"akamai-spin360-goto",
|
|||
|
"akamai-video-play",
|
|||
|
"akamai-video-seek"
|
|||
|
];
|
|||
|
|
|||
|
// clearly there should be an autoplayable interface
|
|||
|
Carousel.prototype._handleAutoplayables = function(){
|
|||
|
var isParentActive = function(comp){
|
|||
|
return !!$(comp.getElement())
|
|||
|
.closest("[" + this.constructor.activeItemAttr + "]")
|
|||
|
.length;
|
|||
|
}.bind(this);
|
|||
|
|
|||
|
this._videos.concat(this._spins).forEach(function(comp){
|
|||
|
if( isParentActive(comp) && comp._carouselWasPlaying ) {
|
|||
|
comp.play();
|
|||
|
} else {
|
|||
|
if( comp.getState() == comp.constructor.States.Playing ){
|
|||
|
comp._carouselWasPlaying = true;
|
|||
|
comp.pause();
|
|||
|
} else {
|
|||
|
comp._carouselWasPlaying = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
// Unique counter for IDs
|
|||
|
Carousel.counter = 0;
|
|||
|
|
|||
|
Carousel.prototype._updateOptions = function(options){
|
|||
|
if( !options ) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
// update the current options
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// update the options for each of the subcomponents
|
|||
|
var update = function(comp, name){
|
|||
|
comp._updateOptions(this._options[name]);
|
|||
|
}.bind(this);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Carousel.defaultOptions, this._options, "Carousel");
|
|||
|
this._$el.snapper( "updateOptions", options );
|
|||
|
|
|||
|
this._spins.forEach(function(c){ update(c, "spin360"); });
|
|||
|
this._magnifiers.forEach(function(c){ update(c, "magnifier"); });
|
|||
|
this._videos.forEach(function(c){ update(c, "video"); });
|
|||
|
|
|||
|
this._setAspectRatio();
|
|||
|
};
|
|||
|
|
|||
|
// Used in preflight to "rename" events based on the child component events
|
|||
|
Carousel._componentEventMapping = {
|
|||
|
"snapper.after-snap": "goto",
|
|||
|
"snapper.snap": "snap",
|
|||
|
"snapper.after-next": "next",
|
|||
|
"snapper.after-prev": "previous"
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Carousels on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} arrows - Show carousel controls - (default: true)
|
|||
|
* @property {Number} aspectratio - Specify a percentage-based height for the carousel, relative to the width. Values: `false`, `100`, `45.6`, `78` - (default `false`, breakpoints supported)
|
|||
|
* @property {Integer} slideshow.interval - Time in milliseconds between slide advances - (default: 4000, breakpoints supported)
|
|||
|
* @property {Integer} slideshow.autostart - Start the slideshow on instantiation - (default: false)
|
|||
|
* @property {String} thumbnail.placement - Placement relative to the slide container: `left`, `right`, `bottom` (default: `bottom`, breakpoints supported)
|
|||
|
* @property {String} thumbnail.type - Type of thumbnail: `dots`, `none`, `images` - (default: `images`, breakpoints supported)
|
|||
|
* @property {String} thumbnail.policy - String to use for query parameter, ex: "&policy=" (default: undefined)
|
|||
|
* @property {String} thumbnail.sizes - Sizes attribute value to use if thumbnail policy is set. Values: `300px`, `200px`, `(min-width:1000px) 300px, 100px` - (default: `300px`)
|
|||
|
* @property {Object} images - Defaults to the Magnifier image option's settings
|
|||
|
*/
|
|||
|
Carousel.defaultOptions = {
|
|||
|
arrows: true,
|
|||
|
|
|||
|
aspectratio: false,
|
|||
|
|
|||
|
slideshow: {
|
|||
|
interval: 4000,
|
|||
|
autostart: false
|
|||
|
},
|
|||
|
|
|||
|
thumbnail: {
|
|||
|
placement: "bottom",
|
|||
|
type: "images",
|
|||
|
policy: undefined,
|
|||
|
sizes: "300px"
|
|||
|
},
|
|||
|
|
|||
|
images: Akamai.Image.defaultConfig
|
|||
|
};
|
|||
|
|
|||
|
Carousel.renderMapping = {
|
|||
|
image: Akamai.Magnifier,
|
|||
|
spin360: Akamai.Spin360,
|
|||
|
video: Akamai.Video
|
|||
|
};
|
|||
|
|
|||
|
Carousel._typeMapping = {
|
|||
|
image: "magnifier"
|
|||
|
};
|
|||
|
|
|||
|
Carousel._uniqueItemId = function(item, i) {
|
|||
|
return "akamai-carousel-" + Carousel.counter + "-" + item.type + "-" + i;
|
|||
|
};
|
|||
|
|
|||
|
// srcset stuff
|
|||
|
Akamai.Sourceable.extendStatic(Carousel);
|
|||
|
|
|||
|
Carousel._renderItem = function(item, i, options) {
|
|||
|
var mappedType = Carousel._typeMapping[item.type] || item.type;
|
|||
|
|
|||
|
if( !Carousel.renderMapping[item.type] ){
|
|||
|
throw new Error(`
|
|||
|
item type ${item.type} at index ${i} must be 'image', 'spin360', or 'video'
|
|||
|
`);
|
|||
|
}
|
|||
|
|
|||
|
return `
|
|||
|
<div class="snapper_item" id="${Carousel._uniqueItemId(item, i)}">
|
|||
|
${Carousel.renderMapping[item.type].render(item, options[mappedType])}
|
|||
|
</div>
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
Carousel._renderThumbnailAnchor = function(item, data, i, options) {
|
|||
|
var thumbUrl = "";
|
|||
|
var altText = item.alt || "";
|
|||
|
|
|||
|
// use the video poster, the canonical url, or the first in a sequence
|
|||
|
if( item.type === 'video' ){
|
|||
|
thumbUrl = item.poster || data.reduce(function(acc, i){
|
|||
|
return acc || Carousel._thumbUrl(i);
|
|||
|
}, "");
|
|||
|
} else {
|
|||
|
thumbUrl = Carousel._thumbUrl(item);
|
|||
|
}
|
|||
|
|
|||
|
// TODO this sucks
|
|||
|
options.images.policy = options.thumbnail.policy;
|
|||
|
|
|||
|
var src = Carousel._fallbackSrc( thumbUrl, options.images );
|
|||
|
var srcset = Carousel._srcset( thumbUrl, options.images );
|
|||
|
var sizes = options.images.sizes;
|
|||
|
|
|||
|
// if there's a thumbnail policy, the thumbnails will be fresh image requests,
|
|||
|
// so they should have better sizes attribute values
|
|||
|
if( options.thumbnail.policy ){
|
|||
|
sizes = options.thumbnail.sizes;
|
|||
|
}
|
|||
|
|
|||
|
var attrs = [
|
|||
|
`${Carousel._dataAttr}-thumb-type="${item.type}"`,
|
|||
|
item.type == "video" && !item.poster ? "${Carousel._dataAttr}-thumb-noposter" : ""
|
|||
|
];
|
|||
|
|
|||
|
return `
|
|||
|
<a href="#${Carousel._uniqueItemId(item, i)}" ${attrs.join(" ")}>
|
|||
|
<img src="${src}" srcset="${srcset}" sizes="${sizes}" alt="${altText}" title="Scroll to ${item.type} ${i}" />
|
|||
|
</a>
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
Carousel._thumbUrl = function(item){
|
|||
|
return item.type === 'video' ? item.poster : item.url || (item.urls && item.urls[0]);
|
|||
|
};
|
|||
|
|
|||
|
Carousel._renderThumbnails = function(data, options) {
|
|||
|
if(data.length <= 1 ){
|
|||
|
return "";
|
|||
|
};
|
|||
|
|
|||
|
return `
|
|||
|
<div class="snapper_nav">
|
|||
|
<div class="snapper_nav_inner">
|
|||
|
${Akamai.Util.map(data, function(item, i){
|
|||
|
return Carousel._renderThumbnailAnchor(item, data, i, options);
|
|||
|
}).join("\n")}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._setAspectAttributes = function( $el ){
|
|||
|
var orientationAttr = `${Carousel._dataAttr}-aspectratio-orientation`;
|
|||
|
if( $el.height() > $el.width() ) {
|
|||
|
$el.attr( orientationAttr, "portrait" );
|
|||
|
} else if( $el.height() < $el.width() ) {
|
|||
|
$el.attr( orientationAttr, "landscape" );
|
|||
|
} else {
|
|||
|
if ($el.parent().height() >= $el.parent().width()) {
|
|||
|
$el.attr( orientationAttr, "landscape" );
|
|||
|
} else {
|
|||
|
$el.attr( orientationAttr, "portrait" );
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._setAspectRatio = function(){
|
|||
|
var value = this._options.aspectratio || 0;
|
|||
|
var $items = this._$el.find( ".snapper_item" );
|
|||
|
$items.css( "padding-top", (value / $items.length) + "%" );
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._bindAspectAttributes = function(){
|
|||
|
if( this._options.aspectratio === false ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var value = this._options.aspectratio || 0;
|
|||
|
var self = this;
|
|||
|
var $items = this._$el.find( ".snapper_item" );
|
|||
|
|
|||
|
$items.each(function(){
|
|||
|
var loadBound;
|
|||
|
var $item = $(this);
|
|||
|
|
|||
|
var $loadable = $item.find( "img, video" ).eq(0);
|
|||
|
|
|||
|
$loadable.bind("load loadedmetadata", loadBinding = function(){
|
|||
|
clearTimeout(loadBound);
|
|||
|
|
|||
|
if( $item.is(`[${Akamai.Spin360._dataAttr}]`) ){
|
|||
|
// TODO it's not always a canvas, sometimes it's a collection of
|
|||
|
// images based on settings
|
|||
|
self._setAspectAttributes( $item.find( "canvas" ) );
|
|||
|
} else{
|
|||
|
self._setAspectAttributes( $loadable );
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
loadBound = setTimeout(loadBinding, 5000);
|
|||
|
});
|
|||
|
|
|||
|
$items.eq(0).find( "img,video" ).eq(0).bind( "load loadedmetadata", function(){
|
|||
|
self._trigger( "first-media-load" );
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
Carousel.render = function(data, options){
|
|||
|
options = options || {};
|
|||
|
|
|||
|
// establish the extended default options for the carousel
|
|||
|
var carouselOptions = Akamai.Util.options(Carousel.defaultOptions, options);
|
|||
|
|
|||
|
var templateAttrs = [
|
|||
|
`${Carousel._dataAttr}`,
|
|||
|
`${Carousel._dataAttr}-item-count="${(data || []).length}"`,
|
|||
|
`data-snapper-deeplinking="false"`,
|
|||
|
carouselOptions.arrows ? "data-snapper-nextprev" : ""
|
|||
|
];
|
|||
|
|
|||
|
Carousel.counter++;
|
|||
|
|
|||
|
// TODO can we do something about the `enlarge_pane` class here, seems it
|
|||
|
// could be conditioned on at least one regular image type in the json
|
|||
|
// TODO options should dictate `data-snapper-nextprev`
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<div ${templateAttrs.join(" ")} class="snapper">
|
|||
|
<div class="snapper_nextprev_contain">
|
|||
|
<div class="snapper_pane_crop">
|
|||
|
<div class="snapper_pane enlarge_pane">
|
|||
|
<div class="snapper_items">
|
|||
|
${Akamai.Util.map(data, function(item, i){
|
|||
|
return Carousel._renderItem(item, i, carouselOptions);
|
|||
|
}).join("\n")}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
${Carousel._renderThumbnails(data, carouselOptions)}
|
|||
|
</div>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Instantiate Carousels by looking for children matching
|
|||
|
* [data-akamai-carousel] in `element` param
|
|||
|
* @method
|
|||
|
* @static
|
|||
|
* @param {HTMLElement} element - the element to search in for
|
|||
|
* @returns {Akamai.Carousel[]}
|
|||
|
*/
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Spin360[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getSpin360s = function(){
|
|||
|
return this._spins;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Magnifier[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getMagnifiers = function(){
|
|||
|
return this._magnifiers;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Video[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getVideos = function(){
|
|||
|
return this._videos;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Go to a particular slide.
|
|||
|
*
|
|||
|
* Note that the state of the DOM on the page and thus the index of the
|
|||
|
* carousel will not be up-to-date until the {@link
|
|||
|
* Akamai.Carousel#akamai-carousel-goto} event has been fired. That is,
|
|||
|
* calling this method and the calling `getIndex` will not necessarily result
|
|||
|
* in an updated index value. Either bind to the event or supply a callback.
|
|||
|
*
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-goto
|
|||
|
* @param {Integer} index - The zero-based slide index to go to
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.goto = function(index, callback){
|
|||
|
this._$el.snapper( "goto", index, callback );
|
|||
|
};
|
|||
|
|
|||
|
Carousel._itemAttr = `${Carousel._dataAttr}-item`;
|
|||
|
Carousel._activeItemAttr = `${Carousel._itemAttr}-active`;
|
|||
|
|
|||
|
/**
|
|||
|
* Add an attribute to all carousel items,
|
|||
|
* and additionally maintain an active attribute on the active carousel item
|
|||
|
*
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype._activeItemAttributes = function(){
|
|||
|
var attrAllItems = this.constructor._itemAttr;
|
|||
|
var attrActiveItem = this.constructor._activeItemAttr;
|
|||
|
|
|||
|
this._$el
|
|||
|
.find( ".snapper_item" )
|
|||
|
.attr( attrAllItems, true )
|
|||
|
.removeAttr( attrActiveItem )
|
|||
|
.eq( this.getIndex() ).attr( attrActiveItem, true );
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current slide index
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.getIndex = function(){
|
|||
|
return this._$el.snapper( "getIndex" );
|
|||
|
};
|
|||
|
|
|||
|
// Extend Carousel with Advanceabl interface
|
|||
|
Akamai.Advanceable.extend(Carousel);
|
|||
|
|
|||
|
/**
|
|||
|
* Advance to the next item
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-next
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.next;
|
|||
|
// required for JSDocs, implementation in Advanceable
|
|||
|
|
|||
|
/**
|
|||
|
* Retreat to the previous item
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-previous
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.previous;
|
|||
|
// required for JSDocs, implementation in Advanceable
|
|||
|
|
|||
|
/**
|
|||
|
* Start automatic advancement of the carousel items
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-start-slideshow
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.startSlideshow = function(){
|
|||
|
this._$el.one("mousedown touchstart", this.stopSlideshow.bind(this));
|
|||
|
|
|||
|
this.next(function(){
|
|||
|
this._trigger("start-slideshow");
|
|||
|
this._slideshowTimer = setTimeout(function(){
|
|||
|
this.startSlideshow();
|
|||
|
|
|||
|
// TODO remove || when default options are added
|
|||
|
}.bind(this), this._options.slideshow.interval );
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Start automatic advancement of the carousel items
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-stop-slideshow
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.stopSlideshow = function(){
|
|||
|
clearTimeout(this._slideshowTimer);
|
|||
|
this._slideshowTimer = undefined;
|
|||
|
this._trigger("stop-slideshow");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#goto}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-goto
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#next}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-next
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#previous}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-previous
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has started the slide show due to a call to
|
|||
|
* {@link Akamai.Carousel#startSlideshow}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-start-slideshow
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has stoped the slide show due to a call to
|
|||
|
* {@link Akamai.Carousel#stopSlideshow}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-stop-slideshow
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Carousel}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-init
|
|||
|
*/
|
|||
|
|
|||
|
Carousel.prototype.refresh = function(){
|
|||
|
this._$el.snapper("updateWidths");
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Carousel = Carousel;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* Image Fullscreen
|
|||
|
* @class
|
|||
|
* @alias Akamai.Fullscreen
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
|
|||
|
// TODO JSDocs
|
|||
|
// TODO Tests
|
|||
|
var Fullscreen = Akamai.Util.component("Fullscreen", function(element, options){
|
|||
|
if( this._options.enabled ) {
|
|||
|
this._init();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
Fullscreen.prototype._init = function(){
|
|||
|
this._$fullscreen = this._$el;
|
|||
|
|
|||
|
// parent container does not allow widths to be set (Firefox, fullscreen), so we’ll adjust the children
|
|||
|
this.addButton();
|
|||
|
|
|||
|
this._fullscreenApiKeys = Fullscreen._keys();
|
|||
|
|
|||
|
if( this._options.native ) {
|
|||
|
this._useNativeApi = !!this._fullscreenApiKeys;
|
|||
|
} else {
|
|||
|
this._useNativeApi = false;
|
|||
|
}
|
|||
|
|
|||
|
// Fullscreen API is disabled and the fallback behavior is active
|
|||
|
this._fullscreenFallbackEnabled = false;
|
|||
|
this._$placeholder = $( "<div>" );
|
|||
|
|
|||
|
this._isFullscreen = false;
|
|||
|
this.addEvents();
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.classes = {
|
|||
|
btn: "akamai-fullscreen-btn",
|
|||
|
btnContainer: "akamai-fullscreen-btncontainer",
|
|||
|
enterBtn: "akamai-fullscreen-btn-enter",
|
|||
|
exitBtn: "akamai-fullscreen-btn-exit",
|
|||
|
active: "akamai-fullscreen-active",
|
|||
|
fallback: "akamai-fullscreen-fallback"
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.attr = {
|
|||
|
width: "data-akamai-fullscreen-width",
|
|||
|
enterBtn: "data-akamai-fullscreen-btn",
|
|||
|
exitBtn: "data-akamai-fullscreen-exit-btn"
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.defaultOptions = {
|
|||
|
enabled: false,
|
|||
|
native: false
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype.addButton = function(){
|
|||
|
this._$widthAdjust = this._$fullscreen.children().filter(".focused");
|
|||
|
|
|||
|
if( !this._$widthAdjust.length ) {
|
|||
|
this._$widthAdjust = this._$fullscreen
|
|||
|
.children()
|
|||
|
.eq(0);
|
|||
|
}
|
|||
|
|
|||
|
if( this._$widthAdjust.attr(Fullscreen.attr.width) === null ){
|
|||
|
this._$widthAdjust.attr( Fullscreen.attr.width, "" );
|
|||
|
}
|
|||
|
|
|||
|
if( this._$widthAdjust.find("[" + Fullscreen.attr.enterBtn + "]").length ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// buttons parent
|
|||
|
this._$buttonContainer = this._$widthAdjust;
|
|||
|
this._$buttonContainer.addClass( Fullscreen.classes.btnContainer );
|
|||
|
|
|||
|
this._$buttonContainer.append(this.render());
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype.addEvents = function() {
|
|||
|
this._$fullscreen.on( "click", function( e ) {
|
|||
|
var $target = $( e.target );
|
|||
|
if( !$target.is("[" + Fullscreen.attr.enterBtn + "]") ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.enter();
|
|||
|
e.preventDefault();
|
|||
|
}.bind( this ));
|
|||
|
|
|||
|
this._$fullscreen.on( "click", function( e ) {
|
|||
|
var $target = $( e.target );
|
|||
|
|
|||
|
if( !$target.is( "[" + Fullscreen.attr.exitBtn + "]" ) ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.exit();
|
|||
|
e.preventDefault();
|
|||
|
}.bind( this ));
|
|||
|
|
|||
|
if( this._useNativeApi ) {
|
|||
|
document.addEventListener( this._fullscreenApiKeys.onchange, function() {
|
|||
|
// exiting fullscreen using native method (ESC or menu option)
|
|||
|
if( !document[ this._fullscreenApiKeys.element ] ) {
|
|||
|
this._exit();
|
|||
|
}
|
|||
|
}.bind( this ), false );
|
|||
|
}
|
|||
|
|
|||
|
// ESC to close
|
|||
|
$( document ).on( "keydown", function( e ) {
|
|||
|
var code = e.keyCode || e.which;
|
|||
|
if( code === 27 ) {
|
|||
|
this._exit();
|
|||
|
}
|
|||
|
}.bind( this ));
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype.render = function() {
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<button ${Fullscreen.attr.enterBtn} class="${Fullscreen.classes.btn} ${Fullscreen.classes.enterBtn} icon-fullscreen">Full Screen</button>
|
|||
|
<button ${Fullscreen.attr.exitBtn} class="${Fullscreen.classes.btn} ${Fullscreen.classes.exitBtn} icon-close-light">Exit Full Screen</button>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen._keyLookup = [
|
|||
|
{
|
|||
|
enter: "requestFullscreen",
|
|||
|
exit: "exitFullscreen",
|
|||
|
element: "fullscreenElement",
|
|||
|
onchange: "fullscreenchange"
|
|||
|
},
|
|||
|
{
|
|||
|
enter: "webkitRequestFullscreen",
|
|||
|
exit: "webkitExitFullscreen",
|
|||
|
element: "webkitFullscreenElement",
|
|||
|
onchange: "webkitfullscreenchange"
|
|||
|
},
|
|||
|
{
|
|||
|
enter: "webkitRequestFullScreen",
|
|||
|
exit: "webkitCancelFullScreen",
|
|||
|
element: "webkitCurrentFullScreenElement",
|
|||
|
onchange: "webkitfullscreenchange"
|
|||
|
},
|
|||
|
{
|
|||
|
enter: "mozRequestFullScreen",
|
|||
|
exit: "mozCancelFullScreen",
|
|||
|
element: "mozFullScreenElement",
|
|||
|
onchange: "mozfullscreenchange"
|
|||
|
},
|
|||
|
{
|
|||
|
enter: "msRequestFullscreen",
|
|||
|
exit: "msExitFullscreen",
|
|||
|
element: "msFullscreenElement",
|
|||
|
onchange: "MSFullscreenChange"
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
Fullscreen._keys = function( el ) {
|
|||
|
var keys = Fullscreen._keyLookup;
|
|||
|
|
|||
|
var el = document.body;
|
|||
|
for( var j = 0, k = keys.length; j < k; j++ ) {
|
|||
|
if( keys[ j ].enter in el ) {
|
|||
|
return keys[ j ];
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._maximizePlacement = function(){
|
|||
|
this._fullscreenFallbackEnabled = true;
|
|||
|
this._$fullscreen.addClass( Fullscreen.classes.fallback );
|
|||
|
this._$placeholder.insertAfter( this._$fullscreen );
|
|||
|
this._$fullscreen.appendTo( document.body );
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._restorePlacement = function(){
|
|||
|
if( !this._fullscreenFallbackEnabled ) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this._fullscreenFallbackEnabled = false;
|
|||
|
this._$fullscreen.removeClass( Fullscreen.classes.fallback );
|
|||
|
this._$fullscreen.insertAfter( this._$placeholder );
|
|||
|
this._$placeholder.remove();
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._adjustWidth = function() {
|
|||
|
this._$fullscreen.css( "width", "auto !important" );
|
|||
|
// wish this could go into the change event above, but alas the dimensions don’t 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% (won’t fill the entire height, but that’s ok)
|
|||
|
if( widthBig > viewportWidth ) {
|
|||
|
widthBig = "100%";
|
|||
|
}
|
|||
|
|
|||
|
this._$widthAdjust.width( widthBig );
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._revertWidth = function() {
|
|||
|
this._$widthAdjust.css( "width", "" );
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Enter full screen mode
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Fullscreen.prototype.enter = function(){
|
|||
|
var activeIndex = this._$fullscreen[0].shoestringData.Viewer._carousels[0].getIndex();
|
|||
|
this._$fullscreen.addClass( Fullscreen.classes.active );
|
|||
|
|
|||
|
|
|||
|
if( this._useNativeApi ) {
|
|||
|
this._$fullscreen[ 0 ][ this._fullscreenApiKeys.enter ]();
|
|||
|
this._adjustWidth();
|
|||
|
} else {
|
|||
|
this._adjustWidth();
|
|||
|
this._maximizePlacement();
|
|||
|
}
|
|||
|
this._isFullscreen = true;
|
|||
|
this._$fullscreen[0].shoestringData.Viewer._carousels[0].goto(activeIndex);
|
|||
|
this._trigger("enter");
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._exit = function(){
|
|||
|
var activeIndex = this._$fullscreen[0].shoestringData.Viewer._carousels[0].getIndex();
|
|||
|
this._$fullscreen.removeClass( Fullscreen.classes.active );
|
|||
|
|
|||
|
if( !this._useNativeApi ) {
|
|||
|
this._restorePlacement();
|
|||
|
}
|
|||
|
|
|||
|
this._revertWidth();
|
|||
|
this._isFullscreen = false;
|
|||
|
this._$fullscreen[0].shoestringData.Viewer._carousels[0].goto(activeIndex);
|
|||
|
this._trigger("exit");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Exits full screen mode
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Fullscreen.prototype.exit = function(){
|
|||
|
if( this._useNativeApi ) {
|
|||
|
// note some close behavior happens in the onchange event handler above
|
|||
|
document[ this._fullscreenApiKeys.exit ]();
|
|||
|
}
|
|||
|
|
|||
|
this._exit();
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype.isFullscreen = function(){
|
|||
|
return this._isFullscreen;
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Fullscreen = Fullscreen;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
/**
|
|||
|
* Akamai Viewer omni-component
|
|||
|
* @class
|
|||
|
* @alias Akamai.Viewer
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*
|
|||
|
* @example <caption>Instatiation</caption>
|
|||
|
* var element = document.querySelector( "[data-akamai-viewer]" );
|
|||
|
* var viewer = Akamai.Viewer( element );
|
|||
|
*/
|
|||
|
var Viewer = Akamai.Util.component("Viewer", true, function(element, options){
|
|||
|
// try to update the carousel and magnifier sizes options to the width of
|
|||
|
// the viewer for more accurate srcset selection
|
|||
|
this._updateSizesOptions(true);
|
|||
|
this._onResizeComplete(this._updateSizesOptions.bind(this));
|
|||
|
|
|||
|
// move all the flattened component options (spin, zoom, video) onto the carousel
|
|||
|
this._options.carousel =
|
|||
|
Viewer._extendCarouselOptions(this._options);
|
|||
|
|
|||
|
this._withData(function(data){
|
|||
|
if( data ) {
|
|||
|
// check all of the urls agains the hostname and whitelist
|
|||
|
this._checkJSONUrls(data);
|
|||
|
|
|||
|
// prevent XSS / injection attacks by escaping string values
|
|||
|
data = Akamai.Util.escapeJSONVals(data);
|
|||
|
|
|||
|
// store escaped data for later reference
|
|||
|
this._options.items.data = data;
|
|||
|
}
|
|||
|
|
|||
|
this._tagMapping = {};
|
|||
|
|
|||
|
// TODO if the element is empty and/or options has `items` render into element
|
|||
|
if( this._$el.children().length == 0 && data ) {
|
|||
|
this._tagMapping = this.constructor._tagSplit(data, this._options.items);
|
|||
|
this._$el.append(this.constructor.render(data, this._options, true));
|
|||
|
}
|
|||
|
|
|||
|
// intantiate all child carousels
|
|||
|
this._carousels = Akamai.Carousel.createMany(element, this._options.carousel);
|
|||
|
this._fullscreen = new Akamai.Fullscreen(element, this._options.fullscreen);
|
|||
|
|
|||
|
// on enter and exit of fullscreen resolve the breakpoints and update options
|
|||
|
this._$el.bind( "akamai-fullscreen-enter akamai-fullscreen-exit", function(){
|
|||
|
this._updateOptions(this._resolveBreakpointOptions());
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
// use the breakpoints to set match media listeners
|
|||
|
this._setBreakpoints();
|
|||
|
|
|||
|
// have to trigger init explicitly for async constructors
|
|||
|
this._trigger("init");
|
|||
|
}.bind(this), function(msg){
|
|||
|
throw new Error(msg);
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
Viewer.prototype._updateSizesOptions = function(localUpdateOnly){
|
|||
|
// update sizes option to something more specific if possible
|
|||
|
var elWidth = this._$el.width();
|
|||
|
if( elWidth && elWidth > 0 ){
|
|||
|
elWidth += "px";
|
|||
|
this._options = Akamai.Util.extend(true, this._options, {
|
|||
|
carousel: {
|
|||
|
images: {
|
|||
|
sizes: elWidth
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
if( !localUpdateOnly ){
|
|||
|
this._updateOptions(this._options);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._withData = function(after, fail){
|
|||
|
if(!this._options.items.uri){
|
|||
|
if( this._exceedsLimit(this._options.items.data, fail)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
after(this._options.items.data);
|
|||
|
}
|
|||
|
|
|||
|
else if (typeof(this._options.items.uri) !== 'undefined' && this._options.items.uri.length > 0)
|
|||
|
{
|
|||
|
$.get(this._options.items.uri, function(data){
|
|||
|
// make sure the data gets sorted out regardless of `get` impl
|
|||
|
data = (typeof data === "string" ? JSON.parse(data) : data);
|
|||
|
|
|||
|
if( this._exceedsLimit(data.items, fail) ){
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
after(data.items);
|
|||
|
}.bind(this));
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// value in kibibytes
|
|||
|
Viewer.prototype._exceedsLimit = function(data, fail){
|
|||
|
// TODO some tests don't provide data
|
|||
|
if( !data ) { return false; }
|
|||
|
|
|||
|
// 1 unicode character = 4 bytes
|
|||
|
// length = # of chars
|
|||
|
// kibibyte = 1024 bytes
|
|||
|
// # of chars * 4 / 1024
|
|||
|
var kb = JSON.stringify(data).length * 4 / 1024;
|
|||
|
|
|||
|
if( kb > this._options.items.limit ) {
|
|||
|
fail = fail || function(){};
|
|||
|
fail(`JSON data size exceeds ${this._options.items.limit} KiB. Limit can be configured with items.limit Akamai.Viewer options.`);
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
return false;
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._setBreakpoints = function(){
|
|||
|
this._breakpoints = this._reduceBreakpoints(this._options.breakpoints || {});
|
|||
|
this._fullscreenBreakpoints =
|
|||
|
this._reduceBreakpoints(this._options.fullscreenBreakpoints || {});
|
|||
|
|
|||
|
this._bindBreakpoints();
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._reduceBreakpoints = function(breakpoints){
|
|||
|
var bps = [];
|
|||
|
|
|||
|
for( var bp in breakpoints ){
|
|||
|
if(breakpoints.hasOwnProperty(bp)){
|
|||
|
try {
|
|||
|
bps.push(parseInt(bp, 10));
|
|||
|
} catch(e){
|
|||
|
Akamai.Util.log(e, 'error');
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
bps.sort(function(a, b){ return b < a;});
|
|||
|
|
|||
|
return bps;
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._bindBreakpoints = function(){
|
|||
|
[this._breakpoints, this._fullscreenBreakpoints].forEach(function(bps){
|
|||
|
// bind using the minwidth and the next breakpoint as the maxwidth
|
|||
|
// adds a range from 0 to the first breakpoint and from the last
|
|||
|
// breakpoint to a very large number
|
|||
|
if(!bps.length) { return; }
|
|||
|
|
|||
|
for( var i = -1; i < bps.length; i++ ){
|
|||
|
this._bindMatchMedia(bps[i], bps[i+1]);
|
|||
|
}
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
// bind a callback to run after resize completes
|
|||
|
Viewer.prototype._onResizeComplete = function( callback ){
|
|||
|
var cbtimer;
|
|||
|
var self = this;
|
|||
|
window.addEventListener( "resize", function(){
|
|||
|
clearTimeout( cbtimer );
|
|||
|
cbtimer = setTimeout( callback, 500 );
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._bindMatchMedia = function(minWidth, maxWidth){
|
|||
|
minWidth = minWidth || 0;
|
|||
|
|
|||
|
// large number so we can use one media query template
|
|||
|
maxWidth = maxWidth || 1000000000;
|
|||
|
|
|||
|
if( window.matchMedia ){
|
|||
|
// bind using the minwidth and maxwidth so we get the events at the boundaries
|
|||
|
// so we can asses which set of options applies.
|
|||
|
var query = `(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`;
|
|||
|
|
|||
|
// create a media list to bind to
|
|||
|
var initList = window.matchMedia(query);
|
|||
|
|
|||
|
if(initList && initList.addListener) {
|
|||
|
// handle a match on instantiation
|
|||
|
this._mediaMatch(initList, minWidth, true);
|
|||
|
|
|||
|
// bind for later changes in whether the media query matches
|
|||
|
initList.addListener(function(list){
|
|||
|
this._mediaMatch(list, minWidth);
|
|||
|
}.bind(this));
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._mediaMatch = function(list, minWidth, ignoreDefault){
|
|||
|
// If there is a match for the breakpoint (we're in the bp range)
|
|||
|
// Then set the options based on that breakpoint
|
|||
|
// Else if there is not a match and the client width is below the
|
|||
|
// breakpoint that's being disabled, then use the original options
|
|||
|
if(list.matches){
|
|||
|
var resolvedOptions = this._resolveBreakpointOptions(minWidth);
|
|||
|
this._updateOptions(resolvedOptions);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// TODO a ton of duplication with bindMatchmedia
|
|||
|
Viewer.prototype._getCurrentMinWidth = function(bps){
|
|||
|
// bind using the minwidth and the next breakpoint as the maxwidth
|
|||
|
// adds a range from 0 to the first breakpoint and from the last
|
|||
|
// breakpoint to a very large number
|
|||
|
for( var i = -1; i < bps.length; i++ ){
|
|||
|
minWidth = bps[i] || 0;
|
|||
|
maxWidth = bps[i+1] || 100000000;
|
|||
|
|
|||
|
if( window.matchMedia ){
|
|||
|
// bind using the minwidth and maxwidth so we get the events at the boundaries
|
|||
|
// so we can asses which set of options applies.
|
|||
|
var query = `(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`;
|
|||
|
|
|||
|
// create a media list to bind to
|
|||
|
var initList = window.matchMedia(query);
|
|||
|
|
|||
|
if(initList && initList.matches) {
|
|||
|
return minWidth;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return false;
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._resolveBreakpointOptions = function(minWidth){
|
|||
|
// TODO bind on the reduce callback was not working
|
|||
|
var self = this;
|
|||
|
var bps, bpConfig;
|
|||
|
|
|||
|
if( this._fullscreen.isFullscreen() ) {
|
|||
|
bps = this._fullscreenBreakpoints;
|
|||
|
bpConfig = self._options.fullscreenBreakpoints;
|
|||
|
} else {
|
|||
|
bps = this._breakpoints;
|
|||
|
bpConfig = self._options.breakpoints;
|
|||
|
}
|
|||
|
|
|||
|
if(!minWidth){
|
|||
|
minWidth = self._getCurrentMinWidth(bps);
|
|||
|
}
|
|||
|
|
|||
|
// There may be no matching breakpoints when this method is called to
|
|||
|
// resolve the current options. If that's the case then we need to default
|
|||
|
// to the top level options
|
|||
|
if(!bps.length){ return self._originalOptions; }
|
|||
|
|
|||
|
return bps.reduce(function(acc, bp){
|
|||
|
var cloned = Akamai.Util.extend(true, {}, acc);
|
|||
|
var ptions;
|
|||
|
|
|||
|
|
|||
|
if(bp <= minWidth){
|
|||
|
cloned = Akamai.Util.extend(true, cloned, bpConfig[bp]);
|
|||
|
}
|
|||
|
|
|||
|
return cloned;
|
|||
|
}, Akamai.Util.extend(true, {}, this._originalOptions));
|
|||
|
};
|
|||
|
|
|||
|
Viewer._extendCarouselOptions = function(options){
|
|||
|
// clone the carousel options for the given breakpoint
|
|||
|
var clonedCarousel = Akamai.Util.extend(true, {}, options.carousel || {});
|
|||
|
|
|||
|
// TODO shared code with constructor
|
|||
|
// extend the cloned options with the breakpoint options so that all the
|
|||
|
// child component config is attached to the carousel config (also happens
|
|||
|
// in the constructor )
|
|||
|
return Akamai.Util.extend(true, clonedCarousel, {
|
|||
|
magnifier: options.magnifier,
|
|||
|
spin360: options.spin360,
|
|||
|
video: options.video,
|
|||
|
fullscreen: options.fullscreen
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._updateOptions = function(options){
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// extend the carousel options for the given breakpoint
|
|||
|
var carouselOptions = Viewer._extendCarouselOptions(options);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Viewer.defaultOptions, options, "Viewer");
|
|||
|
|
|||
|
// pass the new options down to all child carousels
|
|||
|
this._carousels.forEach(function(comp){
|
|||
|
comp._updateOptions(carouselOptions);
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._checkJSONUrls = function(obj){
|
|||
|
var hostnames = this._options.items.hostnames;
|
|||
|
|
|||
|
return Akamai.Util.mapJSONVals(obj,function(val, key){
|
|||
|
if(key === "url" && !this._urlHostnameMatch(val)){
|
|||
|
throw new Error(`The URL \`${val}\` does not match this page's hostname or the whitelist defined in Akamai.Viewer option \`hostnames\` which is:
|
|||
|
|
|||
|
${hostnames.length ? hostnames.join("\n") : "No hostnames"}
|
|||
|
`);
|
|||
|
}
|
|||
|
|
|||
|
return val;
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._urlHostnameMatch = function(url){
|
|||
|
var parser = document.createElement('a');
|
|||
|
parser.href = url;
|
|||
|
|
|||
|
return parser.hostname === "" || parser.hostname === window.location.hostname ||
|
|||
|
this._options.items.hostnames.indexOf(parser.hostname) >= 0;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Viewers on the page
|
|||
|
* @static
|
|||
|
* @property {Object} breakpoints - configuration changes for child components at breakpoints (no default)
|
|||
|
* @property {Object} fullscreenBreakpoints - configuration changes for child components at breakpoints when in fullscreen mode (no default)
|
|||
|
* @property {Object[]} items.data - array of items from the Akamai JSON (default: undefined)
|
|||
|
* @property {String} items.defaultTag - (default: "akamai-untagged")
|
|||
|
* @property {String[]} items.hostnames - whitelist of URL hostnames to check for in JSON, (default: empty array)
|
|||
|
* @property {Integer} items.limit - size limit of JSON data in kibibytes (default: 100)
|
|||
|
* @property {String} items.renderAll - (default: false)
|
|||
|
* @property {String[]} items.tags - Set of tags to match against the Akamai JSON data (default: undefined)
|
|||
|
* @property {String} items.uri - URI at which to retrieve the Akamai JSON (default: undefined)
|
|||
|
* @property {Object} carousel - child {@link Akamai.Carousel} options
|
|||
|
* @property {Object} magnifier - child {@link Akamai.Magnifier} options
|
|||
|
* @property {Object} spin360 - child {@link Akamai.Spin360} options
|
|||
|
* @property {Object} video - child {@link Akamai.Video} options
|
|||
|
* @property {Object} fullscreen - child {@link Akamai.Fullscreen} options
|
|||
|
*/
|
|||
|
Viewer.defaultOptions = {
|
|||
|
breakpoints: {},
|
|||
|
items: {
|
|||
|
data: undefined,
|
|||
|
defaultTag: "akamai-untagged",
|
|||
|
hostnames: [],
|
|||
|
limit: 100,
|
|||
|
renderAll: false,
|
|||
|
tags: undefined,
|
|||
|
uri: undefined
|
|||
|
},
|
|||
|
carousel: Akamai.Carousel.defaultOptions,
|
|||
|
magnifier: Akamai.Magnifier.defaultOptions,
|
|||
|
spin360: Akamai.Spin360.defaultOptions,
|
|||
|
video: Akamai.Video.defaultOptions,
|
|||
|
fullscreen: Akamai.Fullscreen.defaultOptions
|
|||
|
};
|
|||
|
|
|||
|
Viewer._tagSplit = function(data, options){
|
|||
|
var items = options;
|
|||
|
|
|||
|
// set all items without a tag to the default
|
|||
|
data = data.map(function(item){
|
|||
|
item.tags = (item.tags && item.tags.length) ? item.tags : [items.defaultTag];
|
|||
|
return item;
|
|||
|
});
|
|||
|
|
|||
|
// if the tags option was set, filter items out that don't match
|
|||
|
if( items.tags ) {
|
|||
|
data = items.data.filter(function(item){
|
|||
|
return item.tags.reduce(function(acc, tag){
|
|||
|
return acc || items.tags.indexOf(tag) >= 0;
|
|||
|
}, false);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// from the filtered get the first (may be default tag)
|
|||
|
var def = data[0].tags[0];
|
|||
|
|
|||
|
// create a mapping from tags to items in the set
|
|||
|
var tagMapping = data.reduce(function(acc, item){
|
|||
|
(item.tags || []).forEach(function(tag){
|
|||
|
acc[tag] = acc[tag] || [];
|
|||
|
acc[tag].push(item);
|
|||
|
});
|
|||
|
|
|||
|
return acc;
|
|||
|
}, {});
|
|||
|
|
|||
|
// set the first (default) object to the first tag
|
|||
|
tagMapping[Viewer._firstRenderTag] = tagMapping[def];
|
|||
|
|
|||
|
return tagMapping;
|
|||
|
};
|
|||
|
|
|||
|
Viewer._firstRenderTag = "akamai-first-render";
|
|||
|
Viewer._tagAttr = `${Viewer._dataAttr}-tag`;
|
|||
|
|
|||
|
Viewer._renderTag = function(data, options){
|
|||
|
// get the first item and it's tag
|
|||
|
// NOTE this assumes that the data has been normalized to have a default tag
|
|||
|
var tag = data[0].tags[0];
|
|||
|
|
|||
|
// TODO shoestring doesn't treat html correctly unless the leading `<` has
|
|||
|
// no whitepsace in front of it:
|
|||
|
// shoestring/issues/94
|
|||
|
return Akamai.Util.trim(`
|
|||
|
<div ${Viewer._tagAttr}="${tag}">
|
|||
|
${Akamai.Carousel.render(data, options)}
|
|||
|
</div>
|
|||
|
`);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Takes standard options including an `items` attribute and renders HTML
|
|||
|
* that conforms to the component expectations
|
|||
|
* @method
|
|||
|
* @static
|
|||
|
* @params {Object[]} json - Akamai JSON data
|
|||
|
* @params {Object} options - options object with an `items` attribute, see default options
|
|||
|
* @params {Boolean} options.items - configuration for how the items should be
|
|||
|
* rendered, see {@link Akamai.Viewer.defaultOptions} default options items sub-configuration
|
|||
|
* @returns { String }
|
|||
|
*/
|
|||
|
Viewer.render = function(json, options, unwrapped){
|
|||
|
// NOTE we do not extend the default options here because it has no bearing
|
|||
|
// on the rendering of the Viewer markup or child markup. This is contrast
|
|||
|
// to the carousel which does extend the passed options with defaults so the
|
|||
|
// markup can make use of them
|
|||
|
var data = Viewer._tagSplit(json, options.items);
|
|||
|
var carouselOptions = Viewer._extendCarouselOptions(options);
|
|||
|
|
|||
|
var childMarkup;
|
|||
|
|
|||
|
if( options.items.renderAll ) {
|
|||
|
childMarkup = data.map(function(acc, datum){
|
|||
|
return Viewer._renderTag(datum, carouselOptions);
|
|||
|
}).join(" ");
|
|||
|
} else {
|
|||
|
childMarkup = Viewer._renderTag(data[Viewer._firstRenderTag], carouselOptions);
|
|||
|
}
|
|||
|
|
|||
|
var wrapped = `
|
|||
|
<div ${Viewer._dataAttr}>
|
|||
|
${childMarkup}
|
|||
|
</div>
|
|||
|
`;
|
|||
|
|
|||
|
return unwrapped ? childMarkup : wrapped ;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Accessor for child carousel components.
|
|||
|
* @method
|
|||
|
* @returns { Akamai.Carousel[] }
|
|||
|
*/
|
|||
|
Viewer.prototype.getCarousels = function(){
|
|||
|
return this._carousels;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Set the visible carousel based on the tag. Note the method activity does
|
|||
|
* not complete until all the images from the relevant viewer have loaded, use
|
|||
|
* the `akamai-viewer-switch-tag` event
|
|||
|
* @method
|
|||
|
* @fires Akamai.Viewer#akamai-viewer-switch-tag
|
|||
|
* @params {String} tag - The tag corresponding to the desired carousel
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Viewer.prototype.switchTag = function(tag){
|
|||
|
var tags = this.getTags();
|
|||
|
|
|||
|
// if the passed tag isn't in the data set return early and log an error in
|
|||
|
// the console
|
|||
|
if( tags.indexOf(tag) == -1){
|
|||
|
Akamai.Util.log(`tag: ${tag} is not present in the data for this viewer`, 'error');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var selector = `[${this.constructor._tagAttr}="${tag}"]`;
|
|||
|
|
|||
|
// try to find an existing element with the tag
|
|||
|
var $taggedViewer = this._$el.find(selector);
|
|||
|
|
|||
|
// if there's no element append the newly rendered tag markup
|
|||
|
if( !$taggedViewer.length ){
|
|||
|
// make sure the config takes into account the current breakpoint
|
|||
|
var options = this._resolveBreakpointOptions();
|
|||
|
var carouselOptions = Viewer._extendCarouselOptions(options);
|
|||
|
|
|||
|
// create the markup that will be inserted
|
|||
|
var $markup = $(this.constructor._renderTag(this._tagMapping[tag], carouselOptions, true));
|
|||
|
var $imgs = $markup.find("img");
|
|||
|
var imgCount = $imgs.length;
|
|||
|
var loaded = 0;
|
|||
|
|
|||
|
// hide the new carousel initially
|
|||
|
$markup.css("display", "none");
|
|||
|
|
|||
|
$imgs.bind("load", function(){
|
|||
|
if( ++loaded !== imgCount ) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// TODO namespace?
|
|||
|
$imgs.unbind("load");
|
|||
|
|
|||
|
var carousels = Akamai.Carousel.createMany($markup[0], carouselOptions);
|
|||
|
|
|||
|
carousels.forEach(function(c){
|
|||
|
// TODO figure out why the carousel constructor doesn't apply the
|
|||
|
// active index class we want on instantiation, likely due to the fact
|
|||
|
// that the carousel is hidden so the "getIndex" calc is broken
|
|||
|
// mark the carousel item as active
|
|||
|
c.goto(c.getIndex());
|
|||
|
});
|
|||
|
|
|||
|
// create and store the new carousels (should be one)
|
|||
|
this._carousels = this._carousels.concat(carousels);
|
|||
|
this._showViewer($markup, carousels);
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
// append the new markup to the existing viewer
|
|||
|
this._$el.append($markup);
|
|||
|
} else {
|
|||
|
this._showViewer($taggedViewer);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// TODO should be handled in CSS
|
|||
|
Viewer.prototype._showViewer = function($viewer, carousels){
|
|||
|
this._$el
|
|||
|
.find(`[${this.constructor._tagAttr}]`)
|
|||
|
.css("display", "none")
|
|||
|
.removeClass("focused");
|
|||
|
|
|||
|
$viewer
|
|||
|
.css("display", "block")
|
|||
|
.addClass("focused");
|
|||
|
|
|||
|
if( carousels ){
|
|||
|
carousels.map(function(c) { c.refresh(); });
|
|||
|
}
|
|||
|
|
|||
|
// TODO the placement here seems arbitrary, probably belongs in `_showViewer`
|
|||
|
// TODO also sucks to be so tightly coupled
|
|||
|
this._fullscreen.addButton();
|
|||
|
|
|||
|
this._trigger( "switch-tag" );
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Accessor for tags derived from Akamai JSON data
|
|||
|
* @method
|
|||
|
* @returns { String[] }
|
|||
|
*/
|
|||
|
Viewer.prototype.getTags = function(){
|
|||
|
if( this._tags ) {
|
|||
|
return this._tags;
|
|||
|
};
|
|||
|
|
|||
|
var tags = [];
|
|||
|
|
|||
|
// map and store all the carousel tags
|
|||
|
for(var tag in this._tagMapping){
|
|||
|
if(this._tagMapping.hasOwnProperty(tag) && tag !== Viewer._firstRenderTag ) {
|
|||
|
// otherwise grab the list of tags
|
|||
|
tags.push(tag);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this._tags = tags;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the viewer switches tag views. This includes waiting for
|
|||
|
* images to load for carousels dedicated to previously unviewed tags.
|
|||
|
* {@link Akamai.Viewer#switchTag}.
|
|||
|
*
|
|||
|
* @event Akamai.Viewer#akamai-viewer-switch-tag
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Viewer}.
|
|||
|
*
|
|||
|
* @event Akamai.Viewer#akamai-viewer-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Viewer = Viewer;
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function(exports, $){
|
|||
|
$.fn.akamaiViewer = function(options){
|
|||
|
this.each(function(i, element){
|
|||
|
new Akamai.Viewer(element, options);
|
|||
|
});
|
|||
|
};
|
|||
|
})(typeof exports === 'undefined'? window : exports, this.jQuery);
|