/*! 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 null * @param top {Number?null} Vertical scroll position, keeps current if value is null * @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('
') 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 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 On-ready automatic initialization with jQuery * $(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 = $("
").css("position", "absolute").css("left", "0").css("top", "0").prependTo(this.element); if (this.options.canvas !== false) { this.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 = $("
"); 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 "" + text + ""; }; 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 = ""; $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 = $('
').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 = $(''); 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(''); } // 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 = $('
').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\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
\n\t\t\t\t" + Akamai.Util.map(json.urls, function (url) { return Spin360._renderImg(url, finalOptions); }).join("\n") + "\n\t\t\t
\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 Spin360 states * Akamai.Spin360.States = { * Playing: 0, * Paused: 1 * }; * * @example Conditioning on states * 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
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t" + options.buttonText + "\n\t\t\t\t\n\t\t\t
\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
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t" + options.zoomInBtnText + "\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t" + options.zoomOutBtnText + "\n\t\t\t\t\n\t\t\t
\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
\n\t\t\t\t\n\t\t\t
\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
\n\t\t\t\t\n\t\t\t
\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 Video states * Akamai.Video.States = { * Playing: 0, * Paused: 1 * }; * * @example Conditioning on states * 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\n\t\t\t\t\n\t\t\t\t\n\t\t\t\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
\n\t\t\t\t" + Carousel.renderMapping[item.type].render(item, options[mappedType]) + "\n\t\t\t
\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\n\t\t\t\t\""\n\t\t\t\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
\n\t\t\t\t
\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
\n\t\t\t
\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
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\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
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t" + Carousel._renderThumbnails(data, carouselOptions) + "\n\t\t\t
\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 = $("
"); 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\n\t\t\t\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 Instatiation * 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
\n\t\t\t\t" + Akamai.Carousel.render(data, options) + "\n\t\t\t
\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
\n\t\t\t\t" + childMarkup + "\n\t\t\t
\n\t\t"; return unwrapped ? childMarkup : wrapped; }; /** * Accessor for child carousel components. * @method * @returns { Akamai.Carousel[] } */ Viewer.prototype.getCarousels = function () { return this._carousels; }; /** * Set the visible carousel based on the tag. Note the method activity does * not complete until all the images from the relevant viewer have loaded, use * the `akamai-viewer-switch-tag` event * @method * @fires Akamai.Viewer#akamai-viewer-switch-tag * @params {String} tag - The tag corresponding to the desired carousel * @returns {undefined} */ Viewer.prototype.switchTag = function (tag) { var tags = this.getTags(); // if the passed tag isn't in the data set return early and log an error in // the console if (tags.indexOf(tag) == -1) { Akamai.Util.log("tag: " + tag + " is not present in the data for this viewer", 'error'); return; } var selector = "[" + this.constructor._tagAttr + "=\"" + tag + "\"]"; // try to find an existing element with the tag var $taggedViewer = this._$el.find(selector); // if there's no element append the newly rendered tag markup if (!$taggedViewer.length) { // make sure the config takes into account the current breakpoint var options = this._resolveBreakpointOptions(); var carouselOptions = Viewer._extendCarouselOptions(options); // create the markup that will be inserted var $markup = $(this.constructor._renderTag(this._tagMapping[tag], carouselOptions, true)); var $imgs = $markup.find("img"); var imgCount = $imgs.length; var loaded = 0; // hide the new carousel initially $markup.css("display", "none"); $imgs.bind("load", function () { if (++loaded !== imgCount) { return; } // TODO namespace? $imgs.unbind("load"); var carousels = Akamai.Carousel.createMany($markup[0], carouselOptions); carousels.forEach(function (c) { // TODO figure out why the carousel constructor doesn't apply the // active index class we want on instantiation, likely due to the fact // that the carousel is hidden so the "getIndex" calc is broken // mark the carousel item as active c.goto(c.getIndex()); }); // create and store the new carousels (should be one) this._carousels = this._carousels.concat(carousels); this._showViewer($markup, carousels); }.bind(this)); // append the new markup to the existing viewer this._$el.append($markup); } else { this._showViewer($taggedViewer); } }; // TODO should be handled in CSS Viewer.prototype._showViewer = function ($viewer, carousels) { this._$el.find("[" + this.constructor._tagAttr + "]").css("display", "none").removeClass("focused"); $viewer.css("display", "block").addClass("focused"); if (carousels) { carousels.map(function (c) { c.refresh(); }); } // TODO the placement here seems arbitrary, probably belongs in `_showViewer` // TODO also sucks to be so tightly coupled this._fullscreen.addButton(); this._trigger("switch-tag"); }; /** * Accessor for tags derived from Akamai JSON data * @method * @returns { String[] } */ Viewer.prototype.getTags = function () { if (this._tags) { return this._tags; }; var tags = []; // map and store all the carousel tags for (var tag in this._tagMapping) { if (this._tagMapping.hasOwnProperty(tag) && tag !== Viewer._firstRenderTag) { // otherwise grab the list of tags tags.push(tag); } } return this._tags = tags; }; /** * Triggered when the viewer switches tag views. This includes waiting for * images to load for carousels dedicated to previously unviewed tags. * {@link Akamai.Viewer#switchTag}. * * @event Akamai.Viewer#akamai-viewer-switch-tag */ /** * Triggered when initialization finishes * {@link Akamai.Viewer}. * * @event Akamai.Viewer#akamai-viewer-init */ exports.Akamai = exports.Akamai || {}; exports.Akamai.Viewer = Viewer; })(typeof exports === 'undefined' ? window : exports, this.jQuery); (function (exports, $) { $.fn.akamaiViewer = function (options) { this.each(function (i, element) { new Akamai.Viewer(element, options); }); }; })(typeof exports === 'undefined' ? window : exports, this.jQuery); //# sourceMappingURL=akamai-viewer.unmin.js.map