UNPKG

@fancyapps/fancybox

Version:

Touch enabled, responsive and fully customizable jQuery lightbox script

1,639 lines (1,284 loc) 166 kB
// ================================================== // fancyBox v3.5.7 // // Licensed GPLv3 for open source use // or fancyBox Commercial License for commercial use // // http://fancyapps.com/fancybox/ // Copyright 2019 fancyApps // // ================================================== (function (window, document, $, undefined) { "use strict"; window.console = window.console || { info: function (stuff) {} }; // If there's no jQuery, fancyBox can't work // ========================================= if (!$) { return; } // Check if fancyBox is already initialized // ======================================== if ($.fn.fancybox) { console.info("fancyBox already initialized"); return; } // Private default settings // ======================== var defaults = { // Close existing modals // Set this to false if you do not need to stack multiple instances closeExisting: false, // Enable infinite gallery navigation loop: false, // Horizontal space between slides gutter: 50, // Enable keyboard navigation keyboard: true, // Should allow caption to overlap the content preventCaptionOverlap: true, // Should display navigation arrows at the screen edges arrows: true, // Should display counter at the top left corner infobar: true, // Should display close button (using `btnTpl.smallBtn` template) over the content // Can be true, false, "auto" // If "auto" - will be automatically enabled for "html", "inline" or "ajax" items smallBtn: "auto", // Should display toolbar (buttons at the top) // Can be true, false, "auto" // If "auto" - will be automatically hidden if "smallBtn" is enabled toolbar: "auto", // What buttons should appear in the top right corner. // Buttons will be created using templates from `btnTpl` option // and they will be placed into toolbar (class="fancybox-toolbar"` element) buttons: [ "zoom", //"share", "slideShow", //"fullScreen", //"download", "thumbs", "close" ], // Detect "idle" time in seconds idleTime: 3, // Disable right-click and use simple image protection for images protect: false, // Shortcut to make content "modal" - disable keyboard navigtion, hide buttons, etc modal: false, image: { // Wait for images to load before displaying // true - wait for image to load and then display; // false - display thumbnail and load the full-sized image over top, // requires predefined image dimensions (`data-width` and `data-height` attributes) preload: false }, ajax: { // Object containing settings for ajax request settings: { // This helps to indicate that request comes from the modal // Feel free to change naming data: { fancybox: true } } }, iframe: { // Iframe template tpl: '<iframe id="fancybox-frame{rnd}" name="fancybox-frame{rnd}" class="fancybox-iframe" allowfullscreen="allowfullscreen" allow="autoplay; fullscreen" src=""></iframe>', // Preload iframe before displaying it // This allows to calculate iframe content width and height // (note: Due to "Same Origin Policy", you can't get cross domain data). preload: true, // Custom CSS styling for iframe wrapping element // You can use this to set custom iframe dimensions css: {}, // Iframe tag attributes attr: { scrolling: "auto" } }, // For HTML5 video only video: { tpl: '<video class="fancybox-video" controls controlsList="nodownload" poster="{{poster}}">' + '<source src="{{src}}" type="{{format}}" />' + 'Sorry, your browser doesn\'t support embedded videos, <a href="{{src}}">download</a> and watch with your favorite video player!' + "</video>", format: "", // custom video format autoStart: true }, // Default content type if cannot be detected automatically defaultType: "image", // Open/close animation type // Possible values: // false - disable // "zoom" - zoom images from/to thumbnail // "fade" // "zoom-in-out" // animationEffect: "zoom", // Duration in ms for open/close animation animationDuration: 366, // Should image change opacity while zooming // If opacity is "auto", then opacity will be changed if image and thumbnail have different aspect ratios zoomOpacity: "auto", // Transition effect between slides // // Possible values: // false - disable // "fade' // "slide' // "circular' // "tube' // "zoom-in-out' // "rotate' // transitionEffect: "fade", // Duration in ms for transition animation transitionDuration: 366, // Custom CSS class for slide element slideClass: "", // Custom CSS class for layout baseClass: "", // Base template for layout baseTpl: '<div class="fancybox-container" role="dialog" tabindex="-1">' + '<div class="fancybox-bg"></div>' + '<div class="fancybox-inner">' + '<div class="fancybox-infobar"><span data-fancybox-index></span>&nbsp;/&nbsp;<span data-fancybox-count></span></div>' + '<div class="fancybox-toolbar">{{buttons}}</div>' + '<div class="fancybox-navigation">{{arrows}}</div>' + '<div class="fancybox-stage"></div>' + '<div class="fancybox-caption"><div class="fancybox-caption__body"></div></div>' + "</div>" + "</div>", // Loading indicator template spinnerTpl: '<div class="fancybox-loading"></div>', // Error message template errorTpl: '<div class="fancybox-error"><p>{{ERROR}}</p></div>', btnTpl: { download: '<a download data-fancybox-download class="fancybox-button fancybox-button--download" title="{{DOWNLOAD}}" href="javascript:;">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.62 17.09V19H5.38v-1.91zm-2.97-6.96L17 11.45l-5 4.87-5-4.87 1.36-1.32 2.68 2.64V5h1.92v7.77z"/></svg>' + "</a>", zoom: '<button data-fancybox-zoom class="fancybox-button fancybox-button--zoom" title="{{ZOOM}}">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.7 17.3l-3-3a5.9 5.9 0 0 0-.6-7.6 5.9 5.9 0 0 0-8.4 0 5.9 5.9 0 0 0 0 8.4 5.9 5.9 0 0 0 7.7.7l3 3a1 1 0 0 0 1.3 0c.4-.5.4-1 0-1.5zM8.1 13.8a4 4 0 0 1 0-5.7 4 4 0 0 1 5.7 0 4 4 0 0 1 0 5.7 4 4 0 0 1-5.7 0z"/></svg>' + "</button>", close: '<button data-fancybox-close class="fancybox-button fancybox-button--close" title="{{CLOSE}}">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 10.6L6.6 5.2 5.2 6.6l5.4 5.4-5.4 5.4 1.4 1.4 5.4-5.4 5.4 5.4 1.4-1.4-5.4-5.4 5.4-5.4-1.4-1.4-5.4 5.4z"/></svg>' + "</button>", // Arrows arrowLeft: '<button data-fancybox-prev class="fancybox-button fancybox-button--arrow_left" title="{{PREV}}">' + '<div><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.28 15.7l-1.34 1.37L5 12l4.94-5.07 1.34 1.38-2.68 2.72H19v1.94H8.6z"/></svg></div>' + "</button>", arrowRight: '<button data-fancybox-next class="fancybox-button fancybox-button--arrow_right" title="{{NEXT}}">' + '<div><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.4 12.97l-2.68 2.72 1.34 1.38L19 12l-4.94-5.07-1.34 1.38 2.68 2.72H5v1.94z"/></svg></div>' + "</button>", // This small close button will be appended to your html/inline/ajax content by default, // if "smallBtn" option is not set to false smallBtn: '<button type="button" data-fancybox-close class="fancybox-button fancybox-close-small" title="{{CLOSE}}">' + '<svg xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24"><path d="M13 12l5-5-1-1-5 5-5-5-1 1 5 5-5 5 1 1 5-5 5 5 1-1z"/></svg>' + "</button>" }, // Container is injected into this element parentEl: "body", // Hide browser vertical scrollbars; use at your own risk hideScrollbar: true, // Focus handling // ============== // Try to focus on the first focusable element after opening autoFocus: true, // Put focus back to active element after closing backFocus: true, // Do not let user to focus on element outside modal content trapFocus: true, // Module specific options // ======================= fullScreen: { autoStart: false }, // Set `touch: false` to disable panning/swiping touch: { vertical: true, // Allow to drag content vertically momentum: true // Continue movement after releasing mouse/touch when panning }, // Hash value when initializing manually, // set `false` to disable hash change hash: null, // Customize or add new media types // Example: /* media : { youtube : { params : { autoplay : 0 } } } */ media: {}, slideShow: { autoStart: false, speed: 3000 }, thumbs: { autoStart: false, // Display thumbnails on opening hideOnClose: true, // Hide thumbnail grid when closing animation starts parentEl: ".fancybox-container", // Container is injected into this element axis: "y" // Vertical (y) or horizontal (x) scrolling }, // Use mousewheel to navigate gallery // If 'auto' - enabled for images only wheel: "auto", // Callbacks //========== // See Documentation/API/Events for more information // Example: /* afterShow: function( instance, current ) { console.info( 'Clicked element:' ); console.info( current.opts.$orig ); } */ onInit: $.noop, // When instance has been initialized beforeLoad: $.noop, // Before the content of a slide is being loaded afterLoad: $.noop, // When the content of a slide is done loading beforeShow: $.noop, // Before open animation starts afterShow: $.noop, // When content is done loading and animating beforeClose: $.noop, // Before the instance attempts to close. Return false to cancel the close. afterClose: $.noop, // After instance has been closed onActivate: $.noop, // When instance is brought to front onDeactivate: $.noop, // When other instance has been activated // Interaction // =========== // Use options below to customize taken action when user clicks or double clicks on the fancyBox area, // each option can be string or method that returns value. // // Possible values: // "close" - close instance // "next" - move to next gallery item // "nextOrClose" - move to next gallery item or close if gallery has only one item // "toggleControls" - show/hide controls // "zoom" - zoom image (if loaded) // false - do nothing // Clicked on the content clickContent: function (current, event) { return current.type === "image" ? "zoom" : false; }, // Clicked on the slide clickSlide: "close", // Clicked on the background (backdrop) element; // if you have not changed the layout, then most likely you need to use `clickSlide` option clickOutside: "close", // Same as previous two, but for double click dblclickContent: false, dblclickSlide: false, dblclickOutside: false, // Custom options when mobile device is detected // ============================================= mobile: { preventCaptionOverlap: false, idleTime: false, clickContent: function (current, event) { return current.type === "image" ? "toggleControls" : false; }, clickSlide: function (current, event) { return current.type === "image" ? "toggleControls" : "close"; }, dblclickContent: function (current, event) { return current.type === "image" ? "zoom" : false; }, dblclickSlide: function (current, event) { return current.type === "image" ? "zoom" : false; } }, // Internationalization // ==================== lang: "en", i18n: { en: { CLOSE: "Close", NEXT: "Next", PREV: "Previous", ERROR: "The requested content cannot be loaded. <br/> Please try again later.", PLAY_START: "Start slideshow", PLAY_STOP: "Pause slideshow", FULL_SCREEN: "Full screen", THUMBS: "Thumbnails", DOWNLOAD: "Download", SHARE: "Share", ZOOM: "Zoom" }, de: { CLOSE: "Schlie&szlig;en", NEXT: "Weiter", PREV: "Zur&uuml;ck", ERROR: "Die angeforderten Daten konnten nicht geladen werden. <br/> Bitte versuchen Sie es sp&auml;ter nochmal.", PLAY_START: "Diaschau starten", PLAY_STOP: "Diaschau beenden", FULL_SCREEN: "Vollbild", THUMBS: "Vorschaubilder", DOWNLOAD: "Herunterladen", SHARE: "Teilen", ZOOM: "Vergr&ouml;&szlig;ern" } } }; // Few useful variables and methods // ================================ var $W = $(window); var $D = $(document); var called = 0; // Check if an object is a jQuery object and not a native JavaScript object // ======================================================================== var isQuery = function (obj) { return obj && obj.hasOwnProperty && obj instanceof $; }; // Handle multiple browsers for "requestAnimationFrame" and "cancelAnimationFrame" // =============================================================================== var requestAFrame = (function () { return ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || // if all else fails, use setTimeout function (callback) { return window.setTimeout(callback, 1000 / 60); } ); })(); var cancelAFrame = (function () { return ( window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || function (id) { window.clearTimeout(id); } ); })(); // Detect the supported transition-end event property name // ======================================================= var transitionEnd = (function () { var el = document.createElement("fakeelement"), t; var transitions = { transition: "transitionend", OTransition: "oTransitionEnd", MozTransition: "transitionend", WebkitTransition: "webkitTransitionEnd" }; for (t in transitions) { if (el.style[t] !== undefined) { return transitions[t]; } } return "transitionend"; })(); // Force redraw on an element. // This helps in cases where the browser doesn't redraw an updated element properly // ================================================================================ var forceRedraw = function ($el) { return $el && $el.length && $el[0].offsetHeight; }; // Exclude array (`buttons`) options from deep merging // =================================================== var mergeOpts = function (opts1, opts2) { var rez = $.extend(true, {}, opts1, opts2); $.each(opts2, function (key, value) { if ($.isArray(value)) { rez[key] = value; } }); return rez; }; // How much of an element is visible in viewport // ============================================= var inViewport = function (elem) { var elemCenter, rez; if (!elem || elem.ownerDocument !== document) { return false; } $(".fancybox-container").css("pointer-events", "none"); elemCenter = { x: elem.getBoundingClientRect().left + elem.offsetWidth / 2, y: elem.getBoundingClientRect().top + elem.offsetHeight / 2 }; rez = document.elementFromPoint(elemCenter.x, elemCenter.y) === elem; $(".fancybox-container").css("pointer-events", ""); return rez; }; // Class definition // ================ var FancyBox = function (content, opts, index) { var self = this; self.opts = mergeOpts({ index: index }, $.fancybox.defaults); if ($.isPlainObject(opts)) { self.opts = mergeOpts(self.opts, opts); } if ($.fancybox.isMobile) { self.opts = mergeOpts(self.opts, self.opts.mobile); } self.id = self.opts.id || ++called; self.currIndex = parseInt(self.opts.index, 10) || 0; self.prevIndex = null; self.prevPos = null; self.currPos = 0; self.firstRun = true; // All group items self.group = []; // Existing slides (for current, next and previous gallery items) self.slides = {}; // Create group elements self.addContent(content); if (!self.group.length) { return; } self.init(); }; $.extend(FancyBox.prototype, { // Create DOM structure // ==================== init: function () { var self = this, firstItem = self.group[self.currIndex], firstItemOpts = firstItem.opts, $container, buttonStr; if (firstItemOpts.closeExisting) { $.fancybox.close(true); } // Hide scrollbars // =============== $("body").addClass("fancybox-active"); if ( !$.fancybox.getInstance() && firstItemOpts.hideScrollbar !== false && !$.fancybox.isMobile && document.body.scrollHeight > window.innerHeight ) { $("head").append( '<style id="fancybox-style-noscroll" type="text/css">.compensate-for-scrollbar{margin-right:' + (window.innerWidth - document.documentElement.clientWidth) + "px;}</style>" ); $("body").addClass("compensate-for-scrollbar"); } // Build html markup and set references // ==================================== // Build html code for buttons and insert into main template buttonStr = ""; $.each(firstItemOpts.buttons, function (index, value) { buttonStr += firstItemOpts.btnTpl[value] || ""; }); // Create markup from base template, it will be initially hidden to // avoid unnecessary work like painting while initializing is not complete $container = $( self.translate( self, firstItemOpts.baseTpl .replace("{{buttons}}", buttonStr) .replace("{{arrows}}", firstItemOpts.btnTpl.arrowLeft + firstItemOpts.btnTpl.arrowRight) ) ) .attr("id", "fancybox-container-" + self.id) .addClass(firstItemOpts.baseClass) .data("FancyBox", self) .appendTo(firstItemOpts.parentEl); // Create object holding references to jQuery wrapped nodes self.$refs = { container: $container }; ["bg", "inner", "infobar", "toolbar", "stage", "caption", "navigation"].forEach(function (item) { self.$refs[item] = $container.find(".fancybox-" + item); }); self.trigger("onInit"); // Enable events, deactive previous instances self.activate(); // Build slides, load and reveal content self.jumpTo(self.currIndex); }, // Simple i18n support - replaces object keys found in template // with corresponding values // ============================================================ translate: function (obj, str) { var arr = obj.opts.i18n[obj.opts.lang] || obj.opts.i18n.en; return str.replace(/\{\{(\w+)\}\}/g, function (match, n) { return arr[n] === undefined ? match : arr[n]; }); }, // Populate current group with fresh content // Check if each object has valid type and content // =============================================== addContent: function (content) { var self = this, items = $.makeArray(content), thumbs; $.each(items, function (i, item) { var obj = {}, opts = {}, $item, type, found, src, srcParts; // Step 1 - Make sure we have an object // ==================================== if ($.isPlainObject(item)) { // We probably have manual usage here, something like // $.fancybox.open( [ { src : "image.jpg", type : "image" } ] ) obj = item; opts = item.opts || item; } else if ($.type(item) === "object" && $(item).length) { // Here we probably have jQuery collection returned by some selector $item = $(item); // Support attributes like `data-options='{"touch" : false}'` and `data-touch='false'` opts = $item.data() || {}; opts = $.extend(true, {}, opts, opts.options); // Here we store clicked element opts.$orig = $item; obj.src = self.opts.src || opts.src || $item.attr("href"); // Assume that simple syntax is used, for example: // `$.fancybox.open( $("#test"), {} );` if (!obj.type && !obj.src) { obj.type = "inline"; obj.src = item; } } else { // Assume we have a simple html code, for example: // $.fancybox.open( '<div><h1>Hi!</h1></div>' ); obj = { type: "html", src: item + "" }; } // Each gallery object has full collection of options obj.opts = $.extend(true, {}, self.opts, opts); // Do not merge buttons array if ($.isArray(opts.buttons)) { obj.opts.buttons = opts.buttons; } if ($.fancybox.isMobile && obj.opts.mobile) { obj.opts = mergeOpts(obj.opts, obj.opts.mobile); } // Step 2 - Make sure we have content type, if not - try to guess // ============================================================== type = obj.type || obj.opts.type; src = obj.src || ""; if (!type && src) { if ((found = src.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))) { type = "video"; if (!obj.opts.video.format) { obj.opts.video.format = "video/" + (found[1] === "ogv" ? "ogg" : found[1]); } } else if (src.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)) { type = "image"; } else if (src.match(/\.(pdf)((\?|#).*)?$/i)) { type = "iframe"; obj = $.extend(true, obj, { contentType: "pdf", opts: { iframe: { preload: false } } }); } else if (src.charAt(0) === "#") { type = "inline"; } } if (type) { obj.type = type; } else { self.trigger("objectNeedsType", obj); } if (!obj.contentType) { obj.contentType = $.inArray(obj.type, ["html", "inline", "ajax"]) > -1 ? "html" : obj.type; } // Step 3 - Some adjustments // ========================= obj.index = self.group.length; if (obj.opts.smallBtn == "auto") { obj.opts.smallBtn = $.inArray(obj.type, ["html", "inline", "ajax"]) > -1; } if (obj.opts.toolbar === "auto") { obj.opts.toolbar = !obj.opts.smallBtn; } // Find thumbnail image, check if exists and if is in the viewport obj.$thumb = obj.opts.$thumb || null; if (obj.opts.$trigger && obj.index === self.opts.index) { obj.$thumb = obj.opts.$trigger.find("img:first"); if (obj.$thumb.length) { obj.opts.$orig = obj.opts.$trigger; } } if (!(obj.$thumb && obj.$thumb.length) && obj.opts.$orig) { obj.$thumb = obj.opts.$orig.find("img:first"); } if (obj.$thumb && !obj.$thumb.length) { obj.$thumb = null; } obj.thumb = obj.opts.thumb || (obj.$thumb ? obj.$thumb[0].src : null); // "caption" is a "special" option, it can be used to customize caption per gallery item if ($.type(obj.opts.caption) === "function") { obj.opts.caption = obj.opts.caption.apply(item, [self, obj]); } if ($.type(self.opts.caption) === "function") { obj.opts.caption = self.opts.caption.apply(item, [self, obj]); } // Make sure we have caption as a string or jQuery object if (!(obj.opts.caption instanceof $)) { obj.opts.caption = obj.opts.caption === undefined ? "" : obj.opts.caption + ""; } // Check if url contains "filter" used to filter the content // Example: "ajax.html #something" if (obj.type === "ajax") { srcParts = src.split(/\s+/, 2); if (srcParts.length > 1) { obj.src = srcParts.shift(); obj.opts.filter = srcParts.shift(); } } // Hide all buttons and disable interactivity for modal items if (obj.opts.modal) { obj.opts = $.extend(true, obj.opts, { trapFocus: true, // Remove buttons infobar: 0, toolbar: 0, smallBtn: 0, // Disable keyboard navigation keyboard: 0, // Disable some modules slideShow: 0, fullScreen: 0, thumbs: 0, touch: 0, // Disable click event handlers clickContent: false, clickSlide: false, clickOutside: false, dblclickContent: false, dblclickSlide: false, dblclickOutside: false }); } // Step 4 - Add processed object to group // ====================================== self.group.push(obj); }); // Update controls if gallery is already opened if (Object.keys(self.slides).length) { self.updateControls(); // Update thumbnails, if needed thumbs = self.Thumbs; if (thumbs && thumbs.isActive) { thumbs.create(); thumbs.focus(); } } }, // Attach an event handler functions for: // - navigation buttons // - browser scrolling, resizing; // - focusing // - keyboard // - detecting inactivity // ====================================== addEvents: function () { var self = this; self.removeEvents(); // Make navigation elements clickable // ================================== self.$refs.container .on("click.fb-close", "[data-fancybox-close]", function (e) { e.stopPropagation(); e.preventDefault(); self.close(e); }) .on("touchstart.fb-prev click.fb-prev", "[data-fancybox-prev]", function (e) { e.stopPropagation(); e.preventDefault(); self.previous(); }) .on("touchstart.fb-next click.fb-next", "[data-fancybox-next]", function (e) { e.stopPropagation(); e.preventDefault(); self.next(); }) .on("click.fb", "[data-fancybox-zoom]", function (e) { // Click handler for zoom button self[self.isScaledDown() ? "scaleToActual" : "scaleToFit"](); }); // Handle page scrolling and browser resizing // ========================================== $W.on("orientationchange.fb resize.fb", function (e) { if (e && e.originalEvent && e.originalEvent.type === "resize") { if (self.requestId) { cancelAFrame(self.requestId); } self.requestId = requestAFrame(function () { self.update(e); }); } else { if (self.current && self.current.type === "iframe") { self.$refs.stage.hide(); } setTimeout( function () { self.$refs.stage.show(); self.update(e); }, $.fancybox.isMobile ? 600 : 250 ); } }); $D.on("keydown.fb", function (e) { var instance = $.fancybox ? $.fancybox.getInstance() : null, current = instance.current, keycode = e.keyCode || e.which; // Trap keyboard focus inside of the modal // ======================================= if (keycode == 9) { if (current.opts.trapFocus) { self.focus(e); } return; } // Enable keyboard navigation // ========================== if (!current.opts.keyboard || e.ctrlKey || e.altKey || e.shiftKey || $(e.target).is("input,textarea,video,audio,select")) { return; } // Backspace and Esc keys if (keycode === 8 || keycode === 27) { e.preventDefault(); self.close(e); return; } // Left arrow and Up arrow if (keycode === 37 || keycode === 38) { e.preventDefault(); self.previous(); return; } // Righ arrow and Down arrow if (keycode === 39 || keycode === 40) { e.preventDefault(); self.next(); return; } self.trigger("afterKeydown", e, keycode); }); // Hide controls after some inactivity period if (self.group[self.currIndex].opts.idleTime) { self.idleSecondsCounter = 0; $D.on( "mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle", function (e) { self.idleSecondsCounter = 0; if (self.isIdle) { self.showControls(); } self.isIdle = false; } ); self.idleInterval = window.setInterval(function () { self.idleSecondsCounter++; if (self.idleSecondsCounter >= self.group[self.currIndex].opts.idleTime && !self.isDragging) { self.isIdle = true; self.idleSecondsCounter = 0; self.hideControls(); } }, 1000); } }, // Remove events added by the core // =============================== removeEvents: function () { var self = this; $W.off("orientationchange.fb resize.fb"); $D.off("keydown.fb .fb-idle"); this.$refs.container.off(".fb-close .fb-prev .fb-next"); if (self.idleInterval) { window.clearInterval(self.idleInterval); self.idleInterval = null; } }, // Change to previous gallery item // =============================== previous: function (duration) { return this.jumpTo(this.currPos - 1, duration); }, // Change to next gallery item // =========================== next: function (duration) { return this.jumpTo(this.currPos + 1, duration); }, // Switch to selected gallery item // =============================== jumpTo: function (pos, duration) { var self = this, groupLen = self.group.length, firstRun, isMoved, loop, current, previous, slidePos, stagePos, prop, diff; if (self.isDragging || self.isClosing || (self.isAnimating && self.firstRun)) { return; } // Should loop? pos = parseInt(pos, 10); loop = self.current ? self.current.opts.loop : self.opts.loop; if (!loop && (pos < 0 || pos >= groupLen)) { return false; } // Check if opening for the first time; this helps to speed things up firstRun = self.firstRun = !Object.keys(self.slides).length; // Create slides previous = self.current; self.prevIndex = self.currIndex; self.prevPos = self.currPos; current = self.createSlide(pos); if (groupLen > 1) { if (loop || current.index < groupLen - 1) { self.createSlide(pos + 1); } if (loop || current.index > 0) { self.createSlide(pos - 1); } } self.current = current; self.currIndex = current.index; self.currPos = current.pos; self.trigger("beforeShow", firstRun); self.updateControls(); // Validate duration length current.forcedDuration = undefined; if ($.isNumeric(duration)) { current.forcedDuration = duration; } else { duration = current.opts[firstRun ? "animationDuration" : "transitionDuration"]; } duration = parseInt(duration, 10); // Check if user has swiped the slides or if still animating isMoved = self.isMoved(current); // Make sure current slide is visible current.$slide.addClass("fancybox-slide--current"); // Fresh start - reveal container, current slide and start loading content if (firstRun) { if (current.opts.animationEffect && duration) { self.$refs.container.css("transition-duration", duration + "ms"); } self.$refs.container.addClass("fancybox-is-open").trigger("focus"); // Attempt to load content into slide // This will later call `afterLoad` -> `revealContent` self.loadSlide(current); self.preload("image"); return; } // Get actual slide/stage positions (before cleaning up) slidePos = $.fancybox.getTranslate(previous.$slide); stagePos = $.fancybox.getTranslate(self.$refs.stage); // Clean up all slides $.each(self.slides, function (index, slide) { $.fancybox.stop(slide.$slide, true); }); if (previous.pos !== current.pos) { previous.isComplete = false; } previous.$slide.removeClass("fancybox-slide--complete fancybox-slide--current"); // If slides are out of place, then animate them to correct position if (isMoved) { // Calculate horizontal swipe distance diff = slidePos.left - (previous.pos * slidePos.width + previous.pos * previous.opts.gutter); $.each(self.slides, function (index, slide) { slide.$slide.removeClass("fancybox-animated").removeClass(function (index, className) { return (className.match(/(^|\s)fancybox-fx-\S+/g) || []).join(" "); }); // Make sure that each slide is in equal distance // This is mostly needed for freshly added slides, because they are not yet positioned var leftPos = slide.pos * slidePos.width + slide.pos * slide.opts.gutter; $.fancybox.setTranslate(slide.$slide, { top: 0, left: leftPos - stagePos.left + diff }); if (slide.pos !== current.pos) { slide.$slide.addClass("fancybox-slide--" + (slide.pos > current.pos ? "next" : "previous")); } // Redraw to make sure that transition will start forceRedraw(slide.$slide); // Animate the slide $.fancybox.animate( slide.$slide, { top: 0, left: (slide.pos - current.pos) * slidePos.width + (slide.pos - current.pos) * slide.opts.gutter }, duration, function () { slide.$slide .css({ transform: "", opacity: "" }) .removeClass("fancybox-slide--next fancybox-slide--previous"); if (slide.pos === self.currPos) { self.complete(); } } ); }); } else if (duration && current.opts.transitionEffect) { // Set transition effect for previously active slide prop = "fancybox-animated fancybox-fx-" + current.opts.transitionEffect; previous.$slide.addClass("fancybox-slide--" + (previous.pos > current.pos ? "next" : "previous")); $.fancybox.animate( previous.$slide, prop, duration, function () { previous.$slide.removeClass(prop).removeClass("fancybox-slide--next fancybox-slide--previous"); }, false ); } if (current.isLoaded) { self.revealContent(current); } else { self.loadSlide(current); } self.preload("image"); }, // Create new "slide" element // These are gallery items that are actually added to DOM // ======================================================= createSlide: function (pos) { var self = this, $slide, index; index = pos % self.group.length; index = index < 0 ? self.group.length + index : index; if (!self.slides[pos] && self.group[index]) { $slide = $('<div class="fancybox-slide"></div>').appendTo(self.$refs.stage); self.slides[pos] = $.extend(true, {}, self.group[index], { pos: pos, $slide: $slide, isLoaded: false }); self.updateSlide(self.slides[pos]); } return self.slides[pos]; }, // Scale image to the actual size of the image; // x and y values should be relative to the slide // ============================================== scaleToActual: function (x, y, duration) { var self = this, current = self.current, $content = current.$content, canvasWidth = $.fancybox.getTranslate(current.$slide).width, canvasHeight = $.fancybox.getTranslate(current.$slide).height, newImgWidth = current.width, newImgHeight = current.height, imgPos, posX, posY, scaleX, scaleY; if (self.isAnimating || self.isMoved() || !$content || !(current.type == "image" && current.isLoaded && !current.hasError)) { return; } self.isAnimating = true; $.fancybox.stop($content); x = x === undefined ? canvasWidth * 0.5 : x; y = y === undefined ? canvasHeight * 0.5 : y; imgPos = $.fancybox.getTranslate($content); imgPos.top -= $.fancybox.getTranslate(current.$slide).top; imgPos.left -= $.fancybox.getTranslate(current.$slide).left; scaleX = newImgWidth / imgPos.width; scaleY = newImgHeight / imgPos.height; // Get center position for original image posX = canvasWidth * 0.5 - newImgWidth * 0.5; posY = canvasHeight * 0.5 - newImgHeight * 0.5; // Make sure image does not move away from edges if (newImgWidth > canvasWidth) { posX = imgPos.left * scaleX - (x * scaleX - x); if (posX > 0) { posX = 0; } if (posX < canvasWidth - newImgWidth) { posX = canvasWidth - newImgWidth; } } if (newImgHeight > canvasHeight) { posY = imgPos.top * scaleY - (y * scaleY - y); if (posY > 0) { posY = 0; } if (posY < canvasHeight - newImgHeight) { posY = canvasHeight - newImgHeight; } } self.updateCursor(newImgWidth, newImgHeight); $.fancybox.animate( $content, { top: posY, left: posX, scaleX: scaleX, scaleY: scaleY }, duration || 366, function () { self.isAnimating = false; } ); // Stop slideshow if (self.SlideShow && self.SlideShow.isActive) { self.SlideShow.stop(); } }, // Scale image to fit inside parent element // ======================================== scaleToFit: function (duration) { var self = this, current = self.current, $content = current.$content, end; if (self.isAnimating || self.isMoved() || !$content || !(current.type == "image" && current.isLoaded && !current.hasError)) { return; } self.isAnimating = true; $.fancybox.stop($content); end = self.getFitPos(current); self.updateCursor(end.width, end.height); $.fancybox.animate( $content, { top: end.top, left: end.left, scaleX: end.width / $content.width(), scaleY: end.height / $content.height() }, duration || 366, function () { self.isAnimating = false; } ); }, // Calculate image size to fit inside viewport // =========================================== getFitPos: function (slide) { var self = this, $content = slide.$content, $slide = slide.$slide, width = slide.width || slide.opts.width, height = slide.height || slide.opts.height, maxWidth, maxHeight, minRatio, aspectRatio, rez = {}; if (!slide.isLoaded || !$content || !$content.length) { return false; } maxWidth = $.fancybox.getTranslate(self.$refs.stage).width; maxHeight = $.fancybox.getTranslate(self.$refs.stage).height; maxWidth -= parseFloat($slide.css("paddingLeft")) + parseFloat($slide.css("paddingRight")) + parseFloat($content.css("marginLeft")) + parseFloat($content.css("marginRight")); maxHeight -= parseFloat($slide.css("paddingTop")) + parseFloat($slide.css("paddingBottom")) + parseFloat($content.css("marginTop")) + parseFloat($content.css("marginBottom")); if (!width || !height) { width = maxWidth; height = maxHeight; } minRatio = Math.min(1, maxWidth / width, maxHeight / height); width = minRatio * width; height = minRatio * height; // Adjust width/height to precisely fit into container if (width > maxWidth - 0.5) { width = maxWidth; } if (height > maxHeight - 0.5) { height = maxHeight; } if (slide.type === "image") { rez.top = Math.floor((maxHeight - height) * 0.5) + parseFloat($slide.css("paddingTop")); rez.left = Math.floor((maxWidth - width) * 0.5) + parseFloat($slide.css("paddingLeft")); } else if (slide.contentType === "video") { // Force aspect ratio for the video // "I say the whole world must learn of our peaceful ways… by force!" aspectRatio = slide.opts.width && slide.opts.height ? width / height : slide.opts.ratio || 16 / 9; if (height > width / aspectRatio) { height = width / aspectRatio; } else if (width > height * aspectRatio) { width = height * aspectRatio; } } rez.width = width; rez.height = height; return rez; }, // Update content size and position for all slides // ============================================== update: function (e) { var self = this; $.each(self.slides, function (key, slide) { self.updateSlide(slide, e); }); }, // Update slide content position and size // ====================================== updateSlide: function (slide, e) { var self = this, $content = slide && slide.$content, width = slide.width || slide.opts.width, height = slide.height || slide.opts.height, $slide = slide.$slide; // First, prevent caption overlap, if needed self.adjustCaption(slide); // Then resize content to fit inside the slide if ($content && (width || height || slide.contentType === "video") && !slide.hasError) { $.fancybox.stop($content); $.fancybox.setTranslate($content, self.getFitPos(slide)); if (slide.pos === self.currPos) { self.isAnimating = false; self.updateCursor(); } } // Then some adjustments self.adjustLayout(slide); if ($slide.length) { $slide.trigger("refresh"); if (slide.pos === self.currPos) { self.$refs.toolbar .add(self.$refs.navigation.find(".fancybox-button--arrow_right")) .toggleClass("compensate-for-scrollbar", $slide.get(0).scrollHeight > $slide.get(0).clientHeight); } } self.trigger("onUpdate", slide, e); }, // Horizontally center slide // ========================= centerSlide: function (duration) { var self = this, current = self.current, $slide = current.$slide; if (self.isClosing || !current) { return; } $slide.siblings().css({ transform: "", opacity: "" }); $slide .parent() .children() .removeClass("fancybox-slide--previous fancybox-slide--next"); $.fancybox.animate( $slide, { top: 0, left: 0, opacity: 1 }, duration === undefined ? 0 : duration, function () { // Clean up $slide.css({ transform: "", opacity: "" }); if (!current.isComplete) { self.complete(); } }, false ); }, // Check if current slide is moved (swiped) // ======================================== isMoved: function (slide) { var current = slide || this.current, slidePos, stagePos; if (!current) { return false; } stagePos = $.fancybox.getTranslate(this.$refs.stage); slidePos = $.fancybox.getTranslate(current.$slide); return ( !current.$slide.hasClass("fancybox-animated") && (Math.abs(slidePos.top - stagePos.top) > 0.5 || Math.abs(slidePos.left - stagePos.left) > 0.5) ); }, // Update cursor style depending if content can be zoomed // ====================================================== updateCursor: function (nextWidth, nextHeight) { var self = this, current = self.current, $container = self.$refs.container, canPan, isZoomable; if (!current || self.isClosing || !self.Guestures) { return; } $container.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-zoomOut fancybox-can-swipe fancybox-can-pan"); canPan = self.canPan(nextWidth, nextHeight); isZoomable = canPan ? true : self.isZoomable(); $container.toggleClass("fancybox-is-zoomable", isZoomable); $("[data-fancybox-zoom]").prop("disabled", !isZoomable); if (canPan) { $container.addClass("fancybox-can-pan"); } else if ( isZoomable && (current.opts.clickContent === "zoom" || ($.isFunction(current.opts.clickContent) && current.opts.clickContent(current) == "zoom")) ) { $container.addClass("fancybox-can-zoomIn"); } else if (current.opts.touch && (current.opts.touch.vertical || self.group.length > 1) && current.contentType !== "video") { $container.addClass("fancybox-can-swipe"); } }, // Check if current slide is zoomable // ================================== isZoomable: function () { var self = this, current = self.current, fitPos; // Assume that slide is zoomable if: // - image is still loading // - actual size of the image is smaller than available area if (current && !self.isClosing && current.type === "image" && !current.hasError) { if (!current.isLoaded) { return true; } fitPos = self.getFitPos(current); if (fitPos && (current.width > fitPos.width || current.height > fitPos.height)) { return true; } } return false; }, // Check if current image dimensions are smaller than actual // ========================================================= isScaledDown: function (nextWidth, nextHeight) { var self = this, rez = false, current = self.current, $content = current.$content; if (nextWidth !== undefined && nextHeight !== undefined) { rez = nextWidth < current.width && nextHeight < current.height; } else if ($content) { rez = $.fancybox.getTranslate($content)