/*! * Shadowbox, version 4.0.0 * Copyright 2014 Michael Jackson */ (function (global) { var documentElement = document.documentElement; // Detect support for opacity. var supportsOpacity = "opacity" in documentElement.style && typeof documentElement.style.opacity === "string"; // Detect support for fixed positioning. var fixedDiv = document.createElement("div"); fixedDiv.style.position = "fixed"; fixedDiv.style.margin = 0; fixedDiv.style.top = "20px"; documentElement.appendChild(fixedDiv, documentElement.firstChild); var supportsFixed = (fixedDiv.offsetTop == 20); documentElement.removeChild(fixedDiv); // Detect touch-based devices. var supportsTouch = ("createTouch" in document); /** * The current version of Shadowbox. */ shadowbox.version = "4.0.0"; shadowbox.guid = 1; shadowbox.K = function () { return this; }; /** * The default set of options. */ shadowbox.options = { // Animate height/width transitions. animate: true, // Automatically close when done playing movies. autoClose: false, // Able to navigate from one end of a gallery to the other (i.e. from // last item to first or vice versa) by choosing next/previous? continuous: false, // Easing function for animations. Based on a cubic polynomial. ease: function (state) { return 1 + Math.pow(state - 1, 3); }, // Enable control of Shadowbox via the keyboard? enableKeys: !supportsTouch, // The space to maintain around the edge of Shadowbox at all times. margin: 40, // A hook function that is called when closing. onClose: shadowbox.K, // A hook function that is called when a player is finished loading and // all display transitions are complete. Receives the player object as // its only argument. onDone: shadowbox.K, // A hook function that is called when opening. onOpen: shadowbox.K, // A hook function that is called when a player is ready to be // displayed. Receives the player object as its only argument. onShow: shadowbox.K, // Background color for the overlay. overlayColor: "black", // Opacity for the overlay. overlayOpacity: 0.5, // The index in the current gallery at which to start when first opening. startIndex: 0 }; /** * A map of file extensions to the player class that should be used to play * files with that extension. */ shadowbox.players = {}; /** * Registers the given player class to be used with the given file * extensions. * * shadowbox.registerPlayer(shadowbox.VideoPlayer, "mov"); * shadowbox.registerPlayer(shadowbox.PhotoPlayer, [ "jpg", "jpeg" ]); */ shadowbox.registerPlayer = function (playerClass, extensions) { extensions = extensions || []; if (!isArray(extensions)) extensions = [ extensions ]; forEach(extensions, function (extension) { shadowbox.players[extension] = playerClass; }); }; // Cache references to oft-used DOM elements. var containerElement, overlayElement, wrapperElement, bodyElement, contentElement, coverElement; /** * Appends Shadowbox to the DOM and initializes DOM references. */ function initialize() { if (containerElement) return; // Don't initialize twice! // The Shadowbox markup: // //
//
//
//
//
//
//
//
//
//
//
//
containerElement = makeDom("div", { id: "shadowbox" }); overlayElement = makeDom("div", { id: "sb-overlay" }); wrapperElement = makeDom("div", { id: "sb-wrapper" }); bodyElement = makeDom("div", { id: "sb-body" }); contentElement = makeDom("div", { id: "sb-content" }); coverElement = makeDom("div", { id: "sb-cover" }); var closeElement = makeDom("div", { id: "sb-close" }); var nextElement = makeDom("div", { id: "sb-next" }); var previousElement = makeDom("div", { id: "sb-prev" }); // Append #shadowbox to the DOM. makeDom(document.body, [ makeDom(containerElement, [ overlayElement, makeDom(wrapperElement, [ makeDom(bodyElement, [ contentElement, coverElement ]), closeElement, nextElement, previousElement ]) ]) ]); // Use an absolutely positioned container in browsers that don't // support fixed positioning. if (!supportsFixed) setStyle(containerElement, "position", "absolute"); // Setup a click listener on the overlay to close Shadowbox. addEvent(overlayElement, "click", shadowbox.close); // Setup callbacks on navigation elements. addEvent(closeElement, "click", cancel(shadowbox.close)); addEvent(nextElement, "click", cancel(shadowbox.showNext)); addEvent(previousElement, "click", cancel(shadowbox.showPrevious)); } var currentIndex = -1, currentGallery, currentPlayer, currentOptions; /** * Opens an object (or an array of objects) in Shadowbox. Takes options as * the second argument. * * shadowbox("myphoto.jpg"); * shadowbox([ "myphoto1.jpg", "myphoto2.jpg" ]); * shadowbox([ "myphoto1.jpg", "myphoto2.jpg" ], { * animate: false, * overlayColor: "white", * overlayOpacity: 0.8 * }); * * Options may be any of shadowbox.options. Returns the number of objects * that were able to be opened. */ function shadowbox(objects, options) { if (typeof options === 'number') options = { startIndex: options }; if (!isArray(objects)) objects = [ objects ]; currentOptions = mergeProperties({}, shadowbox.options); if (options) mergeProperties(currentOptions, options); currentGallery = []; // Normalize into player objects and append them to the gallery. var startIndex = currentOptions.startIndex; forEach(objects, function (object, index) { var player = shadowbox.makePlayer(object); if (player) { currentGallery.push(player); } else { if (index < startIndex) { startIndex -= 1; } else if (index === startIndex) { startIndex = 0; } } }); // Display the first item in the gallery, if there's anything left. if (currentGallery.length > 0) { if (currentIndex == -1) { initialize(); if (isFunction(currentOptions.onOpen)) currentOptions.onOpen(); setStyle(containerElement, "display", "block"); setContainerPosition(); setContainerSize(); toggleTroubleElements(0); setStyle(overlayElement, "backgroundColor", currentOptions.overlayColor); setStyle(overlayElement, "opacity", 0); setStyle(containerElement, "visibility", "visible"); animateStyle(overlayElement, "opacity", currentOptions.overlayOpacity, 0.35, function () { setWrapperSize({ width: 340, height: 200 }); setStyle(wrapperElement, "visibility", "visible"); shadowbox.show(startIndex); }); } else { shadowbox.show(startIndex); } } return currentGallery.length; } // Alias. shadowbox.open = shadowbox; /** * Displays the gallery item at the given index in Shadowbox. Assumes that * Shadowbox is already initialized and open. */ shadowbox.show = function (index) { // Guard against invalid indices and no-ops. if (index < 0 || !currentGallery[index] || currentIndex === index) return; toggleControls(0); toggleWindowHandlers(0); toggleMouseMoveHandler(0); toggleKeyDownHandler(0); setStyle(coverElement, "display", "block"); setStyle(coverElement, "opacity", 1); if (currentPlayer) currentPlayer.remove(); currentIndex = index; currentPlayer = currentGallery[currentIndex]; function playerIsReady() { return !currentPlayer || currentPlayer.isReady !== false; } waitUntil(playerIsReady, function () { if (!currentPlayer) return; // Shadowbox was closed. if (isFunction(currentOptions.onShow)) currentOptions.onShow(currentPlayer); var size = getWrapperSize(); var fromWidth = parseInt(getStyle(wrapperElement, "width")) || 0, fromHeight = parseInt(getStyle(wrapperElement, "height")) || 0, toWidth = size.width, toHeight = size.height, changeWidth = toWidth - fromWidth, changeHeight = toHeight - fromHeight; function frameHandler(value) { if (!currentPlayer) return false; // Shadowbox was closed, cancel the animation. setWrapperSize({ width: fromWidth + (changeWidth * value), height: fromHeight + (changeHeight * value) }); } // Open to the correct dimensions. Use the low-level animation // primitive to make this transition as smooth as possible. animate(0, 1, 0.5, frameHandler, function () { if (currentPlayer) { currentPlayer.injectInto(contentElement); if (currentPlayer.fadeCover) { animateStyle(coverElement, "opacity", 0, 0.5, finishShow); } else { finishShow(); } } }); }); }; function finishShow() { if (currentPlayer) { setStyle(coverElement, "display", "none"); toggleWindowHandlers(1); toggleMouseMoveHandler(1); toggleKeyDownHandler(1); if (isFunction(currentOptions.onDone)) currentOptions.onDone(currentPlayer); } } /** * Opens the previous item in the gallery. */ shadowbox.showPrevious = function () { shadowbox.show(getPreviousIndex()); }; /** * Gets the index of the previous item in the gallery, -1 if there is none. */ function getPreviousIndex() { if (currentIndex === 0) return currentOptions.continuous ? (currentGallery.length - 1) : -1; return currentIndex - 1; } /** * Opens the next item in the gallery. */ shadowbox.showNext = function () { shadowbox.show(getNextIndex()); }; /** * Gets the index of the next item in the gallery, -1 if there is none. */ function getNextIndex() { if (currentIndex === currentGallery.length - 1) return (currentOptions.continuous && currentIndex !== 0) ? 0 : -1; return currentIndex + 1; } /** * Closes Shadowbox immediately. */ shadowbox.close = function () { if (shadowbox.isOpen()) { currentIndex = -1; currentPlayer = null; setStyle(wrapperElement, "visibility", "hidden"); setStyle(coverElement, "opacity", 1); contentElement.innerHTML = ""; toggleControls(0); toggleWindowHandlers(0); toggleMouseMoveHandler(0); toggleKeyDownHandler(0); animateStyle(overlayElement, "opacity", 0, 0.5, function () { setStyle(containerElement, "visibility", "hidden"); setStyle(containerElement, "display", "none"); toggleTroubleElements(1); if (isFunction(currentOptions.onClose)) currentOptions.onClose(); }); } }; /** * Returns true if Shadowbox is currently open. */ shadowbox.isOpen = function () { return currentIndex !== -1; }; /** * Gets the current player instance. */ shadowbox.getPlayer = function () { return currentPlayer; }; /** * Gets the size that should be used for the wrapper element. Should be * called when Shadowbox is open and has a player that is ready. */ function getWrapperSize() { var margin = Math.max(currentOptions.margin, 20); // Minimum 20px margin. return constrainSize(currentPlayer.width, currentPlayer.height, overlayElement.offsetWidth, overlayElement.offsetHeight, margin); } /** * Sets the size and position of the wrapper. */ function setWrapperSize(size) { setStyle(wrapperElement, "width", size.width + "px"); setStyle(wrapperElement, "marginLeft", (-size.width / 2) + "px"); setStyle(wrapperElement, "height", size.height + "px"); setStyle(wrapperElement, "marginTop", (-size.height / 2) + "px"); } /** * Scales the given width and height to be within the bounds of the given * maximum width and height, allowing for margin. Returns an array of the * constrained [width, height]. */ function constrainSize(width, height, maxWidth, maxHeight, margin) { var originalWidth = width, originalHeight = height; // Constrain width/height to max. var marginWidth = 2 * margin; if (width + marginWidth > maxWidth) width = maxWidth - marginWidth; var marginHeight = 2 * margin; if (height + marginHeight > maxHeight) height = maxHeight - marginHeight; var changeWidth = (originalWidth - width) / originalWidth; var changeHeight = (originalHeight - height) / originalHeight; // Adjust width/height if oversized. if (changeWidth > 0 || changeHeight > 0) { // Preserve original aspect ratio according to greatest change. if (changeWidth > changeHeight) { height = Math.round((originalHeight / originalWidth) * width); } else if (changeHeight > changeWidth) { width = Math.round((originalWidth / originalHeight) * height); } } return { width: width, height: height }; } /** * Sets the size of the container element to the size of the window. */ function setContainerSize() { setStyle(containerElement, "width", documentElement.clientWidth + "px"); setStyle(containerElement, "height", documentElement.clientHeight + "px"); if (currentPlayer) setWrapperSize(getWrapperSize()); } /** * Sets the position of the container element to the top left corner of * the window. Necessary when using absolute positioning instead of fixed. */ function setContainerPosition() { setStyle(containerElement, "left", documentElement.scrollLeft + "px"); setStyle(containerElement, "top", documentElement.scrollTop + "px"); } var troubleElementTagNames = [ "select", "object", "embed", "canvas" ]; var troubleVisibilityCache = []; /** * Toggles the visibility of elements that are troublesome for overlays. */ function toggleTroubleElements(on) { if (on) { forEach(troubleVisibilityCache, function (item) { setStyle(item.element, "visibility", item.visibility || ""); }); } else { troubleVisibilityCache = []; forEach(troubleElementTagNames, function (tagName) { forEach(document.getElementsByTagName(tagName), function (element) { troubleVisibilityCache.push({ element: element, visibility: getStyle(element, "visibility") }); setStyle(element, "visibility", "hidden"); }); }); } } /** * Creates a new player object based on the properties of the given object. * Valid properties include: * * - url The URL of the content to display * - width (optional) The width of the content * - height (optional) The height of the content * - playerClass (optional) The player class to use to play the content. * Can be guessed in most cases from the URL * - encodings (video only) Encoding name/URL pairs of alternate URL's * for the video. Possible encoding names are "h264", "ogg" * "webm", and "flv" * - posterUrl (video only) The URL to a poster image of the video * - flashParams (flash only) Name/value pairs of 's to use for * the Flash * - flashVars (flash only) Name/value pairs of variables to pass to * the Flash object as variables * * If a string is given, it will be used as the value of the URL. If a DOM * element is given, it should have an href property (i.e. either an or * an element) which will be used as the URL. It may also contain * a data-shadowbox attribute that has any of the other options formatted * in a JSON string. * * If no player is specified, it will be guessed using the registered player * for the URL's file extension (see shadowbox.registerPlayer). * * Returns null if no player is able to be created, or this browser does * not have proper support for that content. */ shadowbox.makePlayer = function (object) { if (typeof object === "string") { object = { url: object }; } else if (isElement(object) && object.href) { // The object is a DOM element. Should be an or . The // data-shadowbox attribute may contain a string specifying // options for the player object (see parseData). var data = object.getAttribute("data-shadowbox"); object = { url: object.href }; if (data) mergeProperties(object, parseData(data)); } if (object && typeof object.url === "string") { var playerClass; if (object.playerClass) { playerClass = object.playerClass; } else { // Guess the player class using the URL's file extension. var match = object.url.match(/\.([0-9a-z]+)(\?.*)?$/i); if (match) { var extension = match[1].toLowerCase(); playerClass = shadowbox.players[extension]; } } playerClass = playerClass || FramePlayer; var player = new playerClass(object, "sb-player-" + String(shadowbox.guid++)); if (player.isSupported()) return player; } return null; }; // Toggles the visibility of clickable controls. function toggleControls(on) { var name = ""; if (on) { name += "active"; if (getNextIndex() !== -1) name += " has-next"; if (getPreviousIndex() !== -1) name += " has-prev"; } containerElement.className = name; } var resizeTimer, scrollTimer, mouseMoveTimer; // Toggles window resize and scroll event handlers. function toggleWindowHandlers(on) { var addOrRemoveEvent; if (on) { addOrRemoveEvent = addEvent; } else { addOrRemoveEvent = removeEvent; // Clear cached timers. if (resizeTimer) { clearTimeout(resizeTimer); resizeTimer = null; } if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null; } } addOrRemoveEvent(window, "resize", handleWindowResize); if (!supportsFixed) addOrRemoveEvent(window, "scroll", handleWindowScroll); } // Updates the size of the container when the window size changes. function handleWindowResize() { if (resizeTimer) { clearTimeout(resizeTimer); resizeTimer = null; } resizeTimer = setTimeout(function () { resizeTimer = null; setContainerSize(); }, 10); } // Updates the position of the container when the window scrolls. function handleWindowScroll() { if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null; } scrollTimer = setTimeout(function () { scrollTimer = null; setContainerPosition(); }, 10); } // Toggles document mouse move handler on/off. function toggleMouseMoveHandler(on) { if (supportsTouch) { toggleControls(on); return; } var addOrRemoveEvent; if (on) { addOrRemoveEvent = addEvent; } else { addOrRemoveEvent = removeEvent; // Clear cached timers. if (mouseMoveTimer) { clearTimeout(mouseMoveTimer); mouseMoveTimer = null; } } addOrRemoveEvent(document, "mousemove", handleMouseMove); } var lastMouseX, lastMouseY; // Shows clickable controls when the mouse moves. function handleMouseMove(event) { // Ignore consecutive mousemove events from the same location. if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) { lastMouseX = event.clientX; lastMouseY = event.clientY; if (mouseMoveTimer) { clearTimeout(mouseMoveTimer); mouseMoveTimer = null; } else { toggleControls(1); } mouseMoveTimer = setTimeout(function () { mouseMoveTimer = null; toggleControls(0); }, 1500); } } function toggleKeyDownHandler(on) { if (currentOptions.enableKeys) (on ? addEvent : removeEvent)(document, "keydown", handleDocumentKeyDown); } var KEY_ESCAPE = 27; var KEY_SPACE = 32; var KEY_LEFT = 37; var KEY_RIGHT = 39; var KEY_Q = 81; var KEY_X = 88; function handleDocumentKeyDown(event) { if (eventHasModifierKey(event)) return; switch (event.keyCode) { case KEY_ESCAPE: case KEY_Q: case KEY_X: event.preventDefault(); shadowbox.close(); break; case KEY_LEFT: event.preventDefault(); shadowbox.showPrevious(); break; case KEY_RIGHT: event.preventDefault(); shadowbox.showNext(); break; case KEY_SPACE: if (currentPlayer && isFunction(currentPlayer.togglePlay)) { event.preventDefault(); currentPlayer.togglePlay(); } break; } } function eventHasModifierKey(event) { return event.ctrlKey || event.metaKey; } function toggleClickHandler(on) { (on ? addEvent : removeEvent)(document, 'click', handleDocumentClick); } function handleDocumentClick(event) { var target = event.target; if (isElement(target)) { var matcher = /^(?:shadow|light)box(?:\[(\w+)\])?$/i, links = [], index = 0, match; // Find an ancestor node with rel="shadowbox" attribute. while (target) { match = (target.rel || "").match(matcher); if (match) { var galleryName = match[1]; // Look for other elements in the document that also have // rel="shadowbox" attribute with the same gallery. if (galleryName) { var galleryMatcher = new RegExp("^(shadow|light)box\\[" + galleryName + "\\]$", "i"); forEach(document.getElementsByTagName('a'), function (link) { if (link.rel && galleryMatcher.test(link.rel)) { if (link == target) index = links.length; links.push(link); } }); } else { links.push(target); } break; } target = target.parentNode; } // Good for debugging. // event.preventDefault(); if (links.length > 0 && shadowbox.open(links, index) > 0) event.preventDefault(); // Prevent the browser from following the link. } } //// PLAYERS //// shadowbox.FramePlayer = FramePlayer; /** * A player that displays its content inside an