9184 lines
263 KiB
JavaScript
9184 lines
263 KiB
JavaScript
|
/*! 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 "\n\t\t\t<img src=\"" + Spin360._fallbackSrc(url, options.images) + "\"\n\t\t\t\tsrcset=\"" + Spin360._srcset(url, options.images) + "\"\n\t\t\t\tsizes=\"" + options.images.sizes + "\" />\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
Spin360.render = function (json, options) {
|
|||
|
var finalOptions = Akamai.Util.options(Spin360.defaultOptions, options);
|
|||
|
|
|||
|
// TODO fix the string problem in shoestring
|
|||
|
return Akamai.Util.trim("\n\t\t\t<div class=\"tau\" data-akamai-spin360 title=\"" + json.alt + "\">\n\t\t\t\t" + Akamai.Util.map(json.urls, function (url) {
|
|||
|
return Spin360._renderImg(url, finalOptions);
|
|||
|
}).join("\n") + "\n\t\t\t</div>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Spin360s on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} autoplay.enabled - Enable autoplay (default: false)
|
|||
|
* @property {Integer} autoplay.delay - Delay in milliseconds after initialization before spinning begins (default: 1000)
|
|||
|
* @property {Boolean} controls.arrows - Render controls to spin left and right (default: false)
|
|||
|
* @property {Boolean} controls.play - Render controls to enable and disable automatic spinning (default: false)
|
|||
|
* @property {String} controls.text.left - Left rotation control title and text (default: "Rotate Left")
|
|||
|
* @property {String} controls.text.right - Right rotation control title and text (default: "Rotate Right")
|
|||
|
* @property {String} controls.text.play - Spin control title and text (default: "Spin Object")
|
|||
|
* @property {Array} images.widths - list of available widths for an image (to be combined with image.widthParam), Default: ["320","640","800","1024","2048","5000"]
|
|||
|
* @property {String} images.sizes - value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw`, `200px`, `(min-width:1000px) 500px, 100vw`.
|
|||
|
* @property {String} images.policy - query param value for policy, appended to &policy= per image url when specified. Values: `foo`. Default: undefined.
|
|||
|
* @property {String} images.widthParam - query string name to use for setting each url width. Default urls will be ?imwidth=320 for example. Values: `imwidth` (default), `w`, `width`, etc.
|
|||
|
* @property {Integer} interval - The full rotation interval in milliseconds, determines physics, (default: 3000)
|
|||
|
* @property {Boolean} reverse - Reverse the direction of the spin (default: false)
|
|||
|
* @property {Float} sensitivity - The speed at which the object rotates relative to user input (default: 1). At the default value of `1` the object will complete a full 360 rotation when you drag across the entire width of the spin360 component. To require less effort to complete a rotation, change this value to a higher number. For example, setting `sensitivity: 2` would complete a full 360 rotation by dragging halfway (ex. from the center to the edge).
|
|||
|
*/
|
|||
|
Spin360.defaultOptions = {
|
|||
|
autoplay: {
|
|||
|
enabled: false,
|
|||
|
delay: 1000
|
|||
|
},
|
|||
|
|
|||
|
controls: {
|
|||
|
arrows: false,
|
|||
|
play: false,
|
|||
|
text: {
|
|||
|
left: "Rotate Left",
|
|||
|
right: "Rotate Right",
|
|||
|
play: "Spin Object"
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// unsupported, the number of frame images to create using the configured
|
|||
|
// template
|
|||
|
frames: 72,
|
|||
|
images: Akamai.Image.defaultConfig,
|
|||
|
interval: 3000,
|
|||
|
reverse: false,
|
|||
|
|
|||
|
sensitivity: 1,
|
|||
|
|
|||
|
// unsupported, template used to generate urls when only one image is
|
|||
|
// present in the 360 viewer
|
|||
|
template: undefined
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Goto to a particular frame of the spining image
|
|||
|
* @method
|
|||
|
* @param {Integer} index - the frame to advance to
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.goto = function (index, callback) {
|
|||
|
this._comp.goto(index);
|
|||
|
// NOTE goto must come before the callback because the callback will be
|
|||
|
// used to trigger the `next` and `previous` events. The order should be
|
|||
|
// maintained consistently as `goto` -> `next`/`previous`
|
|||
|
this._trigger("goto");
|
|||
|
if (callback) {
|
|||
|
callback();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current frame index
|
|||
|
* @method
|
|||
|
* @returns {Integer}
|
|||
|
*/
|
|||
|
Spin360.prototype.getIndex = function () {
|
|||
|
return this._comp.index;
|
|||
|
};
|
|||
|
|
|||
|
// Extend Carousel with Advanceable interface
|
|||
|
Akamai.Advanceable.extend(Spin360);
|
|||
|
|
|||
|
/**
|
|||
|
* Go to the next frame
|
|||
|
* @method
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.next;
|
|||
|
// required for JSDocs
|
|||
|
|
|||
|
/**
|
|||
|
* Go to the previous frame
|
|||
|
* @method
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.previous;
|
|||
|
// required for JSDocs
|
|||
|
|
|||
|
/**
|
|||
|
* Begin the automatic rotation of the images
|
|||
|
* @todo support passing in an interval or speed?
|
|||
|
* @method
|
|||
|
* @fires Akamai.Spin360#akamai-spin360-play
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.play = function () {
|
|||
|
this._comp.autoRotate();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Stop the automatic rotation of the images
|
|||
|
* @method
|
|||
|
* @fires Akamai.Spin360#akamai-spin360-pause
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Spin360.prototype.pause = function () {
|
|||
|
this._comp.stopAutoRotate();
|
|||
|
};
|
|||
|
|
|||
|
Spin360.prototype.getElement = function () {
|
|||
|
return this._$el[0];
|
|||
|
};
|
|||
|
|
|||
|
Spin360.States = {
|
|||
|
Playing: 0,
|
|||
|
Paused: 1
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the spin360
|
|||
|
* @example <caption>Spin360 states</caption>
|
|||
|
* Akamai.Spin360.States = {
|
|||
|
* Playing: 0,
|
|||
|
* Paused: 1
|
|||
|
* };
|
|||
|
*
|
|||
|
* @example <caption>Conditioning on states</caption>
|
|||
|
* if( spin360.getState() === Akamai.Spin360.States.Paused ) {
|
|||
|
* spin360.play()
|
|||
|
* }
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Spin360.State}
|
|||
|
*/
|
|||
|
Spin360.prototype.getState = function () {
|
|||
|
// TODO expose using method in Tau
|
|||
|
return !!this._comp.autoInterval ? Spin360.States.Playing : Spin360.States.Paused;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the spin360 has stoped automatically spinning
|
|||
|
* {@link Akamai.Spin360#pause}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-stop-spin
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the spin360 has started automatically spinning
|
|||
|
* {@link Akamai.Spin360#play}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-start-spin
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Spin360}.
|
|||
|
*
|
|||
|
* @event Akamai.Spin360#akamai-spin360-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Spin360 = Spin360;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function () {
|
|||
|
function MagnifierImpl(element, options) {
|
|||
|
throw new Error('Not allowed to instantiate MagnifierImpl');
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImpl.prototype.updateOptions = function (options) {
|
|||
|
throw new Error('magnifier updateOptions not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.zoomIn = function () {
|
|||
|
throw new Error('magnifier zoomIn not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.zoomOut = function () {
|
|||
|
throw new Error('magnifier zoomOut not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.isMagnified = function () {
|
|||
|
throw new Error('magnifier isMagnified not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.toggleZoom = function () {
|
|||
|
throw new Error('magnifier toggleZoom not implemented');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImpl.prototype.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
|
|||
|
throw new Error('magnifier render not implemented');
|
|||
|
};
|
|||
|
|
|||
|
var ex = typeof exports === 'undefined' ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImpl = MagnifierImpl;
|
|||
|
})();
|
|||
|
(function () {
|
|||
|
function MagnifierImplEnlarge(element, options) {
|
|||
|
this._$el = $(element);
|
|||
|
this._$el.enlarge(options);
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype = Object.create(Akamai.MagnifierImpl.prototype);
|
|||
|
MagnifierImplEnlarge.prototype.constructor = MagnifierImplEnlarge;
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.updateOptions = function (options) {
|
|||
|
this._$el.enlarge("updateOptions", options);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.zoomIn = function () {
|
|||
|
this._$el.enlarge("in");
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.zoomOut = function () {
|
|||
|
this._$el.enlarge("out");
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.isMagnified = function () {
|
|||
|
return this._$el.enlarge("isZoomed");
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.prototype.toggleZoom = function () {
|
|||
|
if (this.isMagnified()) {
|
|||
|
this.zoomOut();
|
|||
|
} else {
|
|||
|
this.zoomIn();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplEnlarge.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
|
|||
|
return "\n\t\t\t<div " + dataAttr + ">\n\t\t\t\t<div class=\"enlarge_contain\">\n\t\t\t\t\t<img src=\"" + fallbackSrc + "\"\n\t\t\t\t\t\tsrcset=\"" + srcSet + "\"\n\t\t\t\t\t\tsizes=\"" + options.image.sizes + "\">\n\t\t\t\t</div>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"enlarge_btn\"\n\t\t\t\t\ttitle=\"" + options.buttonText + "\">\n\t\t\t\t\t" + options.buttonText + "\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
var ex = typeof exports === 'undefined' ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImplEnlarge = MagnifierImplEnlarge;
|
|||
|
})();
|
|||
|
(function () {
|
|||
|
function ClickTracker(element, handler) {
|
|||
|
var self = this;
|
|||
|
self.element = element;
|
|||
|
self.handler = handler;
|
|||
|
this.reset();
|
|||
|
}
|
|||
|
|
|||
|
ClickTracker.prototype.onDown = function (e) {
|
|||
|
if (e.target == this.element) {
|
|||
|
if (this.isDown) {
|
|||
|
this.reset();
|
|||
|
} else {
|
|||
|
this.down.x = e.clientX;
|
|||
|
this.down.y = e.clientY;
|
|||
|
this.down.timestamp = new Date();
|
|||
|
this.isDown = true;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.onMove = function () {
|
|||
|
this.isMoved = true;
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.onUp = function (e) {
|
|||
|
if (this.isDown) {
|
|||
|
this.up.x = e.clientX;
|
|||
|
this.up.y = e.clientY;
|
|||
|
this.up.timestamp = new Date();
|
|||
|
|
|||
|
if (!this.isMoved && this.down.x === this.up.x && this.down.y === this.up.y && this.up.timestamp.getTime() - this.down.timestamp.getTime() <= 500) {
|
|||
|
this.handler();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this.reset();
|
|||
|
};
|
|||
|
|
|||
|
ClickTracker.prototype.reset = function (x, y) {
|
|||
|
this.down = {
|
|||
|
x: -Infinity,
|
|||
|
y: -Infinity,
|
|||
|
timestamp: new Date(0)
|
|||
|
};
|
|||
|
|
|||
|
this.isDown = false;
|
|||
|
this.isMoved = false;
|
|||
|
|
|||
|
this.up = {
|
|||
|
x: Infinity,
|
|||
|
y: Infinity,
|
|||
|
timestamp: new Date()
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
function MagnifierImplScroller(element, options) {
|
|||
|
this._$el = $(element);
|
|||
|
this._options = options;
|
|||
|
this._container = this._$el.get(0);
|
|||
|
this._content = this._$el.children().get(0);
|
|||
|
this._imgCast = $(this._content).find('div').get(0);
|
|||
|
this._zoomInBtn = this._$el.find("a.scroller-zoom-in").get(0);
|
|||
|
this._zoomOutBtn = this._$el.find("a.scroller-zoom-out").get(0);
|
|||
|
this._magnification = 1;
|
|||
|
|
|||
|
var self = this;
|
|||
|
|
|||
|
// Initialize Scroller
|
|||
|
this.scroller = new Scroller(this._renderer(), {
|
|||
|
zooming: options.enabled,
|
|||
|
minZoom: 1,
|
|||
|
maxZoom: options.magnification,
|
|||
|
animationDuration: options.animationDuration
|
|||
|
});
|
|||
|
|
|||
|
var rect = this._container.getBoundingClientRect();
|
|||
|
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
|
|||
|
|
|||
|
this._installEventHandlers();
|
|||
|
this._updateButtonStates();
|
|||
|
setTimeout(function () {
|
|||
|
self._onResize();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
MagnifierImplScroller.prototype = Object.create(Akamai.MagnifierImpl.prototype);
|
|||
|
MagnifierImplScroller.prototype.constructor = MagnifierImplScroller;
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.updateOptions = function (options) {
|
|||
|
var rect = this._container.getBoundingClientRect();
|
|||
|
this.scroller.setPosition(rect.left + this._container.clientLeft, rect.top + this._container.clientTop);
|
|||
|
this._onResize();
|
|||
|
this._zoomBy(1 / this._options.magnification, false);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.zoomIn = function () {
|
|||
|
this._zoomBy(this._options.incrementalZoomFactor);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.cyclicZoom = function () {
|
|||
|
var self = this;
|
|||
|
if (this._magnification >= this._options.magnification) {
|
|||
|
this._zoomBy(1 / this._options.magnification);
|
|||
|
} else {
|
|||
|
this.zoomIn();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.zoomOut = function () {
|
|||
|
this._zoomBy(1 / this._options.incrementalZoomFactor);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.isMagnified = function () {
|
|||
|
return this._magnification > 1;
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype.toggleZoom = function () {
|
|||
|
throw new Error('zoom toggle behavior is undefined in scroller mode');
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.render = function (options, dataAttr, fallbackSrc, largestSrc, srcSet) {
|
|||
|
return "\n\t\t\t<div " + dataAttr + ">\n\t\t\t\t<div>\n\t\t\t\t\t<img src=\"" + fallbackSrc + "\"\n\t\t\t\t\tsrcset=\"" + srcSet + "\"\n\t\t\t\t\tsizes=\"" + options.image.sizes + "\">\n\t\t\t\t\t<div></div>\n\t\t\t\t</div>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"scroller-zoom-in\"\n\t\t\t\t\ttitle=\"" + options.zoomInBtnText + "\">\n\t\t\t\t\t" + options.zoomInBtnText + "\n\t\t\t\t</a>\n\t\t\t\t<a href=\"" + largestSrc + "\"\n\t\t\t\t\tclass=\"scroller-zoom-out\"\n\t\t\t\t\ttitle=\"" + options.zoomOutBtnText + "\">\n\t\t\t\t\t" + options.zoomOutBtnText + "\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._zoomBy = function (magnification, animate) {
|
|||
|
var self = this,
|
|||
|
magnification = Number(magnification);
|
|||
|
|
|||
|
if (typeof animate === "undefined") {
|
|||
|
animate = this._options.animateZoom;
|
|||
|
}
|
|||
|
|
|||
|
if (magnification === 1 || // if we are magnifying by 1 OR
|
|||
|
this._magnification === 1 && magnification < 1 || // fully zoomed out and still attempting a zoom out OR
|
|||
|
this._magnification === this._options.magnification && magnification > 1) {
|
|||
|
// fully zoomed in and still attempting a zoom in
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.scroller.zoomTo(this._magnification * magnification, this._options.animateZoom);
|
|||
|
|
|||
|
setTimeout(function () {
|
|||
|
self._onZoom();
|
|||
|
}, this._options.animationDuration);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._onZoom = function () {
|
|||
|
var oldMagnification = this._magnification;
|
|||
|
|
|||
|
this._magnification = Number(this.scroller.getValues().zoom.toFixed(2));
|
|||
|
this._updateButtonStates();
|
|||
|
|
|||
|
if (oldMagnification > this._magnification) {
|
|||
|
this._$el.trigger("scroller.after-zoom-out");
|
|||
|
} else if (oldMagnification < this._magnification) {
|
|||
|
this._$el.trigger("scroller.after-zoom-in");
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._renderer = function () {
|
|||
|
var docStyle = document.documentElement.style,
|
|||
|
self = this;
|
|||
|
|
|||
|
var engine;
|
|||
|
if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
|
|||
|
engine = 'presto';
|
|||
|
} else if ('MozAppearance' in docStyle) {
|
|||
|
engine = 'gecko';
|
|||
|
} else if ('WebkitAppearance' in docStyle) {
|
|||
|
engine = 'webkit';
|
|||
|
} else if (typeof navigator.cpuClass === 'string') {
|
|||
|
engine = 'trident';
|
|||
|
}
|
|||
|
|
|||
|
var vendorPrefix = {
|
|||
|
trident: 'ms',
|
|||
|
gecko: 'Moz',
|
|||
|
webkit: 'Webkit',
|
|||
|
presto: 'O'
|
|||
|
}[engine];
|
|||
|
|
|||
|
var helperElem = document.createElement("div");
|
|||
|
var undef;
|
|||
|
|
|||
|
var perspectiveProperty = vendorPrefix + "Perspective";
|
|||
|
var transformProperty = vendorPrefix + "Transform";
|
|||
|
|
|||
|
if (helperElem.style[perspectiveProperty] !== undef) {
|
|||
|
return function (left, top, zoom) {
|
|||
|
self._content.style[transformProperty] = 'translate3d(' + -left + 'px,' + -top + 'px,0) scale(' + zoom + ')';
|
|||
|
};
|
|||
|
} else if (helperElem.style[transformProperty] !== undef) {
|
|||
|
return function (left, top, zoom) {
|
|||
|
self._content.style[transformProperty] = 'translate(' + -left + 'px,' + -top + 'px) scale(' + zoom + ')';
|
|||
|
};
|
|||
|
} else {
|
|||
|
return function (left, top, zoom) {
|
|||
|
self._content.style.marginLeft = left ? -left / zoom + 'px' : '';
|
|||
|
self._content.style.marginTop = top ? -top / zoom + 'px' : '';
|
|||
|
self._content.style.zoom = zoom || '';
|
|||
|
};
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._onResize = function () {
|
|||
|
this.scroller.setDimensions(this._container.clientWidth, this._container.clientHeight, this._container.clientWidth, this._container.clientHeight);
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._installEventHandlers = function () {
|
|||
|
var self = this,
|
|||
|
clickTracker = new ClickTracker(this._imgCast, function () {
|
|||
|
self.cyclicZoom();
|
|||
|
});
|
|||
|
|
|||
|
window.addEventListener("resize", function (e) {
|
|||
|
self._onResize(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
this._zoomInBtn.addEventListener("click", function (e) {
|
|||
|
e.preventDefault();
|
|||
|
self.zoomIn();
|
|||
|
}, false);
|
|||
|
|
|||
|
this._zoomOutBtn.addEventListener("click", function (e) {
|
|||
|
e.preventDefault();
|
|||
|
self.zoomOut();
|
|||
|
}, false);
|
|||
|
|
|||
|
if ('ontouchstart' in window) {
|
|||
|
self._container.addEventListener("touchstart", function (e) {
|
|||
|
// Don't react if initial down happens on one of the zoom buttons
|
|||
|
if (e.touches[0].target == self._$el.find('a.scroller-zoom-in').get(0) || e.touches[0].target == self._$el.find('a.scroller-zoom-out').get(0)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Don't react if initial down happens on a form element
|
|||
|
if (e.touches[0] && e.touches[0].target && e.touches[0].target.tagName.match(/input|textarea|select/i)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchStart(e.touches, e.timeStamp);
|
|||
|
|
|||
|
for (var i = 0; i < e.touches.length; i++) {
|
|||
|
clickTracker.onDown(e.touches[i]);
|
|||
|
}
|
|||
|
|
|||
|
e.preventDefault();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchmove", function (e) {
|
|||
|
self.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
|
|||
|
clickTracker.onMove();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchend", function (e) {
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
|
|||
|
// handle any zoom that may have occured
|
|||
|
self._onZoom();
|
|||
|
|
|||
|
for (var i = 0; i < e.changedTouches.length; i++) {
|
|||
|
clickTracker.onUp(e.changedTouches[i]);
|
|||
|
}
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("touchcancel", function (e) {
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
// handle any zoom that may have occured
|
|||
|
self._onZoom();
|
|||
|
|
|||
|
for (var i = 0; i < e.changedTouches.length; i++) {
|
|||
|
clickTracker.onUp(e.changedTouches[i]);
|
|||
|
}
|
|||
|
}, false);
|
|||
|
} else {
|
|||
|
self._container.addEventListener("mousedown", function (e) {
|
|||
|
if (e.target.tagName.match(/input|textarea|select/i)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (self.scroller.__clientWidth === 0) {
|
|||
|
self._onResize();
|
|||
|
}
|
|||
|
self.scroller.doTouchStart([{
|
|||
|
pageX: e.pageX,
|
|||
|
pageY: e.pageY
|
|||
|
}], e.timeStamp);
|
|||
|
|
|||
|
clickTracker.onDown(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("mousemove", function (e) {
|
|||
|
if (!clickTracker.isDown) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchMove([{
|
|||
|
pageX: e.pageX,
|
|||
|
pageY: e.pageY
|
|||
|
}], e.timeStamp);
|
|||
|
|
|||
|
clickTracker.onMove();
|
|||
|
}, false);
|
|||
|
|
|||
|
document.addEventListener("mouseup", function (e) {
|
|||
|
if (!clickTracker.isDown) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.scroller.doTouchEnd(e.timeStamp);
|
|||
|
clickTracker.onUp(e);
|
|||
|
}, false);
|
|||
|
|
|||
|
// self._container.addEventListener(navigator.userAgent.indexOf("Firefox") > -1 ? "DOMMouseScroll" : "mousewheel", function(e) {
|
|||
|
// e.preventDefault();
|
|||
|
// self.scroller.doMouseZoom(e.detail ? (e.detail * -120) : e.wheelDelta, e.timeStamp, e.pageX, e.pageY);
|
|||
|
// }, false);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MagnifierImplScroller.prototype._updateButtonStates = function () {
|
|||
|
if (this._magnification === 1) {
|
|||
|
$(this._$el).addClass('scroller-zoom-out-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-in-max');
|
|||
|
} else if (this._magnification === this._options.magnification) {
|
|||
|
$(this._$el).addClass('scroller-zoom-in-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-out-max');
|
|||
|
} else {
|
|||
|
$(this._$el).removeClass('scroller-zoom-in-max');
|
|||
|
$(this._$el).removeClass('scroller-zoom-out-max');
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
var ex = typeof exports === 'undefined' ? window : exports;
|
|||
|
ex.Akamai = ex.Akamai || {};
|
|||
|
ex.Akamai.MagnifierImplScroller = MagnifierImplScroller;
|
|||
|
})();
|
|||
|
(function (exports, $) {
|
|||
|
/**
|
|||
|
* Image magnifier
|
|||
|
* @class
|
|||
|
* @alias Akamai.Magnifier
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Magnifier = Akamai.Util.component("Magnifier", function (element, options) {
|
|||
|
// compat with enlarge `disabled` option
|
|||
|
this._options.disabled = !this._options.enabled;
|
|||
|
if (this._options.mode === Magnifier.MODE_ANIMATED_ZOOM) {
|
|||
|
this._impl = new Akamai.MagnifierImplScroller(element, options);
|
|||
|
} else {
|
|||
|
this._impl = new Akamai.MagnifierImplEnlarge(element, options);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
Magnifier.prototype._updateOptions = function (options) {
|
|||
|
if (!options) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// compat with enlarge `disabled` option
|
|||
|
this._options.disabled = !this._options.enabled;
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Magnifier.defaultOptions, this._options, "Magnifier");
|
|||
|
|
|||
|
this._impl.updateOptions(this._options);
|
|||
|
};
|
|||
|
|
|||
|
// Used in preflight to "rename" events based on the child component events
|
|||
|
Magnifier._componentEventMapping = {
|
|||
|
"enlarge.after-zoom-in": "in",
|
|||
|
"enlarge.after-zoom-out": "out",
|
|||
|
"scroller.after-zoom-in": "in",
|
|||
|
"scroller.after-zoom-out": "out"
|
|||
|
};
|
|||
|
|
|||
|
Magnifier.MODE_HOVER_ZOOM = 'hoverzoom';
|
|||
|
Magnifier.MODE_ANIMATED_ZOOM = 'animatedzoom';
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Magnifiers on the page
|
|||
|
* @static
|
|||
|
* @property {String} mode - Set what mode to run the magnifier in. There are two modes available: `hoverzoom` and `animatedzoom`. The `hoverzoom` mode provides the ability to magnify and pan the image by just hovering your mouse over the image. It also provides a `flyout` option where a clip of the zoomed in image is displayed on a separate widget floating somewhere around (configurable) the image. The `animatedzoom` mode only does inline magnification but provides smooth animation between magnification levels, it also allows for incremental zooming.
|
|||
|
* @property {Boolean} button - Whether to show a button for toggling magnification (default: true)
|
|||
|
* @property {Boolean} enabled - Enabled/disable magnification (default: true, breakpoints supported)
|
|||
|
* @property {Integer} magnification - The scale factor to magnify the image: `2`, `3` (default), `4`, `4.5`, etc
|
|||
|
* @property {Array} image.widths - List of available widths for an image (to be combined with image.widthParam) - (default: ["320","640","800","1024","2048","5000"])
|
|||
|
* @property {String} image.sizes - Value for image sizes attribute. Default is set dynamically to viewer width when rendered with JS, and updated dynamically. Values: `100vw` (default), `200px`, `(min-width:1000px) 500px, 100vw` - (default: `100vw`, breakpoints supported)
|
|||
|
* @property {String} image.policy - Query param value for policy, appended to &policy= per image url when specified. Values: `foo`. (default: undefined)
|
|||
|
* @property {Integer} delay - Only applicable in `hoverzoom` mode. The time delay in milliseconds between mouse hover and magnification (default: 300, breakpoints supported)
|
|||
|
* @property {String} buttonText - Only applicable in `hoverzoom` mode. Text for the zoom button. Also used for its title attribute. (default: "Toggle Image Magnification")
|
|||
|
* @property {Integer} flyout.width - Only applicable in `hoverzoom` mode. Width of the flyout image (default: 200)
|
|||
|
* @property {Integer} flyout.height - Only applicable in `hoverzoom` mode. Height of the flyout image (default: 200)
|
|||
|
* @property {Boolean} hoverZoomWithoutClick - Only applicable in `hoverzoom` mode. Zoom starts on mouse hover with no click needed (default: true; false will require a click to hover-zoom)
|
|||
|
* @property {String} placement - Only applicable in `hoverzoom` mode. Placement of the magnified image: `inline` , `flyoutloupe`, `flyouttopleft`,`flyoutbottomleft` ,`flyouttopright` and `flyoutbottomright` - (default: inline, breakpoins supported)
|
|||
|
* @property {Float} incrementalZoomFactor - Only applicable in `animatedzoom` mode. A number by which to incrementally zoom up until the specified `magnification`, default `3`. For example a `magnification` of `4` and an `incrementalZoomFactor` of `2` will zoom the image in `2` steps
|
|||
|
* @property {Boolean} animateZoom - Only applicable in `animatedzoom` mode. Animates the magnification process. Default `true`
|
|||
|
* @property {Integer} animationDuration - Only applicable in `animatedzoom` mode. If `animateZoom` is true, this specifies the length of the animation in milliseconds. Default `250`
|
|||
|
* @property {String} zoomInBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom in button. Default `Zoom In`
|
|||
|
* @property {String} zoomOutBtnText - Only applicable in `animatedzoom` mode. Hover text to display on the zoom out button. Default `Zoom Out`
|
|||
|
*/
|
|||
|
Magnifier.defaultOptions = {
|
|||
|
// general options
|
|||
|
mode: Magnifier.MODE_HOVER_ZOOM,
|
|||
|
button: true,
|
|||
|
enabled: true,
|
|||
|
magnification: 3,
|
|||
|
|
|||
|
// image options
|
|||
|
image: Akamai.Image.defaultConfig,
|
|||
|
|
|||
|
// hoverzoom options
|
|||
|
delay: 300,
|
|||
|
buttonText: "Toggle Image Magnification",
|
|||
|
flyout: {
|
|||
|
width: 200,
|
|||
|
height: 200
|
|||
|
},
|
|||
|
hoverZoomWithoutClick: true,
|
|||
|
placement: "inline",
|
|||
|
|
|||
|
// animatedzoom options
|
|||
|
incrementalZoomFactor: 3,
|
|||
|
animateZoom: true,
|
|||
|
animationDuration: 250,
|
|||
|
zoomInBtnText: 'Zoom In',
|
|||
|
zoomOutBtnText: 'Zoom Out'
|
|||
|
};
|
|||
|
|
|||
|
// srcset stuff
|
|||
|
Akamai.Sourceable.extendStatic(Magnifier);
|
|||
|
|
|||
|
Magnifier.render = function (json, options) {
|
|||
|
var finalOptions = Akamai.Util.options(Magnifier.defaultOptions, options);
|
|||
|
var dataAttr = Magnifier._dataAttr;
|
|||
|
var fallbackSrc = Magnifier._fallbackSrc(json.url, finalOptions.image);
|
|||
|
var largestSrc = Magnifier._largestSrc(json.url, finalOptions.image);
|
|||
|
var srcSet = Magnifier._srcset(json.url, finalOptions.image);
|
|||
|
var sizes = finalOptions.image.sizes;
|
|||
|
var impl = finalOptions.mode === Magnifier.MODE_ANIMATED_ZOOM ? Akamai.MagnifierImplScroller : Akamai.MagnifierImplEnlarge;
|
|||
|
return impl.render(finalOptions, dataAttr, fallbackSrc, largestSrc, srcSet, sizes);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Enter magnifier mode
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-in
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.in = function () {
|
|||
|
this._impl.zoomIn();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Exit magnifier mode
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-out
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.out = function () {
|
|||
|
this._impl.zoomOut();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the magnifier
|
|||
|
* @method
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
Magnifier.prototype.isMagnified = function () {
|
|||
|
return this._impl.isMagnified();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Toggle the state of the magnifier
|
|||
|
* @method
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-in
|
|||
|
* @fires Akamai.Magnifier#akamai-magnifier-out
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Magnifier.prototype.toggle = function () {
|
|||
|
this._impl.toggleZoom();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the magnifier has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carouse#in}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-in
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the magnifier has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carouse#out}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-out
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Magnifier}.
|
|||
|
*
|
|||
|
* @event Akamai.Magnifier#akamai-magnifier-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Magnifier = Magnifier;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
var VideoImpl = Akamai.Util.component("VideoImpl", function (element, options) {
|
|||
|
throw new Error('Not allowed to instantiate VideoImpl');
|
|||
|
});
|
|||
|
|
|||
|
VideoImpl.render = function (json, options) {
|
|||
|
throw new Error('VideoImpl.render not implemented');
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImpl = VideoImpl;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
var VideoImplPristine = Akamai.Util.component("VideoImplPristine", function (element, options) {});
|
|||
|
|
|||
|
VideoImplPristine.prototype = Object.create(Akamai.VideoImpl.prototype);
|
|||
|
VideoImplPristine.prototype.constructor = VideoImplPristine;
|
|||
|
|
|||
|
VideoImplPristine.render = function (json, options) {
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
|
|||
|
var url = json.url;
|
|||
|
var poster = json.poster || "";
|
|||
|
var mime = json.mime ? 'type="' + json.mime + '"' : '';
|
|||
|
|
|||
|
var attrs = [finalOptions.loop ? "loop" : "", finalOptions.autoplay ? "autoplay" : "", finalOptions.controls ? "controls" : "", finalOptions.muted ? "muted" : "", "playsinline"];
|
|||
|
|
|||
|
return Akamai.Util.trim("\n\t\t\t<div " + Akamai.Video._dataAttr + ">\n\t\t\t\t<video " + attrs.join(" ") + " poster=\"" + poster + "\" preload=\"metadata\">\n\t\t\t\t\t<source src=\"" + url + "\" " + mime + " />\n\t\t\t\t</video>\n\t\t\t</div>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImplPristine = VideoImplPristine;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
var VideoImplIm = Akamai.Util.component("VideoImplIm", function (element, options) {
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
VideoImplIm._validateOptions(finalOptions);
|
|||
|
});
|
|||
|
|
|||
|
VideoImplIm.prototype = Object.create(Akamai.VideoImpl.prototype);
|
|||
|
VideoImplIm.prototype.constructor = VideoImplIm;
|
|||
|
|
|||
|
VideoImplIm._validateOptions = function (options) {
|
|||
|
if (!Array.isArray(options.sizes) || options.sizes.length !== 3) {
|
|||
|
throw new Error('Akamai.Video sizes must have three entries');
|
|||
|
}
|
|||
|
|
|||
|
if (options.sizes.some(function (size) {
|
|||
|
return isNaN(size);
|
|||
|
})) {
|
|||
|
throw new Error('Akamai.VideoImplIm all sizes must be numbers');
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
VideoImplIm.render = function (json, options) {
|
|||
|
var finalOptions = Akamai.Util.options(Akamai.Video.defaultOptions, options);
|
|||
|
VideoImplIm._validateOptions(finalOptions);
|
|||
|
|
|||
|
var url = json.url;
|
|||
|
var poster = json.poster || "";
|
|||
|
var width;
|
|||
|
|
|||
|
var viewPortWidth = window.innerWidth;
|
|||
|
|
|||
|
if (viewPortWidth < 992) {
|
|||
|
width = finalOptions.sizes[0];
|
|||
|
} else if (viewPortWidth < 1200) {
|
|||
|
width = finalOptions.sizes[1];
|
|||
|
} else {
|
|||
|
width = finalOptions.sizes[2];
|
|||
|
}
|
|||
|
|
|||
|
var attrs = [finalOptions.loop ? "loop" : "", finalOptions.autoplay ? "autoplay" : "", finalOptions.controls ? "controls" : "", finalOptions.muted ? "muted" : "", "playsinline"];
|
|||
|
|
|||
|
var joiner = url.indexOf('?') === -1 ? '?' : '&';
|
|||
|
|
|||
|
return Akamai.Util.trim("\n\t\t\t<div " + Akamai.Video._dataAttr + ">\n\t\t\t\t<video " + attrs.join(" ") + " poster=\"" + poster + "\" preload=\"metadata\">\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=vp9&imwidth=" + width + "\" type=\"video/webm\" />\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=h265&imwidth=" + width + "\" type=\"video/mp4; codecs=hevc\" />\n\t\t\t\t\t<source src=\"" + url + joiner + "imformat=h264&imwidth=" + width + "\" type=\"video/mp4\" />\n\t\t\t\t</video>\n\t\t\t</div>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.VideoImplIm = VideoImplIm;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
/**
|
|||
|
* Video component
|
|||
|
* @class
|
|||
|
* @alias Akamai.Video
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Video = Akamai.Util.component("Video", function (element, options) {
|
|||
|
// TODO
|
|||
|
this._comp = this._$el.length && undefined;
|
|||
|
this._$videoElement = this._$el.find("video");
|
|||
|
this._videoElement = this._$videoElement[0];
|
|||
|
|
|||
|
if (!this._videoElement) {
|
|||
|
throw new Error("Akamai.Video requires a child HTML Video element");
|
|||
|
}
|
|||
|
|
|||
|
if (!this._videoElement.play || !this._videoElement.pause) {
|
|||
|
this._unsupportedAPI = true;
|
|||
|
this._unsupported();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// get the initial state (could be autoplaying on render)
|
|||
|
this._setState(this._videoElement.paused ? Video.States.Paused : Video.States.Playing);
|
|||
|
|
|||
|
// state bindings
|
|||
|
this._$el.bind("akamai-video-play", function () {
|
|||
|
this._setState(Video.States.Playing);
|
|||
|
}.bind(this)).bind("akamai-video-pause", function () {
|
|||
|
this._setState(Video.States.Paused);
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
this._createPlayButton();
|
|||
|
});
|
|||
|
|
|||
|
Video.MODE_IM = 'im';
|
|||
|
Video.MODE_PRISTINE = 'pristine';
|
|||
|
|
|||
|
Video._componentEventMapping = {
|
|||
|
"play": {
|
|||
|
to: "play",
|
|||
|
selector: "video"
|
|||
|
},
|
|||
|
|
|||
|
"pause": {
|
|||
|
to: "pause",
|
|||
|
selector: "video"
|
|||
|
},
|
|||
|
|
|||
|
"seeked": {
|
|||
|
to: "seek",
|
|||
|
selector: "video"
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._updateOptions = function (options) {
|
|||
|
if (!options) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Video.defaultOptions, this._options, "Video");
|
|||
|
|
|||
|
// TODO see Magnifier for example
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Spin360s on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} autoplay - Automatically play the video on load (default: false)
|
|||
|
* @property {Boolean} controls - Display the video controls (default: true)
|
|||
|
* @property {Boolean} loop - Restart the video when it reaches the end (default: false)
|
|||
|
* @property {Boolean} muted - Mute the video (default: true)
|
|||
|
* @property {String} mode - What video component to use. `im` will let you use any size pristine video and will automatically request & generate the right size when the page is loaded. `pristine` just passes through the original video into the video player and this is what will always play on the users device. Default: `pristine`
|
|||
|
* @property {Array} sizes - Video sizes (widths) to use for different screen widths. Defaults: [1920 (large screens), 1280 (tablet), 854 (mobile), ]
|
|||
|
*/
|
|||
|
Video.defaultOptions = {
|
|||
|
autoplay: false,
|
|||
|
controls: true,
|
|||
|
loop: false,
|
|||
|
muted: true,
|
|||
|
mode: Video.MODE_PRISTINE,
|
|||
|
sizes: [854, 1280, 1920]
|
|||
|
};
|
|||
|
|
|||
|
Video.render = function (json, options) {
|
|||
|
var impl = options && options.mode === Video.MODE_IM ? Akamai.VideoImplIm : Akamai.VideoImplPristine;
|
|||
|
return impl.render(json, options);
|
|||
|
};
|
|||
|
|
|||
|
Video.States = {
|
|||
|
Playing: 0,
|
|||
|
Paused: 1
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._unsupported = function () {
|
|||
|
if (this._unsupportedAPI) {
|
|||
|
Akamai.Util.log("Video: video API not supported", 'error');
|
|||
|
}
|
|||
|
|
|||
|
return this._unsupportedAPI;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Play the video, idempotent
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-play
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.play = function () {
|
|||
|
if (this._unsupported()) {
|
|||
|
return;
|
|||
|
}
|
|||
|
this._videoElement.play();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Pause the video
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-pause
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.pause = function () {
|
|||
|
if (this._unsupported()) {
|
|||
|
return;
|
|||
|
}
|
|||
|
this._videoElement.pause();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current state of the video
|
|||
|
* @example <caption>Video states</caption>
|
|||
|
* Akamai.Video.States = {
|
|||
|
* Playing: 0,
|
|||
|
* Paused: 1
|
|||
|
* };
|
|||
|
*
|
|||
|
* @example <caption>Conditioning on states</caption>
|
|||
|
* if( video.getState() === Akamai.Video.States.Paused ) {
|
|||
|
* video.play()
|
|||
|
* }
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Video.State}
|
|||
|
*/
|
|||
|
Video.prototype.getState = function () {
|
|||
|
return this._state;
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype._setState = function (value) {
|
|||
|
this._state = value;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Seek to the input percentage.
|
|||
|
* @method
|
|||
|
* @fires Akamai.Video#akamai-video-seek
|
|||
|
* @param {Integer} percent - value between 0 and 100 percent for seeking
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.seek = function (percent) {
|
|||
|
if (percent < 0 || 100 < percent) {
|
|||
|
throw new Error("seek takes a an integer between 0 and 100");
|
|||
|
}
|
|||
|
|
|||
|
var newTime = percent / 100 * (this._videoElement.duration || 1);
|
|||
|
this._videoElement.currentTime = newTime;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Set whether the video should loop or not
|
|||
|
* @method
|
|||
|
* @param {Boolean} value - The value true or false
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Video.prototype.setLoop = function (value) {
|
|||
|
this._videoElement.loop = value;
|
|||
|
};
|
|||
|
|
|||
|
Video.prototype.getElement = function () {
|
|||
|
return this._$el[0];
|
|||
|
};
|
|||
|
|
|||
|
// Borrowed from https://codepen.io/chrisnager/pen/jPrJgQ
|
|||
|
Video.prototype._createPlayButton = function () {
|
|||
|
var videoPlayButton;
|
|||
|
var videoWrapper = this._$el[0];
|
|||
|
var video = this._videoElement;
|
|||
|
|
|||
|
if (this._options.autoplay) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// TODO move to render?
|
|||
|
videoWrapper.insertAdjacentHTML('beforeend', "\n\t\t\t<svg viewBox=\"0 0 200 200\" alt=\"Play video\">\n\t\t\t\t<circle cx=\"100\" cy=\"100\" r=\"90\" fill=\"none\" stroke-width=\"15\" stroke=\"#fff\"/>\n\t\t\t\t<polygon points=\"70, 55 70, 145 145, 100\" fill=\"#fff\"/>\n\t\t\t</svg>\n\t\t");
|
|||
|
|
|||
|
video.removeAttribute('controls');
|
|||
|
videoPlayButton = this._$el.find('svg')[0];
|
|||
|
|
|||
|
videoPlayButton.addEventListener('click', function () {
|
|||
|
video.play();
|
|||
|
videoPlayButton.classList.add('is-hidden');
|
|||
|
if (this._options.controls) {
|
|||
|
video.setAttribute('controls', 'controls');
|
|||
|
}
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has been played. See {@link Akamai.Video#play}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-play
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has been paused. See {@link Akamai.Video#pause}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-pause
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the video has seeked to a position. See {@link Akamai.Video#seek}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-seek
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Video}.
|
|||
|
*
|
|||
|
* @event Akamai.Video#akamai-video-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Video = Video;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
/**
|
|||
|
* Image carousel
|
|||
|
* @class
|
|||
|
* @alias Akamai.Carousel
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
var Carousel = Akamai.Util.component("Carousel", function (element, options) {
|
|||
|
this._spins = Akamai.Spin360.createMany(this._$el[0], this._options.spin360);
|
|||
|
this._magnifiers = Akamai.Magnifier.createMany(this._$el[0], this._options.magnifier);
|
|||
|
this._videos = Akamai.Video.createMany(this._$el[0], this._options.video);
|
|||
|
|
|||
|
this._$el.snapper(this._options);
|
|||
|
|
|||
|
this._setAspectRatio();
|
|||
|
this._bindAspectAttributes();
|
|||
|
|
|||
|
if (this._options.slideshow.autostart) {
|
|||
|
setTimeout(function () {
|
|||
|
this.startSlideshow();
|
|||
|
}.bind(this), this._options.slideshow.interval);
|
|||
|
}
|
|||
|
|
|||
|
// NOTE !! the following two bindings must happen in order, the
|
|||
|
// autoplayables binding relies on the attribut
|
|||
|
|
|||
|
// add item attrs and maintain the active item attributes to determine the
|
|||
|
// active item state
|
|||
|
this._activeItemAttributes();
|
|||
|
this._$el.bind("akamai-carousel-goto", this._activeItemAttributes.bind(this));
|
|||
|
|
|||
|
// TODO pause autoplay videos that are not visible
|
|||
|
this._handleAutoplayables();
|
|||
|
this._$el.bind("akamai-carousel-goto", this._handleAutoplayables.bind(this));
|
|||
|
|
|||
|
this._$el.bind("tau.touch-tracking-start", function () {
|
|||
|
this._$el.find(".snapper_pane").addClass("no-scroll");
|
|||
|
}.bind(this)).bind("tau.touch-tracking-stop", function () {
|
|||
|
this._$el.find(".snapper_pane").removeClass("no-scroll");
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
// when child components are doing things, stop the carousel from
|
|||
|
// automatically advancing
|
|||
|
this._$el.bind(this.constructor._stopSlideshowEvents.join(" "), this.stopSlideshow.bind(this));
|
|||
|
});
|
|||
|
|
|||
|
Carousel._stopSlideshowEvents = ["akamai-magnifier-in", "akamai-magnifier-out", "akamai-spin360-goto", "akamai-video-play", "akamai-video-seek"];
|
|||
|
|
|||
|
// clearly there should be an autoplayable interface
|
|||
|
Carousel.prototype._handleAutoplayables = function () {
|
|||
|
var isParentActive = function (comp) {
|
|||
|
return !!$(comp.getElement()).closest("[" + this.constructor.activeItemAttr + "]").length;
|
|||
|
}.bind(this);
|
|||
|
|
|||
|
this._videos.concat(this._spins).forEach(function (comp) {
|
|||
|
if (isParentActive(comp) && comp._carouselWasPlaying) {
|
|||
|
comp.play();
|
|||
|
} else {
|
|||
|
if (comp.getState() == comp.constructor.States.Playing) {
|
|||
|
comp._carouselWasPlaying = true;
|
|||
|
comp.pause();
|
|||
|
} else {
|
|||
|
comp._carouselWasPlaying = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
// Unique counter for IDs
|
|||
|
Carousel.counter = 0;
|
|||
|
|
|||
|
Carousel.prototype._updateOptions = function (options) {
|
|||
|
if (!options) {
|
|||
|
this._options = this._originalOptions;
|
|||
|
}
|
|||
|
|
|||
|
// update the current options
|
|||
|
this._options = Akamai.Util.extend(true, this._options, options);
|
|||
|
|
|||
|
// update the options for each of the subcomponents
|
|||
|
var update = function (comp, name) {
|
|||
|
comp._updateOptions(this._options[name]);
|
|||
|
}.bind(this);
|
|||
|
|
|||
|
// push the final options down to the dom element so that CSS that keys off
|
|||
|
// of the attributes can apply when JS config is used
|
|||
|
Akamai.Util.setDataAttrOptions(this._$el, Carousel.defaultOptions, this._options, "Carousel");
|
|||
|
this._$el.snapper("updateOptions", options);
|
|||
|
|
|||
|
this._spins.forEach(function (c) {
|
|||
|
update(c, "spin360");
|
|||
|
});
|
|||
|
this._magnifiers.forEach(function (c) {
|
|||
|
update(c, "magnifier");
|
|||
|
});
|
|||
|
this._videos.forEach(function (c) {
|
|||
|
update(c, "video");
|
|||
|
});
|
|||
|
|
|||
|
this._setAspectRatio();
|
|||
|
};
|
|||
|
|
|||
|
// Used in preflight to "rename" events based on the child component events
|
|||
|
Carousel._componentEventMapping = {
|
|||
|
"snapper.after-snap": "goto",
|
|||
|
"snapper.snap": "snap",
|
|||
|
"snapper.after-next": "next",
|
|||
|
"snapper.after-prev": "previous"
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Carousels on the page
|
|||
|
* @static
|
|||
|
* @property {Boolean} arrows - Show carousel controls - (default: true)
|
|||
|
* @property {Number} aspectratio - Specify a percentage-based height for the carousel, relative to the width. Values: `false`, `100`, `45.6`, `78` - (default `false`, breakpoints supported)
|
|||
|
* @property {Integer} slideshow.interval - Time in milliseconds between slide advances - (default: 4000, breakpoints supported)
|
|||
|
* @property {Integer} slideshow.autostart - Start the slideshow on instantiation - (default: false)
|
|||
|
* @property {String} thumbnail.placement - Placement relative to the slide container: `left`, `right`, `bottom` (default: `bottom`, breakpoints supported)
|
|||
|
* @property {String} thumbnail.type - Type of thumbnail: `dots`, `none`, `images` - (default: `images`, breakpoints supported)
|
|||
|
* @property {String} thumbnail.policy - String to use for query parameter, ex: "&policy=" (default: undefined)
|
|||
|
* @property {String} thumbnail.sizes - Sizes attribute value to use if thumbnail policy is set. Values: `300px`, `200px`, `(min-width:1000px) 300px, 100px` - (default: `300px`)
|
|||
|
* @property {Object} images - Defaults to the Magnifier image option's settings
|
|||
|
*/
|
|||
|
Carousel.defaultOptions = {
|
|||
|
arrows: true,
|
|||
|
|
|||
|
aspectratio: false,
|
|||
|
|
|||
|
slideshow: {
|
|||
|
interval: 4000,
|
|||
|
autostart: false
|
|||
|
},
|
|||
|
|
|||
|
thumbnail: {
|
|||
|
placement: "bottom",
|
|||
|
type: "images",
|
|||
|
policy: undefined,
|
|||
|
sizes: "300px"
|
|||
|
},
|
|||
|
|
|||
|
images: Akamai.Image.defaultConfig
|
|||
|
};
|
|||
|
|
|||
|
Carousel.renderMapping = {
|
|||
|
image: Akamai.Magnifier,
|
|||
|
spin360: Akamai.Spin360,
|
|||
|
video: Akamai.Video
|
|||
|
};
|
|||
|
|
|||
|
Carousel._typeMapping = {
|
|||
|
image: "magnifier"
|
|||
|
};
|
|||
|
|
|||
|
Carousel._uniqueItemId = function (item, i) {
|
|||
|
return "akamai-carousel-" + Carousel.counter + "-" + item.type + "-" + i;
|
|||
|
};
|
|||
|
|
|||
|
// srcset stuff
|
|||
|
Akamai.Sourceable.extendStatic(Carousel);
|
|||
|
|
|||
|
Carousel._renderItem = function (item, i, options) {
|
|||
|
var mappedType = Carousel._typeMapping[item.type] || item.type;
|
|||
|
|
|||
|
if (!Carousel.renderMapping[item.type]) {
|
|||
|
throw new Error("\n\t\t\t\titem type " + item.type + " at index " + i + " must be 'image', 'spin360', or 'video'\n\t\t\t");
|
|||
|
}
|
|||
|
|
|||
|
return "\n\t\t\t<div class=\"snapper_item\" id=\"" + Carousel._uniqueItemId(item, i) + "\">\n\t\t\t\t" + Carousel.renderMapping[item.type].render(item, options[mappedType]) + "\n\t\t\t</div>\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
Carousel._renderThumbnailAnchor = function (item, data, i, options) {
|
|||
|
var thumbUrl = "";
|
|||
|
var altText = item.alt || "";
|
|||
|
|
|||
|
// use the video poster, the canonical url, or the first in a sequence
|
|||
|
if (item.type === 'video') {
|
|||
|
thumbUrl = item.poster || data.reduce(function (acc, i) {
|
|||
|
return acc || Carousel._thumbUrl(i);
|
|||
|
}, "");
|
|||
|
} else {
|
|||
|
thumbUrl = Carousel._thumbUrl(item);
|
|||
|
}
|
|||
|
|
|||
|
// TODO this sucks
|
|||
|
options.images.policy = options.thumbnail.policy;
|
|||
|
|
|||
|
var src = Carousel._fallbackSrc(thumbUrl, options.images);
|
|||
|
var srcset = Carousel._srcset(thumbUrl, options.images);
|
|||
|
var sizes = options.images.sizes;
|
|||
|
|
|||
|
// if there's a thumbnail policy, the thumbnails will be fresh image requests,
|
|||
|
// so they should have better sizes attribute values
|
|||
|
if (options.thumbnail.policy) {
|
|||
|
sizes = options.thumbnail.sizes;
|
|||
|
}
|
|||
|
|
|||
|
var attrs = [Carousel._dataAttr + "-thumb-type=\"" + item.type + "\"", item.type == "video" && !item.poster ? "${Carousel._dataAttr}-thumb-noposter" : ""];
|
|||
|
|
|||
|
return "\n\t\t\t<a href=\"#" + Carousel._uniqueItemId(item, i) + "\" " + attrs.join(" ") + ">\n\t\t\t\t<img src=\"" + src + "\" srcset=\"" + srcset + "\" sizes=\"" + sizes + "\" alt=\"" + altText + "\" title=\"Scroll to " + item.type + " " + i + "\" />\n\t\t\t</a>\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
Carousel._thumbUrl = function (item) {
|
|||
|
return item.type === 'video' ? item.poster : item.url || item.urls && item.urls[0];
|
|||
|
};
|
|||
|
|
|||
|
Carousel._renderThumbnails = function (data, options) {
|
|||
|
if (data.length <= 1) {
|
|||
|
return "";
|
|||
|
};
|
|||
|
|
|||
|
return "\n\t\t\t<div class=\"snapper_nav\">\n\t\t\t\t<div class=\"snapper_nav_inner\">\n\t\t\t\t\t" + Akamai.Util.map(data, function (item, i) {
|
|||
|
return Carousel._renderThumbnailAnchor(item, data, i, options);
|
|||
|
}).join("\n") + "\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t";
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._setAspectAttributes = function ($el) {
|
|||
|
var orientationAttr = Carousel._dataAttr + "-aspectratio-orientation";
|
|||
|
if ($el.height() > $el.width()) {
|
|||
|
$el.attr(orientationAttr, "portrait");
|
|||
|
} else if ($el.height() < $el.width()) {
|
|||
|
$el.attr(orientationAttr, "landscape");
|
|||
|
} else {
|
|||
|
if ($el.parent().height() >= $el.parent().width()) {
|
|||
|
$el.attr(orientationAttr, "landscape");
|
|||
|
} else {
|
|||
|
$el.attr(orientationAttr, "portrait");
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._setAspectRatio = function () {
|
|||
|
var value = this._options.aspectratio || 0;
|
|||
|
var $items = this._$el.find(".snapper_item");
|
|||
|
$items.css("padding-top", value / $items.length + "%");
|
|||
|
};
|
|||
|
|
|||
|
Carousel.prototype._bindAspectAttributes = function () {
|
|||
|
if (this._options.aspectratio === false) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var value = this._options.aspectratio || 0;
|
|||
|
var self = this;
|
|||
|
var $items = this._$el.find(".snapper_item");
|
|||
|
|
|||
|
$items.each(function () {
|
|||
|
var loadBound;
|
|||
|
var $item = $(this);
|
|||
|
|
|||
|
var $loadable = $item.find("img, video").eq(0);
|
|||
|
|
|||
|
$loadable.bind("load loadedmetadata", loadBinding = function () {
|
|||
|
clearTimeout(loadBound);
|
|||
|
|
|||
|
if ($item.is("[" + Akamai.Spin360._dataAttr + "]")) {
|
|||
|
// TODO it's not always a canvas, sometimes it's a collection of
|
|||
|
// images based on settings
|
|||
|
self._setAspectAttributes($item.find("canvas"));
|
|||
|
} else {
|
|||
|
self._setAspectAttributes($loadable);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
loadBound = setTimeout(loadBinding, 5000);
|
|||
|
});
|
|||
|
|
|||
|
$items.eq(0).find("img,video").eq(0).bind("load loadedmetadata", function () {
|
|||
|
self._trigger("first-media-load");
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
Carousel.render = function (data, options) {
|
|||
|
options = options || {};
|
|||
|
|
|||
|
// establish the extended default options for the carousel
|
|||
|
var carouselOptions = Akamai.Util.options(Carousel.defaultOptions, options);
|
|||
|
|
|||
|
var templateAttrs = ["" + Carousel._dataAttr, Carousel._dataAttr + "-item-count=\"" + (data || []).length + "\"", "data-snapper-deeplinking=\"false\"", carouselOptions.arrows ? "data-snapper-nextprev" : ""];
|
|||
|
|
|||
|
Carousel.counter++;
|
|||
|
|
|||
|
// TODO can we do something about the `enlarge_pane` class here, seems it
|
|||
|
// could be conditioned on at least one regular image type in the json
|
|||
|
// TODO options should dictate `data-snapper-nextprev`
|
|||
|
return Akamai.Util.trim("\n\t\t\t<div " + templateAttrs.join(" ") + " class=\"snapper\">\n\t\t\t\t<div class=\"snapper_nextprev_contain\">\n\t\t\t\t\t<div class=\"snapper_pane_crop\">\n\t\t\t\t\t\t<div class=\"snapper_pane enlarge_pane\">\n\t\t\t\t\t\t\t<div class=\"snapper_items\">\n\t\t\t\t\t\t\t\t" + Akamai.Util.map(data, function (item, i) {
|
|||
|
return Carousel._renderItem(item, i, carouselOptions);
|
|||
|
}).join("\n") + "\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t" + Carousel._renderThumbnails(data, carouselOptions) + "\n\t\t\t</div>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Instantiate Carousels by looking for children matching
|
|||
|
* [data-akamai-carousel] in `element` param
|
|||
|
* @method
|
|||
|
* @static
|
|||
|
* @param {HTMLElement} element - the element to search in for
|
|||
|
* @returns {Akamai.Carousel[]}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Spin360[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getSpin360s = function () {
|
|||
|
return this._spins;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Magnifier[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getMagnifiers = function () {
|
|||
|
return this._magnifiers;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @method
|
|||
|
* @returns {Akamai.Video[]}
|
|||
|
*/
|
|||
|
Carousel.prototype.getVideos = function () {
|
|||
|
return this._videos;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Go to a particular slide.
|
|||
|
*
|
|||
|
* Note that the state of the DOM on the page and thus the index of the
|
|||
|
* carousel will not be up-to-date until the {@link
|
|||
|
* Akamai.Carousel#akamai-carousel-goto} event has been fired. That is,
|
|||
|
* calling this method and the calling `getIndex` will not necessarily result
|
|||
|
* in an updated index value. Either bind to the event or supply a callback.
|
|||
|
*
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-goto
|
|||
|
* @param {Integer} index - The zero-based slide index to go to
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.goto = function (index, callback) {
|
|||
|
this._$el.snapper("goto", index, callback);
|
|||
|
};
|
|||
|
|
|||
|
Carousel._itemAttr = Carousel._dataAttr + "-item";
|
|||
|
Carousel._activeItemAttr = Carousel._itemAttr + "-active";
|
|||
|
|
|||
|
/**
|
|||
|
* Add an attribute to all carousel items,
|
|||
|
* and additionally maintain an active attribute on the active carousel item
|
|||
|
*
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype._activeItemAttributes = function () {
|
|||
|
var attrAllItems = this.constructor._itemAttr;
|
|||
|
var attrActiveItem = this.constructor._activeItemAttr;
|
|||
|
|
|||
|
this._$el.find(".snapper_item").attr(attrAllItems, true).removeAttr(attrActiveItem).eq(this.getIndex()).attr(attrActiveItem, true);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Return the current slide index
|
|||
|
* @method
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.getIndex = function () {
|
|||
|
return this._$el.snapper("getIndex");
|
|||
|
};
|
|||
|
|
|||
|
// Extend Carousel with Advanceabl interface
|
|||
|
Akamai.Advanceable.extend(Carousel);
|
|||
|
|
|||
|
/**
|
|||
|
* Advance to the next item
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-next
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.next;
|
|||
|
// required for JSDocs, implementation in Advanceable
|
|||
|
|
|||
|
/**
|
|||
|
* Retreat to the previous item
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-previous
|
|||
|
* @param {Function?} callback - callback invoked after the action has completed in the DOM
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.previous;
|
|||
|
// required for JSDocs, implementation in Advanceable
|
|||
|
|
|||
|
/**
|
|||
|
* Start automatic advancement of the carousel items
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-start-slideshow
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.startSlideshow = function () {
|
|||
|
this._$el.one("mousedown touchstart", this.stopSlideshow.bind(this));
|
|||
|
|
|||
|
this.next(function () {
|
|||
|
this._trigger("start-slideshow");
|
|||
|
this._slideshowTimer = setTimeout(function () {
|
|||
|
this.startSlideshow();
|
|||
|
|
|||
|
// TODO remove || when default options are added
|
|||
|
}.bind(this), this._options.slideshow.interval);
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Start automatic advancement of the carousel items
|
|||
|
* @method
|
|||
|
* @fires Akamai.Carousel#akamai-carousel-stop-slideshow
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Carousel.prototype.stopSlideshow = function () {
|
|||
|
clearTimeout(this._slideshowTimer);
|
|||
|
this._slideshowTimer = undefined;
|
|||
|
this._trigger("stop-slideshow");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#goto}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-goto
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#next}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-next
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has completed it transition to
|
|||
|
* a new index due to user interaction or a call to {@link Akamai.Carousel#previous}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-previous
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has started the slide show due to a call to
|
|||
|
* {@link Akamai.Carousel#startSlideshow}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-start-slideshow
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the carousel has stoped the slide show due to a call to
|
|||
|
* {@link Akamai.Carousel#stopSlideshow}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-stop-slideshow
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Carousel}.
|
|||
|
*
|
|||
|
* @event Akamai.Carousel#akamai-carousel-init
|
|||
|
*/
|
|||
|
|
|||
|
Carousel.prototype.refresh = function () {
|
|||
|
this._$el.snapper("updateWidths");
|
|||
|
};
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Carousel = Carousel;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
/**
|
|||
|
* Image Fullscreen
|
|||
|
* @class
|
|||
|
* @alias Akamai.Fullscreen
|
|||
|
* @param {HTMLElement} element - the DOM element representing the component markup
|
|||
|
* @param {Object} options - configuration options
|
|||
|
*/
|
|||
|
|
|||
|
// TODO JSDocs
|
|||
|
// TODO Tests
|
|||
|
var Fullscreen = Akamai.Util.component("Fullscreen", function (element, options) {
|
|||
|
if (this._options.enabled) {
|
|||
|
this._init();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
Fullscreen.prototype._init = function () {
|
|||
|
this._$fullscreen = this._$el;
|
|||
|
|
|||
|
// parent container does not allow widths to be set (Firefox, fullscreen), so 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("\n\t\t\t<button " + Fullscreen.attr.enterBtn + " class=\"" + Fullscreen.classes.btn + " " + Fullscreen.classes.enterBtn + " icon-fullscreen\">Full Screen</button>\n\t\t\t<button " + Fullscreen.attr.exitBtn + " class=\"" + Fullscreen.classes.btn + " " + Fullscreen.classes.exitBtn + " icon-close-light\">Exit Full Screen</button>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen._keyLookup = [{
|
|||
|
enter: "requestFullscreen",
|
|||
|
exit: "exitFullscreen",
|
|||
|
element: "fullscreenElement",
|
|||
|
onchange: "fullscreenchange"
|
|||
|
}, {
|
|||
|
enter: "webkitRequestFullscreen",
|
|||
|
exit: "webkitExitFullscreen",
|
|||
|
element: "webkitFullscreenElement",
|
|||
|
onchange: "webkitfullscreenchange"
|
|||
|
}, {
|
|||
|
enter: "webkitRequestFullScreen",
|
|||
|
exit: "webkitCancelFullScreen",
|
|||
|
element: "webkitCurrentFullScreenElement",
|
|||
|
onchange: "webkitfullscreenchange"
|
|||
|
}, {
|
|||
|
enter: "mozRequestFullScreen",
|
|||
|
exit: "mozCancelFullScreen",
|
|||
|
element: "mozFullScreenElement",
|
|||
|
onchange: "mozfullscreenchange"
|
|||
|
}, {
|
|||
|
enter: "msRequestFullscreen",
|
|||
|
exit: "msExitFullscreen",
|
|||
|
element: "msFullscreenElement",
|
|||
|
onchange: "MSFullscreenChange"
|
|||
|
}];
|
|||
|
|
|||
|
Fullscreen._keys = function (el) {
|
|||
|
var keys = Fullscreen._keyLookup;
|
|||
|
|
|||
|
var el = document.body;
|
|||
|
for (var j = 0, k = keys.length; j < k; j++) {
|
|||
|
if (keys[j].enter in el) {
|
|||
|
return keys[j];
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._maximizePlacement = function () {
|
|||
|
this._fullscreenFallbackEnabled = true;
|
|||
|
this._$fullscreen.addClass(Fullscreen.classes.fallback);
|
|||
|
this._$placeholder.insertAfter(this._$fullscreen);
|
|||
|
this._$fullscreen.appendTo(document.body);
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._restorePlacement = function () {
|
|||
|
if (!this._fullscreenFallbackEnabled) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this._fullscreenFallbackEnabled = false;
|
|||
|
this._$fullscreen.removeClass(Fullscreen.classes.fallback);
|
|||
|
this._$fullscreen.insertAfter(this._$placeholder);
|
|||
|
this._$placeholder.remove();
|
|||
|
};
|
|||
|
|
|||
|
Fullscreen.prototype._adjustWidth = function () {
|
|||
|
this._$fullscreen.css("width", "auto !important");
|
|||
|
// wish this could go into the change event above, but alas the dimensions 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:\n\n" + (hostnames.length ? hostnames.join("\n") : "No hostnames") + "\n");
|
|||
|
}
|
|||
|
|
|||
|
return val;
|
|||
|
}.bind(this));
|
|||
|
};
|
|||
|
|
|||
|
Viewer.prototype._urlHostnameMatch = function (url) {
|
|||
|
var parser = document.createElement('a');
|
|||
|
parser.href = url;
|
|||
|
|
|||
|
return parser.hostname === "" || parser.hostname === window.location.hostname || this._options.items.hostnames.indexOf(parser.hostname) >= 0;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Defines the global default options for all Viewers on the page
|
|||
|
* @static
|
|||
|
* @property {Object} breakpoints - configuration changes for child components at breakpoints (no default)
|
|||
|
* @property {Object} fullscreenBreakpoints - configuration changes for child components at breakpoints when in fullscreen mode (no default)
|
|||
|
* @property {Object[]} items.data - array of items from the Akamai JSON (default: undefined)
|
|||
|
* @property {String} items.defaultTag - (default: "akamai-untagged")
|
|||
|
* @property {String[]} items.hostnames - whitelist of URL hostnames to check for in JSON, (default: empty array)
|
|||
|
* @property {Integer} items.limit - size limit of JSON data in kibibytes (default: 100)
|
|||
|
* @property {String} items.renderAll - (default: false)
|
|||
|
* @property {String[]} items.tags - Set of tags to match against the Akamai JSON data (default: undefined)
|
|||
|
* @property {String} items.uri - URI at which to retrieve the Akamai JSON (default: undefined)
|
|||
|
* @property {Object} carousel - child {@link Akamai.Carousel} options
|
|||
|
* @property {Object} magnifier - child {@link Akamai.Magnifier} options
|
|||
|
* @property {Object} spin360 - child {@link Akamai.Spin360} options
|
|||
|
* @property {Object} video - child {@link Akamai.Video} options
|
|||
|
* @property {Object} fullscreen - child {@link Akamai.Fullscreen} options
|
|||
|
*/
|
|||
|
Viewer.defaultOptions = {
|
|||
|
breakpoints: {},
|
|||
|
items: {
|
|||
|
data: undefined,
|
|||
|
defaultTag: "akamai-untagged",
|
|||
|
hostnames: [],
|
|||
|
limit: 100,
|
|||
|
renderAll: false,
|
|||
|
tags: undefined,
|
|||
|
uri: undefined
|
|||
|
},
|
|||
|
carousel: Akamai.Carousel.defaultOptions,
|
|||
|
magnifier: Akamai.Magnifier.defaultOptions,
|
|||
|
spin360: Akamai.Spin360.defaultOptions,
|
|||
|
video: Akamai.Video.defaultOptions,
|
|||
|
fullscreen: Akamai.Fullscreen.defaultOptions
|
|||
|
};
|
|||
|
|
|||
|
Viewer._tagSplit = function (data, options) {
|
|||
|
var items = options;
|
|||
|
|
|||
|
// set all items without a tag to the default
|
|||
|
data = data.map(function (item) {
|
|||
|
item.tags = item.tags && item.tags.length ? item.tags : [items.defaultTag];
|
|||
|
return item;
|
|||
|
});
|
|||
|
|
|||
|
// if the tags option was set, filter items out that don't match
|
|||
|
if (items.tags) {
|
|||
|
data = items.data.filter(function (item) {
|
|||
|
return item.tags.reduce(function (acc, tag) {
|
|||
|
return acc || items.tags.indexOf(tag) >= 0;
|
|||
|
}, false);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// from the filtered get the first (may be default tag)
|
|||
|
var def = data[0].tags[0];
|
|||
|
|
|||
|
// create a mapping from tags to items in the set
|
|||
|
var tagMapping = data.reduce(function (acc, item) {
|
|||
|
(item.tags || []).forEach(function (tag) {
|
|||
|
acc[tag] = acc[tag] || [];
|
|||
|
acc[tag].push(item);
|
|||
|
});
|
|||
|
|
|||
|
return acc;
|
|||
|
}, {});
|
|||
|
|
|||
|
// set the first (default) object to the first tag
|
|||
|
tagMapping[Viewer._firstRenderTag] = tagMapping[def];
|
|||
|
|
|||
|
return tagMapping;
|
|||
|
};
|
|||
|
|
|||
|
Viewer._firstRenderTag = "akamai-first-render";
|
|||
|
Viewer._tagAttr = Viewer._dataAttr + "-tag";
|
|||
|
|
|||
|
Viewer._renderTag = function (data, options) {
|
|||
|
// get the first item and it's tag
|
|||
|
// NOTE this assumes that the data has been normalized to have a default tag
|
|||
|
var tag = data[0].tags[0];
|
|||
|
|
|||
|
// TODO shoestring doesn't treat html correctly unless the leading `<` has
|
|||
|
// no whitepsace in front of it:
|
|||
|
// shoestring/issues/94
|
|||
|
return Akamai.Util.trim("\n\t\t\t<div " + Viewer._tagAttr + "=\"" + tag + "\">\n\t\t\t\t" + Akamai.Carousel.render(data, options) + "\n\t\t\t</div>\n\t\t");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Takes standard options including an `items` attribute and renders HTML
|
|||
|
* that conforms to the component expectations
|
|||
|
* @method
|
|||
|
* @static
|
|||
|
* @params {Object[]} json - Akamai JSON data
|
|||
|
* @params {Object} options - options object with an `items` attribute, see default options
|
|||
|
* @params {Boolean} options.items - configuration for how the items should be
|
|||
|
* rendered, see {@link Akamai.Viewer.defaultOptions} default options items sub-configuration
|
|||
|
* @returns { String }
|
|||
|
*/
|
|||
|
Viewer.render = function (json, options, unwrapped) {
|
|||
|
// NOTE we do not extend the default options here because it has no bearing
|
|||
|
// on the rendering of the Viewer markup or child markup. This is contrast
|
|||
|
// to the carousel which does extend the passed options with defaults so the
|
|||
|
// markup can make use of them
|
|||
|
var data = Viewer._tagSplit(json, options.items);
|
|||
|
var carouselOptions = Viewer._extendCarouselOptions(options);
|
|||
|
|
|||
|
var childMarkup;
|
|||
|
|
|||
|
if (options.items.renderAll) {
|
|||
|
childMarkup = data.map(function (acc, datum) {
|
|||
|
return Viewer._renderTag(datum, carouselOptions);
|
|||
|
}).join(" ");
|
|||
|
} else {
|
|||
|
childMarkup = Viewer._renderTag(data[Viewer._firstRenderTag], carouselOptions);
|
|||
|
}
|
|||
|
|
|||
|
var wrapped = "\n\t\t\t<div " + Viewer._dataAttr + ">\n\t\t\t\t" + childMarkup + "\n\t\t\t</div>\n\t\t";
|
|||
|
|
|||
|
return unwrapped ? childMarkup : wrapped;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Accessor for child carousel components.
|
|||
|
* @method
|
|||
|
* @returns { Akamai.Carousel[] }
|
|||
|
*/
|
|||
|
Viewer.prototype.getCarousels = function () {
|
|||
|
return this._carousels;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Set the visible carousel based on the tag. Note the method activity does
|
|||
|
* not complete until all the images from the relevant viewer have loaded, use
|
|||
|
* the `akamai-viewer-switch-tag` event
|
|||
|
* @method
|
|||
|
* @fires Akamai.Viewer#akamai-viewer-switch-tag
|
|||
|
* @params {String} tag - The tag corresponding to the desired carousel
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
Viewer.prototype.switchTag = function (tag) {
|
|||
|
var tags = this.getTags();
|
|||
|
|
|||
|
// if the passed tag isn't in the data set return early and log an error in
|
|||
|
// the console
|
|||
|
if (tags.indexOf(tag) == -1) {
|
|||
|
Akamai.Util.log("tag: " + tag + " is not present in the data for this viewer", 'error');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var selector = "[" + this.constructor._tagAttr + "=\"" + tag + "\"]";
|
|||
|
|
|||
|
// try to find an existing element with the tag
|
|||
|
var $taggedViewer = this._$el.find(selector);
|
|||
|
|
|||
|
// if there's no element append the newly rendered tag markup
|
|||
|
if (!$taggedViewer.length) {
|
|||
|
// make sure the config takes into account the current breakpoint
|
|||
|
var options = this._resolveBreakpointOptions();
|
|||
|
var carouselOptions = Viewer._extendCarouselOptions(options);
|
|||
|
|
|||
|
// create the markup that will be inserted
|
|||
|
var $markup = $(this.constructor._renderTag(this._tagMapping[tag], carouselOptions, true));
|
|||
|
var $imgs = $markup.find("img");
|
|||
|
var imgCount = $imgs.length;
|
|||
|
var loaded = 0;
|
|||
|
|
|||
|
// hide the new carousel initially
|
|||
|
$markup.css("display", "none");
|
|||
|
|
|||
|
$imgs.bind("load", function () {
|
|||
|
if (++loaded !== imgCount) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// TODO namespace?
|
|||
|
$imgs.unbind("load");
|
|||
|
|
|||
|
var carousels = Akamai.Carousel.createMany($markup[0], carouselOptions);
|
|||
|
|
|||
|
carousels.forEach(function (c) {
|
|||
|
// TODO figure out why the carousel constructor doesn't apply the
|
|||
|
// active index class we want on instantiation, likely due to the fact
|
|||
|
// that the carousel is hidden so the "getIndex" calc is broken
|
|||
|
// mark the carousel item as active
|
|||
|
c.goto(c.getIndex());
|
|||
|
});
|
|||
|
|
|||
|
// create and store the new carousels (should be one)
|
|||
|
this._carousels = this._carousels.concat(carousels);
|
|||
|
this._showViewer($markup, carousels);
|
|||
|
}.bind(this));
|
|||
|
|
|||
|
// append the new markup to the existing viewer
|
|||
|
this._$el.append($markup);
|
|||
|
} else {
|
|||
|
this._showViewer($taggedViewer);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// TODO should be handled in CSS
|
|||
|
Viewer.prototype._showViewer = function ($viewer, carousels) {
|
|||
|
this._$el.find("[" + this.constructor._tagAttr + "]").css("display", "none").removeClass("focused");
|
|||
|
|
|||
|
$viewer.css("display", "block").addClass("focused");
|
|||
|
|
|||
|
if (carousels) {
|
|||
|
carousels.map(function (c) {
|
|||
|
c.refresh();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// TODO the placement here seems arbitrary, probably belongs in `_showViewer`
|
|||
|
// TODO also sucks to be so tightly coupled
|
|||
|
this._fullscreen.addButton();
|
|||
|
|
|||
|
this._trigger("switch-tag");
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Accessor for tags derived from Akamai JSON data
|
|||
|
* @method
|
|||
|
* @returns { String[] }
|
|||
|
*/
|
|||
|
Viewer.prototype.getTags = function () {
|
|||
|
if (this._tags) {
|
|||
|
return this._tags;
|
|||
|
};
|
|||
|
|
|||
|
var tags = [];
|
|||
|
|
|||
|
// map and store all the carousel tags
|
|||
|
for (var tag in this._tagMapping) {
|
|||
|
if (this._tagMapping.hasOwnProperty(tag) && tag !== Viewer._firstRenderTag) {
|
|||
|
// otherwise grab the list of tags
|
|||
|
tags.push(tag);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this._tags = tags;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when the viewer switches tag views. This includes waiting for
|
|||
|
* images to load for carousels dedicated to previously unviewed tags.
|
|||
|
* {@link Akamai.Viewer#switchTag}.
|
|||
|
*
|
|||
|
* @event Akamai.Viewer#akamai-viewer-switch-tag
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Triggered when initialization finishes
|
|||
|
* {@link Akamai.Viewer}.
|
|||
|
*
|
|||
|
* @event Akamai.Viewer#akamai-viewer-init
|
|||
|
*/
|
|||
|
|
|||
|
exports.Akamai = exports.Akamai || {};
|
|||
|
exports.Akamai.Viewer = Viewer;
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (exports, $) {
|
|||
|
$.fn.akamaiViewer = function (options) {
|
|||
|
this.each(function (i, element) {
|
|||
|
new Akamai.Viewer(element, options);
|
|||
|
});
|
|||
|
};
|
|||
|
})(typeof exports === 'undefined' ? window : exports, this.jQuery);
|
|||
|
|
|||
|
(function (f) {
|
|||
|
if (typeof exports === "object" && typeof module !== "undefined") {
|
|||
|
module.exports = f();
|
|||
|
} else if (typeof define === "function" && define.amd) {
|
|||
|
define([], f);
|
|||
|
} else {
|
|||
|
var g;if (typeof window !== "undefined") {
|
|||
|
g = window;
|
|||
|
} else if (typeof global !== "undefined") {
|
|||
|
g = global;
|
|||
|
} else if (typeof self !== "undefined") {
|
|||
|
g = self;
|
|||
|
} else {
|
|||
|
g = this;
|
|||
|
}g.Clipboard = f();
|
|||
|
}
|
|||
|
})(function () {
|
|||
|
var define, module, exports;return function e(t, n, r) {
|
|||
|
function s(o, u) {
|
|||
|
if (!n[o]) {
|
|||
|
if (!t[o]) {
|
|||
|
var a = typeof require == "function" && require;if (!u && a) return a(o, !0);if (i) return i(o, !0);var f = new Error("Cannot find module '" + o + "'");throw f.code = "MODULE_NOT_FOUND", f;
|
|||
|
}var l = n[o] = { exports: {} };t[o][0].call(l.exports, function (e) {
|
|||
|
var n = t[o][1][e];return s(n ? n : e);
|
|||
|
}, l, l.exports, e, t, n, r);
|
|||
|
}return n[o].exports;
|
|||
|
}var i = typeof require == "function" && require;for (var o = 0; o < r.length; o++) s(r[o]);return s;
|
|||
|
}({ 1: [function (require, module, exports) {
|
|||
|
var DOCUMENT_NODE_TYPE = 9;
|
|||
|
|
|||
|
/**
|
|||
|
* A polyfill for Element.matches()
|
|||
|
*/
|
|||
|
if (typeof Element !== 'undefined' && !Element.prototype.matches) {
|
|||
|
var proto = Element.prototype;
|
|||
|
|
|||
|
proto.matches = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || proto.oMatchesSelector || proto.webkitMatchesSelector;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Finds the closest parent that matches a selector.
|
|||
|
*
|
|||
|
* @param {Element} element
|
|||
|
* @param {String} selector
|
|||
|
* @return {Function}
|
|||
|
*/
|
|||
|
function closest(element, selector) {
|
|||
|
while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {
|
|||
|
if (typeof element.matches === 'function' && element.matches(selector)) {
|
|||
|
return element;
|
|||
|
}
|
|||
|
element = element.parentNode;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
module.exports = closest;
|
|||
|
}, {}], 2: [function (require, module, exports) {
|
|||
|
var closest = require('./closest');
|
|||
|
|
|||
|
/**
|
|||
|
* Delegates event to a selector.
|
|||
|
*
|
|||
|
* @param {Element} element
|
|||
|
* @param {String} selector
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @param {Boolean} useCapture
|
|||
|
* @return {Object}
|
|||
|
*/
|
|||
|
function delegate(element, selector, type, callback, useCapture) {
|
|||
|
var listenerFn = listener.apply(this, arguments);
|
|||
|
|
|||
|
element.addEventListener(type, listenerFn, useCapture);
|
|||
|
|
|||
|
return {
|
|||
|
destroy: function () {
|
|||
|
element.removeEventListener(type, listenerFn, useCapture);
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Finds closest match and invokes callback.
|
|||
|
*
|
|||
|
* @param {Element} element
|
|||
|
* @param {String} selector
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @return {Function}
|
|||
|
*/
|
|||
|
function listener(element, selector, type, callback) {
|
|||
|
return function (e) {
|
|||
|
e.delegateTarget = closest(e.target, selector);
|
|||
|
|
|||
|
if (e.delegateTarget) {
|
|||
|
callback.call(element, e);
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
module.exports = delegate;
|
|||
|
}, { "./closest": 1 }], 3: [function (require, module, exports) {
|
|||
|
/**
|
|||
|
* Check if argument is a HTML element.
|
|||
|
*
|
|||
|
* @param {Object} value
|
|||
|
* @return {Boolean}
|
|||
|
*/
|
|||
|
exports.node = function (value) {
|
|||
|
return value !== undefined && value instanceof HTMLElement && value.nodeType === 1;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Check if argument is a list of HTML elements.
|
|||
|
*
|
|||
|
* @param {Object} value
|
|||
|
* @return {Boolean}
|
|||
|
*/
|
|||
|
exports.nodeList = function (value) {
|
|||
|
var type = Object.prototype.toString.call(value);
|
|||
|
|
|||
|
return value !== undefined && (type === '[object NodeList]' || type === '[object HTMLCollection]') && 'length' in value && (value.length === 0 || exports.node(value[0]));
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Check if argument is a string.
|
|||
|
*
|
|||
|
* @param {Object} value
|
|||
|
* @return {Boolean}
|
|||
|
*/
|
|||
|
exports.string = function (value) {
|
|||
|
return typeof value === 'string' || value instanceof String;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Check if argument is a function.
|
|||
|
*
|
|||
|
* @param {Object} value
|
|||
|
* @return {Boolean}
|
|||
|
*/
|
|||
|
exports.fn = function (value) {
|
|||
|
var type = Object.prototype.toString.call(value);
|
|||
|
|
|||
|
return type === '[object Function]';
|
|||
|
};
|
|||
|
}, {}], 4: [function (require, module, exports) {
|
|||
|
var is = require('./is');
|
|||
|
var delegate = require('delegate');
|
|||
|
|
|||
|
/**
|
|||
|
* Validates all params and calls the right
|
|||
|
* listener function based on its target type.
|
|||
|
*
|
|||
|
* @param {String|HTMLElement|HTMLCollection|NodeList} target
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @return {Object}
|
|||
|
*/
|
|||
|
function listen(target, type, callback) {
|
|||
|
if (!target && !type && !callback) {
|
|||
|
throw new Error('Missing required arguments');
|
|||
|
}
|
|||
|
|
|||
|
if (!is.string(type)) {
|
|||
|
throw new TypeError('Second argument must be a String');
|
|||
|
}
|
|||
|
|
|||
|
if (!is.fn(callback)) {
|
|||
|
throw new TypeError('Third argument must be a Function');
|
|||
|
}
|
|||
|
|
|||
|
if (is.node(target)) {
|
|||
|
return listenNode(target, type, callback);
|
|||
|
} else if (is.nodeList(target)) {
|
|||
|
return listenNodeList(target, type, callback);
|
|||
|
} else if (is.string(target)) {
|
|||
|
return listenSelector(target, type, callback);
|
|||
|
} else {
|
|||
|
throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Adds an event listener to a HTML element
|
|||
|
* and returns a remove listener function.
|
|||
|
*
|
|||
|
* @param {HTMLElement} node
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @return {Object}
|
|||
|
*/
|
|||
|
function listenNode(node, type, callback) {
|
|||
|
node.addEventListener(type, callback);
|
|||
|
|
|||
|
return {
|
|||
|
destroy: function () {
|
|||
|
node.removeEventListener(type, callback);
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Add an event listener to a list of HTML elements
|
|||
|
* and returns a remove listener function.
|
|||
|
*
|
|||
|
* @param {NodeList|HTMLCollection} nodeList
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @return {Object}
|
|||
|
*/
|
|||
|
function listenNodeList(nodeList, type, callback) {
|
|||
|
Array.prototype.forEach.call(nodeList, function (node) {
|
|||
|
node.addEventListener(type, callback);
|
|||
|
});
|
|||
|
|
|||
|
return {
|
|||
|
destroy: function () {
|
|||
|
Array.prototype.forEach.call(nodeList, function (node) {
|
|||
|
node.removeEventListener(type, callback);
|
|||
|
});
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Add an event listener to a selector
|
|||
|
* and returns a remove listener function.
|
|||
|
*
|
|||
|
* @param {String} selector
|
|||
|
* @param {String} type
|
|||
|
* @param {Function} callback
|
|||
|
* @return {Object}
|
|||
|
*/
|
|||
|
function listenSelector(selector, type, callback) {
|
|||
|
return delegate(document.body, selector, type, callback);
|
|||
|
}
|
|||
|
|
|||
|
module.exports = listen;
|
|||
|
}, { "./is": 3, "delegate": 2 }], 5: [function (require, module, exports) {
|
|||
|
function select(element) {
|
|||
|
var selectedText;
|
|||
|
|
|||
|
if (element.nodeName === 'SELECT') {
|
|||
|
element.focus();
|
|||
|
|
|||
|
selectedText = element.value;
|
|||
|
} else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
|||
|
var isReadOnly = element.hasAttribute('readonly');
|
|||
|
|
|||
|
if (!isReadOnly) {
|
|||
|
element.setAttribute('readonly', '');
|
|||
|
}
|
|||
|
|
|||
|
element.select();
|
|||
|
element.setSelectionRange(0, element.value.length);
|
|||
|
|
|||
|
if (!isReadOnly) {
|
|||
|
element.removeAttribute('readonly');
|
|||
|
}
|
|||
|
|
|||
|
selectedText = element.value;
|
|||
|
} else {
|
|||
|
if (element.hasAttribute('contenteditable')) {
|
|||
|
element.focus();
|
|||
|
}
|
|||
|
|
|||
|
var selection = window.getSelection();
|
|||
|
var range = document.createRange();
|
|||
|
|
|||
|
range.selectNodeContents(element);
|
|||
|
selection.removeAllRanges();
|
|||
|
selection.addRange(range);
|
|||
|
|
|||
|
selectedText = selection.toString();
|
|||
|
}
|
|||
|
|
|||
|
return selectedText;
|
|||
|
}
|
|||
|
|
|||
|
module.exports = select;
|
|||
|
}, {}], 6: [function (require, module, exports) {
|
|||
|
function E() {
|
|||
|
// Keep this empty so it's easier to inherit from
|
|||
|
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
|
|||
|
}
|
|||
|
|
|||
|
E.prototype = {
|
|||
|
on: function (name, callback, ctx) {
|
|||
|
var e = this.e || (this.e = {});
|
|||
|
|
|||
|
(e[name] || (e[name] = [])).push({
|
|||
|
fn: callback,
|
|||
|
ctx: ctx
|
|||
|
});
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
once: function (name, callback, ctx) {
|
|||
|
var self = this;
|
|||
|
function listener() {
|
|||
|
self.off(name, listener);
|
|||
|
callback.apply(ctx, arguments);
|
|||
|
};
|
|||
|
|
|||
|
listener._ = callback;
|
|||
|
return this.on(name, listener, ctx);
|
|||
|
},
|
|||
|
|
|||
|
emit: function (name) {
|
|||
|
var data = [].slice.call(arguments, 1);
|
|||
|
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
|
|||
|
var i = 0;
|
|||
|
var len = evtArr.length;
|
|||
|
|
|||
|
for (i; i < len; i++) {
|
|||
|
evtArr[i].fn.apply(evtArr[i].ctx, data);
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
off: function (name, callback) {
|
|||
|
var e = this.e || (this.e = {});
|
|||
|
var evts = e[name];
|
|||
|
var liveEvents = [];
|
|||
|
|
|||
|
if (evts && callback) {
|
|||
|
for (var i = 0, len = evts.length; i < len; i++) {
|
|||
|
if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Remove event from queue to prevent memory leak
|
|||
|
// Suggested by https://github.com/lazd
|
|||
|
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
|
|||
|
|
|||
|
liveEvents.length ? e[name] = liveEvents : delete e[name];
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
module.exports = E;
|
|||
|
}, {}], 7: [function (require, module, exports) {
|
|||
|
(function (global, factory) {
|
|||
|
if (typeof define === "function" && define.amd) {
|
|||
|
define(['module', 'select'], factory);
|
|||
|
} else if (typeof exports !== "undefined") {
|
|||
|
factory(module, require('select'));
|
|||
|
} else {
|
|||
|
var mod = {
|
|||
|
exports: {}
|
|||
|
};
|
|||
|
factory(mod, global.select);
|
|||
|
global.clipboardAction = mod.exports;
|
|||
|
}
|
|||
|
})(this, function (module, _select) {
|
|||
|
'use strict';
|
|||
|
|
|||
|
var _select2 = _interopRequireDefault(_select);
|
|||
|
|
|||
|
function _interopRequireDefault(obj) {
|
|||
|
return obj && obj.__esModule ? obj : {
|
|||
|
default: obj
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
|
|||
|
return typeof obj;
|
|||
|
} : function (obj) {
|
|||
|
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
|||
|
};
|
|||
|
|
|||
|
function _classCallCheck(instance, Constructor) {
|
|||
|
if (!(instance instanceof Constructor)) {
|
|||
|
throw new TypeError("Cannot call a class as a function");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _createClass = function () {
|
|||
|
function defineProperties(target, props) {
|
|||
|
for (var i = 0; i < props.length; i++) {
|
|||
|
var descriptor = props[i];
|
|||
|
descriptor.enumerable = descriptor.enumerable || false;
|
|||
|
descriptor.configurable = true;
|
|||
|
if ("value" in descriptor) descriptor.writable = true;
|
|||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return function (Constructor, protoProps, staticProps) {
|
|||
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
|||
|
if (staticProps) defineProperties(Constructor, staticProps);
|
|||
|
return Constructor;
|
|||
|
};
|
|||
|
}();
|
|||
|
|
|||
|
var ClipboardAction = function () {
|
|||
|
/**
|
|||
|
* @param {Object} options
|
|||
|
*/
|
|||
|
function ClipboardAction(options) {
|
|||
|
_classCallCheck(this, ClipboardAction);
|
|||
|
|
|||
|
this.resolveOptions(options);
|
|||
|
this.initSelection();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Defines base properties passed from constructor.
|
|||
|
* @param {Object} options
|
|||
|
*/
|
|||
|
|
|||
|
_createClass(ClipboardAction, [{
|
|||
|
key: 'resolveOptions',
|
|||
|
value: function resolveOptions() {
|
|||
|
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|||
|
|
|||
|
this.action = options.action;
|
|||
|
this.container = options.container;
|
|||
|
this.emitter = options.emitter;
|
|||
|
this.target = options.target;
|
|||
|
this.text = options.text;
|
|||
|
this.trigger = options.trigger;
|
|||
|
|
|||
|
this.selectedText = '';
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'initSelection',
|
|||
|
value: function initSelection() {
|
|||
|
if (this.text) {
|
|||
|
this.selectFake();
|
|||
|
} else if (this.target) {
|
|||
|
this.selectTarget();
|
|||
|
}
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'selectFake',
|
|||
|
value: function selectFake() {
|
|||
|
var _this = this;
|
|||
|
|
|||
|
var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
|
|||
|
|
|||
|
this.removeFake();
|
|||
|
|
|||
|
this.fakeHandlerCallback = function () {
|
|||
|
return _this.removeFake();
|
|||
|
};
|
|||
|
this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;
|
|||
|
|
|||
|
this.fakeElem = document.createElement('textarea');
|
|||
|
// Prevent zooming on iOS
|
|||
|
this.fakeElem.style.fontSize = '12pt';
|
|||
|
// Reset box model
|
|||
|
this.fakeElem.style.border = '0';
|
|||
|
this.fakeElem.style.padding = '0';
|
|||
|
this.fakeElem.style.margin = '0';
|
|||
|
// Move element out of screen horizontally
|
|||
|
this.fakeElem.style.position = 'absolute';
|
|||
|
this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
|
|||
|
// Move element to the same position vertically
|
|||
|
var yPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|||
|
this.fakeElem.style.top = yPosition + 'px';
|
|||
|
|
|||
|
this.fakeElem.setAttribute('readonly', '');
|
|||
|
this.fakeElem.value = this.text;
|
|||
|
|
|||
|
this.container.appendChild(this.fakeElem);
|
|||
|
|
|||
|
this.selectedText = (0, _select2.default)(this.fakeElem);
|
|||
|
this.copyText();
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'removeFake',
|
|||
|
value: function removeFake() {
|
|||
|
if (this.fakeHandler) {
|
|||
|
this.container.removeEventListener('click', this.fakeHandlerCallback);
|
|||
|
this.fakeHandler = null;
|
|||
|
this.fakeHandlerCallback = null;
|
|||
|
}
|
|||
|
|
|||
|
if (this.fakeElem) {
|
|||
|
this.container.removeChild(this.fakeElem);
|
|||
|
this.fakeElem = null;
|
|||
|
}
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'selectTarget',
|
|||
|
value: function selectTarget() {
|
|||
|
this.selectedText = (0, _select2.default)(this.target);
|
|||
|
this.copyText();
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'copyText',
|
|||
|
value: function copyText() {
|
|||
|
var succeeded = void 0;
|
|||
|
|
|||
|
try {
|
|||
|
succeeded = document.execCommand(this.action);
|
|||
|
} catch (err) {
|
|||
|
succeeded = false;
|
|||
|
}
|
|||
|
|
|||
|
this.handleResult(succeeded);
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'handleResult',
|
|||
|
value: function handleResult(succeeded) {
|
|||
|
this.emitter.emit(succeeded ? 'success' : 'error', {
|
|||
|
action: this.action,
|
|||
|
text: this.selectedText,
|
|||
|
trigger: this.trigger,
|
|||
|
clearSelection: this.clearSelection.bind(this)
|
|||
|
});
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'clearSelection',
|
|||
|
value: function clearSelection() {
|
|||
|
if (this.trigger) {
|
|||
|
this.trigger.focus();
|
|||
|
}
|
|||
|
|
|||
|
window.getSelection().removeAllRanges();
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'destroy',
|
|||
|
value: function destroy() {
|
|||
|
this.removeFake();
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'action',
|
|||
|
set: function set() {
|
|||
|
var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy';
|
|||
|
|
|||
|
this._action = action;
|
|||
|
|
|||
|
if (this._action !== 'copy' && this._action !== 'cut') {
|
|||
|
throw new Error('Invalid "action" value, use either "copy" or "cut"');
|
|||
|
}
|
|||
|
},
|
|||
|
get: function get() {
|
|||
|
return this._action;
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'target',
|
|||
|
set: function set(target) {
|
|||
|
if (target !== undefined) {
|
|||
|
if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {
|
|||
|
if (this.action === 'copy' && target.hasAttribute('disabled')) {
|
|||
|
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
|
|||
|
}
|
|||
|
|
|||
|
if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
|
|||
|
throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
|
|||
|
}
|
|||
|
|
|||
|
this._target = target;
|
|||
|
} else {
|
|||
|
throw new Error('Invalid "target" value, use a valid Element');
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
get: function get() {
|
|||
|
return this._target;
|
|||
|
}
|
|||
|
}]);
|
|||
|
|
|||
|
return ClipboardAction;
|
|||
|
}();
|
|||
|
|
|||
|
module.exports = ClipboardAction;
|
|||
|
});
|
|||
|
}, { "select": 5 }], 8: [function (require, module, exports) {
|
|||
|
(function (global, factory) {
|
|||
|
if (typeof define === "function" && define.amd) {
|
|||
|
define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory);
|
|||
|
} else if (typeof exports !== "undefined") {
|
|||
|
factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener'));
|
|||
|
} else {
|
|||
|
var mod = {
|
|||
|
exports: {}
|
|||
|
};
|
|||
|
factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener);
|
|||
|
global.clipboard = mod.exports;
|
|||
|
}
|
|||
|
})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) {
|
|||
|
'use strict';
|
|||
|
|
|||
|
var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
|
|||
|
|
|||
|
var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
|
|||
|
|
|||
|
var _goodListener2 = _interopRequireDefault(_goodListener);
|
|||
|
|
|||
|
function _interopRequireDefault(obj) {
|
|||
|
return obj && obj.__esModule ? obj : {
|
|||
|
default: obj
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
|
|||
|
return typeof obj;
|
|||
|
} : function (obj) {
|
|||
|
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
|||
|
};
|
|||
|
|
|||
|
function _classCallCheck(instance, Constructor) {
|
|||
|
if (!(instance instanceof Constructor)) {
|
|||
|
throw new TypeError("Cannot call a class as a function");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _createClass = function () {
|
|||
|
function defineProperties(target, props) {
|
|||
|
for (var i = 0; i < props.length; i++) {
|
|||
|
var descriptor = props[i];
|
|||
|
descriptor.enumerable = descriptor.enumerable || false;
|
|||
|
descriptor.configurable = true;
|
|||
|
if ("value" in descriptor) descriptor.writable = true;
|
|||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return function (Constructor, protoProps, staticProps) {
|
|||
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
|||
|
if (staticProps) defineProperties(Constructor, staticProps);
|
|||
|
return Constructor;
|
|||
|
};
|
|||
|
}();
|
|||
|
|
|||
|
function _possibleConstructorReturn(self, call) {
|
|||
|
if (!self) {
|
|||
|
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
|||
|
}
|
|||
|
|
|||
|
return call && (typeof call === "object" || typeof call === "function") ? call : self;
|
|||
|
}
|
|||
|
|
|||
|
function _inherits(subClass, superClass) {
|
|||
|
if (typeof superClass !== "function" && superClass !== null) {
|
|||
|
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
|
|||
|
}
|
|||
|
|
|||
|
subClass.prototype = Object.create(superClass && superClass.prototype, {
|
|||
|
constructor: {
|
|||
|
value: subClass,
|
|||
|
enumerable: false,
|
|||
|
writable: true,
|
|||
|
configurable: true
|
|||
|
}
|
|||
|
});
|
|||
|
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
|
|||
|
}
|
|||
|
|
|||
|
var Clipboard = function (_Emitter) {
|
|||
|
_inherits(Clipboard, _Emitter);
|
|||
|
|
|||
|
/**
|
|||
|
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
|
|||
|
* @param {Object} options
|
|||
|
*/
|
|||
|
function Clipboard(trigger, options) {
|
|||
|
_classCallCheck(this, Clipboard);
|
|||
|
|
|||
|
var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this));
|
|||
|
|
|||
|
_this.resolveOptions(options);
|
|||
|
_this.listenClick(trigger);
|
|||
|
return _this;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Defines if attributes would be resolved using internal setter functions
|
|||
|
* or custom functions that were passed in the constructor.
|
|||
|
* @param {Object} options
|
|||
|
*/
|
|||
|
|
|||
|
_createClass(Clipboard, [{
|
|||
|
key: 'resolveOptions',
|
|||
|
value: function resolveOptions() {
|
|||
|
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|||
|
|
|||
|
this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
|
|||
|
this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
|
|||
|
this.text = typeof options.text === 'function' ? options.text : this.defaultText;
|
|||
|
this.container = _typeof(options.container) === 'object' ? options.container : document.body;
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'listenClick',
|
|||
|
value: function listenClick(trigger) {
|
|||
|
var _this2 = this;
|
|||
|
|
|||
|
this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) {
|
|||
|
return _this2.onClick(e);
|
|||
|
});
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'onClick',
|
|||
|
value: function onClick(e) {
|
|||
|
var trigger = e.delegateTarget || e.currentTarget;
|
|||
|
|
|||
|
if (this.clipboardAction) {
|
|||
|
this.clipboardAction = null;
|
|||
|
}
|
|||
|
|
|||
|
this.clipboardAction = new _clipboardAction2.default({
|
|||
|
action: this.action(trigger),
|
|||
|
target: this.target(trigger),
|
|||
|
text: this.text(trigger),
|
|||
|
container: this.container,
|
|||
|
trigger: trigger,
|
|||
|
emitter: this
|
|||
|
});
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'defaultAction',
|
|||
|
value: function defaultAction(trigger) {
|
|||
|
return getAttributeValue('action', trigger);
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'defaultTarget',
|
|||
|
value: function defaultTarget(trigger) {
|
|||
|
var selector = getAttributeValue('target', trigger);
|
|||
|
|
|||
|
if (selector) {
|
|||
|
return document.querySelector(selector);
|
|||
|
}
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'defaultText',
|
|||
|
value: function defaultText(trigger) {
|
|||
|
return getAttributeValue('text', trigger);
|
|||
|
}
|
|||
|
}, {
|
|||
|
key: 'destroy',
|
|||
|
value: function destroy() {
|
|||
|
this.listener.destroy();
|
|||
|
|
|||
|
if (this.clipboardAction) {
|
|||
|
this.clipboardAction.destroy();
|
|||
|
this.clipboardAction = null;
|
|||
|
}
|
|||
|
}
|
|||
|
}], [{
|
|||
|
key: 'isSupported',
|
|||
|
value: function isSupported() {
|
|||
|
var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];
|
|||
|
|
|||
|
var actions = typeof action === 'string' ? [action] : action;
|
|||
|
var support = !!document.queryCommandSupported;
|
|||
|
|
|||
|
actions.forEach(function (action) {
|
|||
|
support = support && !!document.queryCommandSupported(action);
|
|||
|
});
|
|||
|
|
|||
|
return support;
|
|||
|
}
|
|||
|
}]);
|
|||
|
|
|||
|
return Clipboard;
|
|||
|
}(_tinyEmitter2.default);
|
|||
|
|
|||
|
/**
|
|||
|
* Helper function to retrieve attribute value.
|
|||
|
* @param {String} suffix
|
|||
|
* @param {Element} element
|
|||
|
*/
|
|||
|
function getAttributeValue(suffix, element) {
|
|||
|
var attribute = 'data-clipboard-' + suffix;
|
|||
|
|
|||
|
if (!element.hasAttribute(attribute)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
return element.getAttribute(attribute);
|
|||
|
}
|
|||
|
|
|||
|
module.exports = Clipboard;
|
|||
|
});
|
|||
|
}, { "./clipboard-action": 7, "good-listener": 4, "tiny-emitter": 6 }] }, {}, [8])(8);
|
|||
|
});
|
|||
|
window.jQuery = window.jQuery || window.shoestring;
|
|||
|
|
|||
|
(function ($) {
|
|||
|
var xrayiframeid = 0;
|
|||
|
var pluginName = "xrayhtml",
|
|||
|
o = {
|
|||
|
text: {
|
|||
|
open: "View Source",
|
|||
|
close: "View Demo",
|
|||
|
titlePrefix: "Example",
|
|||
|
antipattern: "Do Not Use"
|
|||
|
},
|
|||
|
classes: {
|
|||
|
button: "btn btn-small btn-xrayhtml-flipsource",
|
|||
|
open: "view-source",
|
|||
|
sourcepanel: "source-panel",
|
|||
|
title: "xraytitle",
|
|||
|
antipattern: "antipattern"
|
|||
|
},
|
|||
|
initSelector: "[data-" + pluginName + "]",
|
|||
|
defaultReveal: "inline"
|
|||
|
},
|
|||
|
methods = {
|
|||
|
_create: function () {
|
|||
|
return $(this).each(function () {
|
|||
|
var init = $(this).data("init." + pluginName);
|
|||
|
|
|||
|
if (init) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
$(this).data("init." + pluginName, true)[pluginName]("_init").trigger("create." + pluginName);
|
|||
|
});
|
|||
|
},
|
|||
|
_init: function () {
|
|||
|
var $self = $(this);
|
|||
|
|
|||
|
$self.data("id." + pluginName, xrayiframeid++);
|
|||
|
|
|||
|
var method = $(this).attr("data-" + pluginName) || o.defaultReveal;
|
|||
|
|
|||
|
if (method === "flip") {
|
|||
|
$(this)[pluginName]("_createButton");
|
|||
|
}
|
|||
|
|
|||
|
$(this).addClass(pluginName + " " + "method-" + method)[pluginName]("_createSource");
|
|||
|
|
|||
|
// use an iframe to host the source
|
|||
|
if ($(this).is("[data-" + pluginName + "-iframe]")) {
|
|||
|
|
|||
|
// grab the snippet html to ship to the iframe
|
|||
|
var snippetHTML = $(this).find(".snippet").html();
|
|||
|
|
|||
|
// grab the url of the iframe to load
|
|||
|
var url = $(this).attr("data-" + pluginName + "-iframe");
|
|||
|
|
|||
|
// grab the selector for the element in the iframe to put the html in
|
|||
|
var selector = $(this).attr("data-" + pluginName + "-iframe-target");
|
|||
|
|
|||
|
// create the iframe element, so we can bind to the load event
|
|||
|
var $iframe = $("<iframe src='" + url + "'/>");
|
|||
|
|
|||
|
// get the scripts and styles to ship to the iframe
|
|||
|
// TODO we should support styles/scripts elsewhere in the page
|
|||
|
var headHTML = $("head").html();
|
|||
|
|
|||
|
// wait until the iframe loads to send the data
|
|||
|
$iframe.bind("load", function () {
|
|||
|
|
|||
|
// wait for the iframe page to transmit the height of the page
|
|||
|
$(window).bind("message", function (event) {
|
|||
|
var data = JSON.parse(event.data || event.originalEvent.data);
|
|||
|
|
|||
|
if (data.iframeid !== $self.data("id." + pluginName)) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
$iframe.attr("height", data.iframeheight);
|
|||
|
});
|
|||
|
|
|||
|
// send a message to the iframe with the snippet to load and any
|
|||
|
// assets that are required to make it look right
|
|||
|
$iframe[0].contentWindow.postMessage({
|
|||
|
html: snippetHTML,
|
|||
|
head: headHTML,
|
|||
|
id: $self.data("id." + pluginName),
|
|||
|
selector: selector
|
|||
|
}, "*");
|
|||
|
});
|
|||
|
|
|||
|
// style the iframe properly
|
|||
|
$iframe.addClass("xray-iframe");
|
|||
|
|
|||
|
// replace the snippet which is rendered in the page with the iframe
|
|||
|
$(this).find(".snippet").html("").append($iframe);
|
|||
|
}
|
|||
|
},
|
|||
|
_createButton: function () {
|
|||
|
var btn = document.createElement("a"),
|
|||
|
txt = document.createTextNode(o.text.open),
|
|||
|
el = $(this);
|
|||
|
|
|||
|
btn.setAttribute("class", o.classes.button);
|
|||
|
btn.href = "#";
|
|||
|
btn.appendChild(txt);
|
|||
|
|
|||
|
$(btn).bind("click", function (e) {
|
|||
|
var isOpen = el.attr("class").indexOf(o.classes.open) > -1;
|
|||
|
|
|||
|
el[isOpen ? "removeClass" : "addClass"](o.classes.open);
|
|||
|
btn.innerHTML = isOpen ? o.text.open : o.text.close;
|
|||
|
|
|||
|
e.preventDefault();
|
|||
|
}).insertBefore(el);
|
|||
|
},
|
|||
|
_createSource: function () {
|
|||
|
var el = this;
|
|||
|
var getPrefixText = function () {
|
|||
|
if (el.className.match(new RegExp("\\b" + o.classes.antipattern + "\\b", "gi"))) {
|
|||
|
return o.text.antipattern;
|
|||
|
}
|
|||
|
return o.text.titlePrefix;
|
|||
|
};
|
|||
|
|
|||
|
var title = el.getElementsByClassName(o.classes.title);
|
|||
|
var deprecatedTitle;
|
|||
|
|
|||
|
if (title.length) {
|
|||
|
title = title[0];
|
|||
|
title.parentNode.removeChild(title);
|
|||
|
title.innerHTML = getPrefixText() + ": " + title.innerHTML;
|
|||
|
} else {
|
|||
|
deprecatedTitle = el.getAttribute("data-title");
|
|||
|
title = document.createElement("div");
|
|||
|
title.className = o.classes.title;
|
|||
|
title.innerHTML = getPrefixText() + (deprecatedTitle ? ": " + deprecatedTitle : "");
|
|||
|
}
|
|||
|
|
|||
|
var suppliedsourcepanel = $(el).find("." + o.classes.sourcepanel);
|
|||
|
var sourcepanel = document.createElement("div");
|
|||
|
var preel = document.createElement("pre");
|
|||
|
var codeel = document.createElement("code");
|
|||
|
var wrap = document.createElement("div");
|
|||
|
var code;
|
|||
|
var leadingWhiteSpace;
|
|||
|
var source;
|
|||
|
|
|||
|
if (suppliedsourcepanel.length) {
|
|||
|
code = suppliedsourcepanel[0].innerHTML;
|
|||
|
suppliedsourcepanel.remove();
|
|||
|
} else {
|
|||
|
code = el.innerHTML;
|
|||
|
}
|
|||
|
|
|||
|
// remove empty value attributes
|
|||
|
code = code.replace(/\=\"\"/g, '');
|
|||
|
leadingWhiteSpace = code.match(/(^[\s]+)/);
|
|||
|
|
|||
|
if (leadingWhiteSpace) {
|
|||
|
code = code.replace(new RegExp(leadingWhiteSpace[1], "gmi"), "\n");
|
|||
|
}
|
|||
|
|
|||
|
source = document.createTextNode(code);
|
|||
|
|
|||
|
wrap.setAttribute("class", "snippet");
|
|||
|
|
|||
|
$(el).wrapInner(wrap);
|
|||
|
|
|||
|
codeel.appendChild(source);
|
|||
|
preel.appendChild(codeel);
|
|||
|
|
|||
|
sourcepanel.setAttribute("class", o.classes.sourcepanel);
|
|||
|
sourcepanel.appendChild(preel);
|
|||
|
|
|||
|
this.appendChild(sourcepanel);
|
|||
|
|
|||
|
this.insertBefore(title, this.firstChild);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// Collection method.
|
|||
|
$.fn[pluginName] = function (arrg, a, b, c) {
|
|||
|
return this.each(function () {
|
|||
|
|
|||
|
// if it's a method
|
|||
|
if (arrg && typeof arrg === "string") {
|
|||
|
return $.fn[pluginName].prototype[arrg].call(this, a, b, c);
|
|||
|
}
|
|||
|
|
|||
|
// don't re-init
|
|||
|
if ($(this).data(pluginName + "data")) {
|
|||
|
return $(this);
|
|||
|
}
|
|||
|
|
|||
|
// otherwise, init
|
|||
|
$(this).data(pluginName + "active", true);
|
|||
|
$.fn[pluginName].prototype._create.call(this);
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
// add methods
|
|||
|
$.extend($.fn[pluginName].prototype, methods);
|
|||
|
|
|||
|
// auto-init
|
|||
|
var initted;
|
|||
|
function init() {
|
|||
|
if (!initted) {
|
|||
|
$(o.initSelector)[pluginName]();
|
|||
|
initted = true;
|
|||
|
}
|
|||
|
}
|
|||
|
// init either on beforeenhance event or domready, whichever comes first.
|
|||
|
$(document).bind("beforeenhance", init);
|
|||
|
$(init);
|
|||
|
})(jQuery);
|
|||
|
|
|||
|
;(function (w) {
|
|||
|
|
|||
|
var w = window;
|
|||
|
|
|||
|
var beforeEnhanceDocs = function () {
|
|||
|
|
|||
|
/* ----- prep xray snippets for clipboard ----- */
|
|||
|
Array.prototype.slice.call(document.querySelectorAll(".source-panel")).forEach(function (panel) {
|
|||
|
var snippet = panel.querySelector("pre code");
|
|||
|
var rawcode = snippet.innerText;
|
|||
|
|
|||
|
var code = function () {
|
|||
|
// add any clean up functions here
|
|||
|
// the one below removes any browser attrs added to svg `use` elements (i.e., <svg class="icon"><use xlink:href="#icon-edit"></svg>)
|
|||
|
return rawcode.replace(/..(use)\S/g, "").replace(/(xmlns:xlink)\S.(http:)..(www.w3.org).(1999).(xlink).\s/g, "");
|
|||
|
};
|
|||
|
|
|||
|
// update xray snippet with cleaned code
|
|||
|
snippet.innerText = code();
|
|||
|
|
|||
|
// populate custom attr for Clipboard
|
|||
|
panel.setAttribute("data-clipboard-text", code());
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
var enhanceDocs = function () {
|
|||
|
|
|||
|
/* ----- docs menu toggle on smaller screens ----- */
|
|||
|
document.querySelector(".docs-nav-toggle").addEventListener("mouseup", function (e) {
|
|||
|
document.querySelector(".docs-nav").classList.toggle("docs-nav-open");
|
|||
|
});
|
|||
|
|
|||
|
/* ----- assign 'on' state to global nav ----- */
|
|||
|
Array.prototype.slice.call(document.querySelector(".docs-nav").querySelectorAll("a[href]")).forEach(function (navlink) {
|
|||
|
if (navlink.pathname === document.location.pathname) {
|
|||
|
navlink.classList.add("docs-nav-active");
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
/* ----- add 'copy' option to xray snippets ----- */
|
|||
|
Array.prototype.slice.call(document.querySelectorAll(".source-panel")).forEach(function (panel) {
|
|||
|
var clipboard = new Clipboard(panel);
|
|||
|
var highlightclass = "enhanced-copy-active";
|
|||
|
|
|||
|
// add "click to copy" label via CSS
|
|||
|
panel.classList.add("enhanced-copy");
|
|||
|
|
|||
|
// highlight when clicked
|
|||
|
panel.addEventListener("click", function () {
|
|||
|
this.classList.add(highlightclass);
|
|||
|
});
|
|||
|
panel.addEventListener("transitionend", function () {
|
|||
|
this.classList.remove(highlightclass);
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
/* make a toc */
|
|||
|
var $tocwrapper = $('<div class="docs-pagenav"></div>');
|
|||
|
var $toc = $('<ul></ul>');
|
|||
|
var $h2s = $(".docbody h2[id], .docbody h3[id]");
|
|||
|
$h2s.each(function () {
|
|||
|
var id = $(this).attr("id");
|
|||
|
var li = $('<li><a href="#' + id + '">' + $(this).text() + '</li>').appendTo($toc);
|
|||
|
if ($(this).is("h3")) {
|
|||
|
li.addClass("docs-toc-l2");
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
$toc.appendTo($tocwrapper);
|
|||
|
|
|||
|
if ($h2s.length > 1) {
|
|||
|
$tocwrapper.insertBefore($h2s.eq(0));
|
|||
|
$('<h4>Jump to:</h4>').insertBefore($toc);
|
|||
|
}
|
|||
|
|
|||
|
if (!$("[data-xrayhtml-source]").is(".view-source")) {
|
|||
|
$("[data-xrayhtml-source]").prev().trigger("click");
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
//document.addEventListener( "beforeenhance", beforeEnhanceDocs );
|
|||
|
document.addEventListener("enhance", enhanceDocs);
|
|||
|
|
|||
|
/* ----- fixed-scroll page nav ----- */
|
|||
|
w.addEventListener("scroll", function () {
|
|||
|
var fixedclass = "docs-pagenav-fixed";
|
|||
|
var pg = document.body;
|
|||
|
|
|||
|
if (pg.scrollTop > 180) {
|
|||
|
pg.classList.add(fixedclass);
|
|||
|
} else {
|
|||
|
pg.classList.remove(fixedclass);
|
|||
|
};
|
|||
|
});
|
|||
|
})(window);
|
|||
|
|
|||
|
// DOM-ready auto-init of plugins.
|
|||
|
// Many plugins bind to an "enhance" event to init themselves on dom ready, or when new markup is inserted into the DOM
|
|||
|
(function ($) {
|
|||
|
|
|||
|
$(window).bind("create.xrayhtml", function (e) {
|
|||
|
var prism = !!~e.target.getAttribute("class").indexOf("prism");
|
|||
|
|
|||
|
if (prism && "Prism" in window) {
|
|||
|
$(".prism").find("code").addClass("language-markup");
|
|||
|
Prism.highlightAll();
|
|||
|
}
|
|||
|
$(e.target).prev(".btn").addClass("xrayviewsource");
|
|||
|
});
|
|||
|
|
|||
|
$(function () {
|
|||
|
$(document).trigger("beforeenhance");
|
|||
|
$(document).trigger("enhance");
|
|||
|
$(document.documentElement).addClass("enhanced");
|
|||
|
});
|
|||
|
})(jQuery);
|
|||
|
|
|||
|
(function ($) {
|
|||
|
Akamai.Viewer.defaultOptions.items.hostnames = ["a9.g.akamai.net", "akapies.akaimaging.com", "productdemo.akaimaging.com"];
|
|||
|
|
|||
|
$(function () {
|
|||
|
$("[data-akamai-viewer]").each(function (i, e) {
|
|||
|
// ignore explicit examples so they can be initialized in the example code
|
|||
|
if ($(e).closest("[data-xrayhtml-noinit]").length === 0) {
|
|||
|
new Akamai.Viewer(e);
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
})(jQuery);
|
|||
|
//# sourceMappingURL=docs.js.map
|