\
-
\
-
\
-

\
-
\
-
\
-
\
-
\
-
\
- ').appendTo($('body'));
-
- // Cache jQuery objects
- this.$lightbox = $('#lightbox');
- this.$overlay = $('#lightboxOverlay');
- this.$outerContainer = this.$lightbox.find('.lb-outerContainer');
- this.$container = this.$lightbox.find('.lb-container');
- this.$image = this.$lightbox.find('.lb-image');
- this.$nav = this.$lightbox.find('.lb-nav');
-
- if(self.options.hasVideo) {
- this.$video = $('
');
- this.$image.after(this.$video);
- this.videoBorderWidth = {
- top: parseInt(this.$video.css('border-top-width'), 10),
- right: parseInt(this.$video.css('border-right-width'), 10),
- bottom: parseInt(this.$video.css('border-bottom-width'), 10),
- left: parseInt(this.$video.css('border-left-width'), 10)
- };
- }
-
- // Store css values for future lookup
- this.containerPadding = {
- top: parseInt(this.$container.css('padding-top'), 10),
- right: parseInt(this.$container.css('padding-right'), 10),
- bottom: parseInt(this.$container.css('padding-bottom'), 10),
- left: parseInt(this.$container.css('padding-left'), 10)
- };
-
- this.imageBorderWidth = {
- top: parseInt(this.$image.css('border-top-width'), 10),
- right: parseInt(this.$image.css('border-right-width'), 10),
- bottom: parseInt(this.$image.css('border-bottom-width'), 10),
- left: parseInt(this.$image.css('border-left-width'), 10)
- };
-
- // Attach event handlers to the newly minted DOM elements
- this.$overlay.hide().add(this.$lightbox.find('.lb-dataContainer')).on('click', function() {
- self.end();
- return false;
- });
-
- this.$lightbox.hide().on('click', function(event) {
- if ($(event.target).attr('id') === 'lightbox') {
- self.end();
- }
- });
-
- this.$outerContainer.on('click', function(event) {
- if ($(event.target).attr('id') === 'lightbox') {
- self.end();
- }
- return false;
- });
-
- this.$lightbox.find('.lb-prev').on('click', function() {
- if (self.currentImageIndex === 0) {
- self.changeImage(self.album.length - 1);
- } else {
- self.changeImage(self.currentImageIndex - 1);
- }
- return false;
- });
-
- this.$lightbox.find('.lb-next').on('click', function() {
- if (self.currentImageIndex === self.album.length - 1) {
- self.changeImage(0);
- } else {
- self.changeImage(self.currentImageIndex + 1);
- }
- return false;
- });
-
- /*
- Show context menu for image on right-click
-
- There is a div containing the navigation that spans the entire image and lives above of it. If
- you right-click, you are right clicking this div and not the image. This prevents users from
- saving the image or using other context menu actions with the image.
-
- To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
- set pointer-events to none on the nav div. This is so that the upcoming right-click event on
- the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
- we set the pointer events back to auto for the nav div so it can capture hover and left-click
- events as usual.
- */
- this.$nav.on('mousedown', function(event) {
- if (event.which === 3) {
- self.$nav.css('pointer-events', 'none');
-
- self.$lightbox.one('contextmenu', function() {
- setTimeout(function() {
- this.$nav.css('pointer-events', 'auto');
- }.bind(self), 0);
- });
- }
- });
-
-
- this.$lightbox.find('.lb-loader, .lb-close').on('click keyup', function(e) {
- // If mouse click OR 'enter' or 'space' keypress, close LB
- if (
- e.type === 'click' || (e.type === 'keyup' && (e.which === 13 || e.which === 32))) {
- self.end();
- return false;
- }
- });
- };
-
- // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
- Lightbox.prototype.start = function($link) {
- var self = this;
- var $window = $(window);
-
- $window.on('resize', $.proxy(this.sizeOverlay, this));
-
- this.sizeOverlay();
-
- //Manage Zoom Event
- this.$nav.mousewheel((e) => {
- var asImg = self.album[this.currentImageIndex];
- if(!asImg.type != 'video') {
- let asTransform = this.$image.css('transform').replace(/[^0-9\-.,]/g, '').split(',');
- let fOldZoom = parseFloat(asTransform[0] || 1);
- let fOldTranslateX = parseFloat(asTransform[4] || 0);
- let fOldTranslateY = parseFloat(asTransform[5] || 0);
- let fNewZoom = Math.min(Math.max(fOldZoom + e.deltaY / 10, 1), Math.max(asImg.width/this.$image.width(), asImg.height/this.$image.height()));
-
- let fTransX = fOldTranslateX + (fNewZoom - fOldZoom) * (this.$image.width()/2 - e.offsetX);
- let fTransY = fOldTranslateY + (fNewZoom - fOldZoom) * (this.$image.height()/2 - e.offsetY);
- let fTransMaxX = (fNewZoom - 1) * this.$image.width() / 2;
- let fTransMaxY = (fNewZoom - 1) * this.$image.height() / 2;
-
- fTransX = Math.max(Math.min(fTransX, fTransMaxX), fTransMaxX * -1);
- fTransY = Math.max(Math.min(fTransY, fTransMaxY), fTransMaxY * -1);
-
- this.$image.css('--scale', fNewZoom);
- this.$container.toggleClass('moveable', (fNewZoom > 1));
- this.$image.css('--translate-x', fTransX+'px');
- this.$image.css('--translate-y', fTransY+'px');
- }
- });
-
- //Manage Repositioning Event
- this.$nav.on('mousedown', (e) => {
- if(this.$image.css('--scale') > 1) {
- //The following block gets the X/Y offset (the difference between where it starts and where it was clicked)
- this.gMouseDownOffsetX = e.clientX - parseFloat(this.$image.css('--translate-x') || 0);
- this.gMouseDownOffsetY = e.clientY - parseFloat(this.$image.css('--translate-y') || 0);
-
- //Change cursor
- this.$container.addClass('moving');
-
- $window.on('mousemove', divMove);
- }
- });
-
- $window.on('mouseup', () => {
- $window.off('mousemove', divMove);
- this.$container.removeClass('moving');
- });
-
- function divMove(e){
- let iZoom = self.$image.css('--scale');
- let fTransX = e.clientX - self.gMouseDownOffsetX;
- let fTransY = e.clientY - self.gMouseDownOffsetY;
- let fTransMaxX = (iZoom - 1) * self.$image.width() / 2;
- let fTransMaxY = (iZoom - 1) * self.$image.height() / 2;
-
- fTransX = Math.max(Math.min(fTransX, fTransMaxX), fTransMaxX * -1);
- fTransY = Math.max(Math.min(fTransY, fTransMaxY), fTransMaxY * -1);
-
- self.$image.css('--translate-x', fTransX + 'px');
- self.$image.css('--translate-y', fTransY + 'px');
- }
-
- this.album = [];
- var imageNumber = 0;
-
- // Support both data-lightbox attribute and rel attribute implementations
- var dataLightboxValue = $link.attr('data-lightbox');
- var $links;
-
- if (dataLightboxValue) {
- $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
- for (var i = 0; i < $links.length; i = ++i) {
- this.addToAlbum($($links[i]));
- if ($links[i] === $link[0]) {
- imageNumber = i;
- }
- }
- } else {
- if ($link.attr('rel') === 'lightbox') {
- // If image is not part of a set
- this.addToAlbum($link);
- } else {
- // If image is part of a set
- $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
- for (var j = 0; j < $links.length; j = ++j) {
- this.addToAlbum($($links[j]));
- if ($links[j] === $link[0]) {
- imageNumber = j;
- }
- }
- }
- }
-
- // Position Lightbox
- this.$lightbox.fadeIn(this.options.fadeDuration);
-
- // Disable scrolling of the page while open
- if (this.options.disableScrolling) {
- $('body').addClass('lb-disable-scrolling');
- }
-
- this.changeImage(imageNumber);
- };
-
- Lightbox.prototype.addToAlbum = function($link) {
- this.album.push({
- alt: $link.attr('data-alt'),
- link: $link.attr('href'),
- title: $link.attr('data-title') || $link.attr('title'),
-
- orientation: $link.attr('data-orientation'),
- type: $link.attr('data-type'),
- id: $link.attr('data-id'),
- $Media: $link.attr('data-type')=='video'?this.$video:this.$image,
- width: $link.find('img').attr('width'),
- height: $link.find('img').attr('height'),
- set: $link.attr('data-lightbox') || $link.attr('rel')
- });
- }
-
- Lightbox.prototype.getMaxSizes = function(iMediaWidth, iMediaHeight, sMediaType) {
- var iWindowWidth = $(window).width();
- var iWindowHeight = $(window).height();
- var oBorder = (sMediaType=='image')?this.imageBorderWidth:this.videoBorderWidth;
- var iMaxMediaWidth = iWindowWidth - this.containerPadding.left - this.containerPadding.right - oBorder.left - oBorder.right;
- var iMaxMediaHeight = iWindowHeight - this.containerPadding.top - this.containerPadding.bottom - oBorder.top - oBorder.bottom - this.options.positionFromTop;
-
- var iDataMaxWidth = this.$lightbox.find('.lb-dataContainer').width(), iDataMaxHeight = this.$lightbox.find('.lb-dataContainer').height();
- var iImageRatio = iMediaWidth / iMediaHeight;
-
- //Case horizontal
- var iHeightH = Math.min(iMaxMediaHeight, iMediaHeight);
- var iWidthH = Math.min(iHeightH * iImageRatio, iMaxMediaWidth - iDataMaxWidth);
- var iSurfaceH = Math.min(iHeightH, iWidthH / iImageRatio) * iWidthH;
-
- //Case vertical
- var iWidthV = Math.min(iMaxMediaWidth, iMediaWidth);
- var iHeightV = Math.min(iWidthV / iImageRatio, iMaxMediaHeight - iDataMaxHeight);
- var iSurfaceV = Math.min(iWidthV, iHeightV * iImageRatio) * iHeightV;
-
- var sDirection = (iSurfaceV > iSurfaceH)?'vertical':'horizontal';
-
- if(sDirection == 'vertical') iMaxMediaHeight -= iDataMaxHeight;
- else iMaxMediaWidth -= iDataMaxWidth;
-
- return {maxWidth: iMaxMediaWidth, maxHeight: iMaxMediaHeight, direction: sDirection};
- };
-
- Lightbox.prototype.updateSize = function(iMediaNumber) {
- var oMedia = this.album[iMediaNumber];
- var sFileType = oMedia.link.split('.').slice(-1)[0];
- var oMaxSizes = this.getMaxSizes(oMedia.width, oMedia.height, oMedia.type);
- var iMaxMediaWidth = oMaxSizes.maxWidth;
- var iMaxMediaHeight = oMaxSizes.maxHeight;
- this.$lightbox.removeClass('vertical horizontal').addClass(oMaxSizes.direction);
-
- /*
- Since many SVGs have small intrinsic dimensions, but they support scaling
- up without quality loss because of their vector format, max out their
- size.
- */
- if(sFileType === 'svg') {
- oMedia.$Media.width(iMaxMediaWidth);
- oMedia.$Media.height(iMaxMediaHeight);
- }
-
- if(this.options.fitImagesInViewport) {
- //Check if image size is larger than maxWidth|maxHeight in settings
- if(this.options.maxWidth && this.options.maxWidth < iMaxMediaWidth) iMaxMediaWidth = this.options.maxWidth;
- if(this.options.maxHeight && this.options.maxHeight < iMaxMediaHeight) iMaxMediaHeight = this.options.maxHeight;
- }
- else {
- iMaxMediaWidth = this.options.maxWidth || oMedia.width || iMaxMediaWidth;
- iMaxMediaHeight = this.options.maxHeight || oMedia.height || iMaxMediaHeight;
- }
-
- //Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
- //option than we need to size down while maintaining the aspect ratio.
- var iMediaFinalWidth, iMediaFinalHeight;
- if((oMedia.width > iMaxMediaWidth) || (oMedia.height > iMaxMediaHeight)) {
- if ((oMedia.width / iMaxMediaWidth) > (oMedia.height / iMaxMediaHeight)) {
- iMediaFinalWidth = iMaxMediaWidth;
- iMediaFinalHeight = Math.round(oMedia.height / (oMedia.width / iMaxMediaWidth));
- } else {
- iMediaFinalWidth = Math.round(oMedia.width / (oMedia.height / iMaxMediaHeight));
- iMediaFinalHeight = iMaxMediaHeight;
- }
- }
- else {
- iMediaFinalWidth = oMedia.width;
- iMediaFinalHeight = oMedia.height;
- }
-
- oMedia.$Media.width(iMediaFinalWidth);
- oMedia.$Media.height(iMediaFinalHeight);
- this.sizeContainer(iMediaFinalWidth, iMediaFinalHeight, oMedia.type);
- };
-
- // Hide most UI elements in preparation for the animated resizing of the lightbox.
- Lightbox.prototype.changeImage = function(imageNumber) {
- var self = this;
- var filename = this.album[imageNumber].link;
-
- // Disable keyboard nav during transitions
- this.disableKeyboardNav();
-
- // Show loading state
- this.$overlay.fadeIn(this.options.fadeDuration);
- $('.lb-loader').fadeIn('slow');
-
- this.$lightbox.find('.lb-image, .lb-video, .lb-nav, .lb-prev, .lb-next, .lb-number, .lb-caption, .lb-close').hide();
- this.$image.css({'--scale': '1', '--translate-x': '0', '--translate-y': '0'});
- self.$lightbox.find('.lb-dataContainer').css({width:'200px', height:'30px'});
- this.$outerContainer.addClass('animating');
- this.$container.removeClass('moveable moving');
-
- this.options.onMediaChange(self.album[imageNumber]);
-
- var $hasVideoNav = this.$container.hasClass('lb-video-nav');
- switch(self.album[imageNumber].type) {
- case 'video':
- self.$image.removeAttr('src');
- this.$video.on('loadedmetadata', function(){
- self.album[imageNumber].width = this.videoWidth;
- self.album[imageNumber].height = this.videoHeight;
- self.updateSize(imageNumber);
- $(this).off('loadedmetadata');
- });
-
- this.$video.attr('src', filename);
-
- if(!$hasVideoNav) this.$container.addClass('lb-video-nav');
- break;
- case 'image':
- this.$video.trigger('pause').removeAttr('src');
- if($hasVideoNav) this.$container.removeClass('lb-video-nav');
-
- // When image to show is preloaded, we send the width and height to sizeContainer()
- var preloader = new Image();
- preloader.onload = function(){
- self.$image.attr({
- 'alt': self.album[imageNumber].alt,
- 'src': filename
- });
-
- //Orientation management
- if(Math.abs(self.album[imageNumber].orientation) == 90 && preloader.width > preloader.height) {
- var sWidth = preloader.width;
- preloader.width = preloader.height;
- preloader.height = sWidth;
- }
- self.album[imageNumber].width = preloader.width;
- self.album[imageNumber].height = preloader.height;
-
- self.updateSize(imageNumber);
- };
-
- // Preload image before showing
- preloader.src = this.album[imageNumber].link;
- break;
- }
-
- this.currentImageIndex = imageNumber;
- };
-
- // Stretch overlay to fit the viewport
- Lightbox.prototype.sizeOverlay = function(e) {
- /*
- We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
- Why do this? If the `disableScrolling` option is set to true, a class is added to the body
- tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
- hidden before we measure the document width, as the presence of the scrollbar will affect the
- number.
- */
- if(e) {
- if(typeof oResizeTimer != 'undefined') clearTimeout(oResizeTimer);
- oResizeTimer = setTimeout(
- () => {
- switch(this.album[this.currentImageIndex].type) {
- case 'image':
- this.changeImage(this.currentImageIndex);
- break;
- case 'video':
- this.updateSize(this.currentImageIndex);
- break;
- }
- },
- 200
- );
- }
- };
-
- // Animate the size of the lightbox to fit the image we are showing
- // This method also shows the the image.
- //Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
- Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight, media) {
- media = media || 'image';
- var self = this;
-
- var oldWidth = this.$outerContainer.outerWidth();
- var oldHeight = this.$outerContainer.outerHeight();
- //var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
- //var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
- var mediaBorderWidth = (media=='image')?this.imageBorderWidth:this.videoBorderWidth;
- var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + mediaBorderWidth.left + mediaBorderWidth.right;
- var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + mediaBorderWidth.top + mediaBorderWidth.bottom;
-
- function postResize() {
- if(self.$lightbox.hasClass('vertical')) self.$lightbox.find('.lb-dataContainer').width(newWidth);
- else self.$lightbox.find('.lb-dataContainer').height(newHeight);
- self.$lightbox.find('.lb-prevLink').height(newHeight);
- self.$lightbox.find('.lb-nextLink').height(newHeight);
-
- // Set focus on one of the two root nodes so keyboard events are captured.
- self.$overlay.trigger('focus');
-
- self.showImage();
- }
-
- if (oldWidth !== newWidth || oldHeight !== newHeight) {
- this.$outerContainer.animate({
- width: newWidth,
- height: newHeight
- }, this.options.resizeDuration, 'swing', function() {
- postResize();
- });
- } else {
- postResize();
- }
- };
-
- // Display the image and its details and begin preload neighboring images.
- Lightbox.prototype.showImage = function() {
- this.$lightbox.find('.lb-loader').stop(true).hide();
-
- if(this.options.hasVideo && this.album[this.currentImageIndex].type == 'video') this.$lightbox.find('.lb-video').fadeIn(this.options.imageFadeDuration);
- else this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
-
- this.updateNav();
- this.updateDetails();
- this.preloadNeighboringImages();
- this.enableKeyboardNav();
- };
-
- // Display previous and next navigation if appropriate.
- Lightbox.prototype.updateNav = function() {
- // Check to see if the browser supports touch events. If so, we take the conservative approach
- // and assume that mouse hover events are not supported and always show prev/next navigation
- // arrows in image sets.
- var alwaysShowNav = false;
- try {
- document.createEvent('TouchEvent');
- alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
- } catch (e) {}
-
- this.$lightbox.find('.lb-nav').show();
-
- if (this.album.length > 1) {
- if (this.options.wrapAround) {
- if (alwaysShowNav) {
- this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
- }
- this.$lightbox.find('.lb-prev, .lb-next').show();
- } else {
- if (this.currentImageIndex > 0) {
- this.$lightbox.find('.lb-prev').show();
- if (alwaysShowNav) {
- this.$lightbox.find('.lb-prev').css('opacity', '1');
- }
- }
- if (this.currentImageIndex < this.album.length - 1) {
- this.$lightbox.find('.lb-next').show();
- if (alwaysShowNav) {
- this.$lightbox.find('.lb-next').css('opacity', '1');
- }
- }
- }
- }
- };
-
- // Display caption, image number, and closing button.
- Lightbox.prototype.updateDetails = function() {
- var self = this;
-
- // Enable anchor clicks in the injected caption html.
- // Thanks Nate Wright for the fix. @https://github.com/NateWr
- if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
- this.album[this.currentImageIndex].title !== '') {
- var $caption = this.$lightbox.find('.lb-caption');
- if (this.options.sanitizeTitle) {
- $caption.text(this.album[this.currentImageIndex].title);
- } else {
- $caption.html(this.album[this.currentImageIndex].title);
- }
- $caption.add(this.$lightbox.find('.lb-close')).fadeIn('fast');
- }
-
- this.$outerContainer.removeClass('animating');
-
- this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
- return self.sizeOverlay();
- });
- };
-
- // Preload previous and next images in set.
- Lightbox.prototype.preloadNeighboringImages = function() {
- if (this.album.length > this.currentImageIndex + 1 && this.album[this.currentImageIndex + 1].type == 'image') {
- var preloadNext = new Image();
- preloadNext.src = this.album[this.currentImageIndex + 1].link;
- }
- if (this.currentImageIndex > 0 && this.album[this.currentImageIndex - 1].type == 'image') {
- var preloadPrev = new Image();
- preloadPrev.src = this.album[this.currentImageIndex - 1].link;
- }
- };
-
- Lightbox.prototype.enableKeyboardNav = function() {
- this.disableKeyboardNav();
- this.$lightbox.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
- this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
- };
-
- Lightbox.prototype.disableKeyboardNav = function() {
- this.$lightbox.off('.keyboard');
- this.$overlay.off('.keyboard');
- };
-
- Lightbox.prototype.keyboardAction = function(event) {
- var KEYCODE_ESC = 27;
- var KEYCODE_LEFTARROW = 37;
- var KEYCODE_RIGHTARROW = 39;
-
- var keycode = event.keyCode;
- if (keycode === KEYCODE_ESC) {
- // Prevent bubbling so as to not affect other components on the page.
- event.stopPropagation();
- this.end();
- } else if (keycode === KEYCODE_LEFTARROW) {
- if (this.currentImageIndex !== 0) {
- this.changeImage(this.currentImageIndex - 1);
- } else if (this.options.wrapAround && this.album.length > 1) {
- this.changeImage(this.album.length - 1);
- }
- } else if (keycode === KEYCODE_RIGHTARROW) {
- if (this.currentImageIndex !== this.album.length - 1) {
- this.changeImage(this.currentImageIndex + 1);
- } else if (this.options.wrapAround && this.album.length > 1) {
- this.changeImage(0);
- }
- }
- };
-
- // Closing time. :-(
- Lightbox.prototype.end = function() {
- this.disableKeyboardNav();
-
- if(this.options.hasVideo) {
- var $lbContainer = this.$lightbox.find('.lb-container');
- var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
- this.$video.trigger('pause').removeAttr('src');
-
- if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
- }
-
- $(window).off('resize', this.sizeOverlay);
- this.$nav.off('mousewheel');
- this.$lightbox.fadeOut(this.options.fadeDuration);
- this.$overlay.fadeOut(this.options.fadeDuration);
-
- if (this.options.disableScrolling) {
- $('body').removeClass('lb-disable-scrolling');
- }
-
- this.options.onClosing();
- };
-
- return new Lightbox();
-}));
+const defaults = {
+ albumLabel: 'Image %1 of %2',
+ alwaysShowNavOnTouchDevices: false,
+ fadeDuration: 600,
+ fitImagesInViewport: true,
+ imageFadeDuration: 600,
+ positionFromTop: 50,
+ resizeDuration: 700,
+ wrapAround: false,
+ disableScrolling: false,
+ sanitizeTitle: false,
+ hasVideo: true,
+ onMediaChange: () => {},
+ onClosing: () => {}
+};
+
+class Lightbox {
+ constructor() {
+ this.album = [];
+ this.currentImageIndex = 0;
+ this.options = { ...defaults };
+ this.gMouseDownOffsetX = 0;
+ this.gMouseDownOffsetY = 0;
+ this.resizeTimer = null;
+ this.boundOnBodyClick = this.onBodyClick.bind(this);
+ this.boundOnResize = this.sizeOverlay.bind(this);
+ this.boundOnKeyUp = this.keyboardAction.bind(this);
+ this.boundOnWheel = this.onWheel.bind(this);
+ this.boundOnDragStart = this.onDragStart.bind(this);
+ this.boundOnDragMove = this.onDragMove.bind(this);
+ this.boundOnDragEnd = this.onDragEnd.bind(this);
+ this.init();
+ }
+
+ option(options = {}) {
+ Object.assign(this.options, options);
+ }
+
+ init() {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ this.enable();
+ this.build();
+ }, { once: true });
+ } else {
+ this.enable();
+ this.build();
+ }
+ }
+
+ enable() {
+ document.body.addEventListener('click', this.boundOnBodyClick);
+ }
+
+ onBodyClick(event) {
+ const link = event.target.closest('a[rel^="lightbox"], area[rel^="lightbox"], a[data-lightbox], area[data-lightbox]');
+ if (!link) return;
+ event.preventDefault();
+ this.start(link);
+ }
+
+ build() {
+ if (document.getElementById('lightbox')) return;
+
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = `
+
+
+
+
+

+
+
+
+
+
+
+ `;
+ document.body.append(...wrapper.children);
+
+ this.overlay = document.getElementById('lightboxOverlay');
+ this.lightbox = document.getElementById('lightbox');
+ this.outerContainer = this.lightbox.querySelector('.lb-outerContainer');
+ this.container = this.lightbox.querySelector('.lb-container');
+ this.image = this.lightbox.querySelector('.lb-image');
+ this.nav = this.lightbox.querySelector('.lb-nav');
+ this.loader = this.lightbox.querySelector('.lb-loader');
+ this.caption = this.lightbox.querySelector('.lb-caption');
+ this.closeButton = this.lightbox.querySelector('.lb-close');
+ this.dataContainer = this.lightbox.querySelector('.lb-dataContainer');
+ this.prev = this.lightbox.querySelector('.lb-prev');
+ this.next = this.lightbox.querySelector('.lb-next');
+ this.video = document.createElement('video');
+ this.video.className = 'lb-video';
+ this.video.controls = true;
+ this.video.autoplay = true;
+ this.image.insertAdjacentElement('afterend', this.video);
+
+ this.overlay.style.display = 'none';
+ this.lightbox.style.display = 'none';
+
+ this.containerPadding = this.getBoxMetrics(this.container, 'padding');
+ this.imageBorderWidth = this.getBoxMetrics(this.image, 'border');
+ this.videoBorderWidth = this.getBoxMetrics(this.video, 'border');
+
+ this.overlay.addEventListener('click', () => this.end());
+ this.dataContainer.addEventListener('click', () => this.end());
+ this.lightbox.addEventListener('click', (event) => {
+ if (event.target === this.lightbox) this.end();
+ });
+ this.outerContainer.addEventListener('click', (event) => {
+ if (event.target === this.outerContainer) this.end();
+ event.stopPropagation();
+ });
+
+ this.prev.addEventListener('click', (event) => {
+ event.preventDefault();
+ if (this.currentImageIndex === 0) this.changeImage(this.album.length - 1);
+ else this.changeImage(this.currentImageIndex - 1);
+ });
+ this.next.addEventListener('click', (event) => {
+ event.preventDefault();
+ if (this.currentImageIndex === this.album.length - 1) this.changeImage(0);
+ else this.changeImage(this.currentImageIndex + 1);
+ });
+
+ this.loader.addEventListener('click', (event) => {
+ event.preventDefault();
+ this.end();
+ });
+ this.closeButton.addEventListener('click', (event) => {
+ event.preventDefault();
+ this.end();
+ });
+ this.closeButton.addEventListener('keyup', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') this.end();
+ });
+
+ this.nav.addEventListener('wheel', this.boundOnWheel, { passive: false });
+ this.nav.addEventListener('mousedown', this.boundOnDragStart);
+ window.addEventListener('mouseup', this.boundOnDragEnd);
+ }
+
+ getBoxMetrics(element, type) {
+ const styles = getComputedStyle(element);
+ return {
+ top: parseInt(styles[`${type}-top-width`], 10) || 0,
+ right: parseInt(styles[`${type}-right-width`], 10) || 0,
+ bottom: parseInt(styles[`${type}-bottom-width`], 10) || 0,
+ left: parseInt(styles[`${type}-left-width`], 10) || 0
+ };
+ }
+
+ start(link) {
+ this.sizeOverlay();
+ this.album = [];
+ let imageNumber = 0;
+ const setName = link.getAttribute('data-lightbox');
+
+ if (setName) {
+ const links = [...document.querySelectorAll(`${link.tagName}[data-lightbox="${CSS.escape(setName)}"]`)];
+ links.forEach((item, index) => {
+ this.addToAlbum(item);
+ if (item === link) imageNumber = index;
+ });
+ } else if (link.getAttribute('rel') === 'lightbox') {
+ this.addToAlbum(link);
+ } else {
+ const rel = link.getAttribute('rel');
+ const links = [...document.querySelectorAll(`${link.tagName}[rel="${CSS.escape(rel)}"]`)];
+ links.forEach((item, index) => {
+ this.addToAlbum(item);
+ if (item === link) imageNumber = index;
+ });
+ }
+
+ this.fade(this.overlay, true, this.options.fadeDuration);
+ this.fade(this.lightbox, true, this.options.fadeDuration);
+
+ if (this.options.disableScrolling) document.body.classList.add('lb-disable-scrolling');
+
+ window.addEventListener('resize', this.boundOnResize);
+ this.changeImage(imageNumber);
+ }
+
+ addToAlbum(link) {
+ const img = link.querySelector('img');
+ this.album.push({
+ alt: link.getAttribute('data-alt') || '',
+ link: link.getAttribute('href'),
+ title: link.getAttribute('data-title') || link.getAttribute('title') || '',
+ orientation: parseInt(link.getAttribute('data-orientation') || '0', 10),
+ type: link.getAttribute('data-type') || 'image',
+ id: link.getAttribute('data-id'),
+ width: parseInt(img?.getAttribute('width') || '0', 10),
+ height: parseInt(img?.getAttribute('height') || '0', 10),
+ set: link.getAttribute('data-lightbox') || link.getAttribute('rel') || ''
+ });
+ }
+
+ getMaxSizes(mediaWidth, mediaHeight, mediaType) {
+ let maxWidth = window.innerWidth - this.containerPadding.left - this.containerPadding.right;
+ let maxHeight = window.innerHeight - this.containerPadding.top - this.containerPadding.bottom - this.options.positionFromTop;
+ const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
+ maxWidth -= border.left + border.right;
+ maxHeight -= border.top + border.bottom;
+
+ const dataMaxWidth = this.dataContainer.offsetWidth || 0;
+ const dataMaxHeight = this.dataContainer.offsetHeight || 0;
+ const ratio = mediaWidth / mediaHeight;
+
+ const heightH = Math.min(maxHeight, mediaHeight);
+ const widthH = Math.min(heightH * ratio, maxWidth - dataMaxWidth);
+ const surfaceH = Math.min(heightH, widthH / ratio) * widthH;
+
+ const widthV = Math.min(maxWidth, mediaWidth);
+ const heightV = Math.min(widthV / ratio, maxHeight - dataMaxHeight);
+ const surfaceV = Math.min(widthV, heightV * ratio) * heightV;
+
+ const direction = surfaceV > surfaceH ? 'vertical' : 'horizontal';
+ if (direction === 'vertical') maxHeight -= dataMaxHeight;
+ else maxWidth -= dataMaxWidth;
+
+ return { maxWidth, maxHeight, direction };
+ }
+
+ updateSize(index) {
+ const media = this.album[index];
+ const maxSizes = this.getMaxSizes(media.width, media.height, media.type);
+ let maxWidth = maxSizes.maxWidth;
+ let maxHeight = maxSizes.maxHeight;
+
+ this.lightbox.classList.remove('vertical', 'horizontal');
+ this.lightbox.classList.add(maxSizes.direction);
+
+ if (this.options.fitImagesInViewport) {
+ if (this.options.maxWidth && this.options.maxWidth < maxWidth) maxWidth = this.options.maxWidth;
+ if (this.options.maxHeight && this.options.maxHeight < maxHeight) maxHeight = this.options.maxHeight;
+ } else {
+ maxWidth = this.options.maxWidth || media.width || maxWidth;
+ maxHeight = this.options.maxHeight || media.height || maxHeight;
+ }
+
+ let finalWidth;
+ let finalHeight;
+ if (media.width > maxWidth || media.height > maxHeight) {
+ if ((media.width / maxWidth) > (media.height / maxHeight)) {
+ finalWidth = maxWidth;
+ finalHeight = Math.round(media.height / (media.width / maxWidth));
+ } else {
+ finalWidth = Math.round(media.width / (media.height / maxHeight));
+ finalHeight = maxHeight;
+ }
+ } else {
+ finalWidth = media.width;
+ finalHeight = media.height;
+ }
+
+ const target = media.type === 'video' ? this.video : this.image;
+ target.width = finalWidth;
+ target.height = finalHeight;
+ this.sizeContainer(finalWidth, finalHeight, media.type);
+ }
+
+ changeImage(index) {
+ const media = this.album[index];
+ if (!media) return;
+
+ this.disableKeyboardNav();
+ this.fade(this.overlay, true, this.options.fadeDuration);
+ this.fade(this.loader, true, 200);
+ this.hideElements([this.image, this.video, this.nav, this.prev, this.next, this.caption, this.closeButton]);
+ this.resetImageTransform();
+ this.dataContainer.style.width = '200px';
+ this.dataContainer.style.height = '30px';
+ this.outerContainer.classList.add('animating');
+ this.container.classList.remove('moveable', 'moving', 'lb-video-nav');
+
+ this.options.onMediaChange(media);
+
+ if (media.type === 'video') {
+ this.image.removeAttribute('src');
+ this.container.classList.add('lb-video-nav');
+ this.video.onloadedmetadata = () => {
+ media.width = this.video.videoWidth;
+ media.height = this.video.videoHeight;
+ this.video.onloadedmetadata = null;
+ this.updateSize(index);
+ };
+ this.video.src = media.link;
+ } else {
+ this.video.pause();
+ this.video.removeAttribute('src');
+ this.image.onload = () => {
+ this.image.alt = media.alt;
+ let width = this.image.naturalWidth;
+ let height = this.image.naturalHeight;
+ if (Math.abs(media.orientation) === 90 && width > height) {
+ const tmp = width;
+ width = height;
+ height = tmp;
+ }
+ media.width = width;
+ media.height = height;
+ this.image.onload = null;
+ this.updateSize(index);
+ };
+ this.image.src = media.link;
+ }
+
+ this.currentImageIndex = index;
+ }
+
+ sizeOverlay() {
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
+ if (!this.album.length) return;
+
+ this.resizeTimer = window.setTimeout(() => {
+ const current = this.album[this.currentImageIndex];
+ if (!current) return;
+ if (current.type === 'image') this.changeImage(this.currentImageIndex);
+ else this.updateSize(this.currentImageIndex);
+ }, 200);
+ }
+
+ sizeContainer(width, height, mediaType = 'image') {
+ const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
+ const newWidth = width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
+ const newHeight = height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
+
+ this.outerContainer.style.transition = `width ${this.options.resizeDuration}ms, height ${this.options.resizeDuration}ms`;
+ this.outerContainer.style.width = `${newWidth}px`;
+ this.outerContainer.style.height = `${newHeight}px`;
+
+ window.setTimeout(() => {
+ if (this.lightbox.classList.contains('vertical')) this.dataContainer.style.width = `${newWidth}px`;
+ else this.dataContainer.style.height = `${newHeight}px`;
+
+ this.overlay.focus();
+ this.showImage();
+ this.outerContainer.style.transition = '';
+ }, this.options.resizeDuration);
+ }
+
+ showImage() {
+ this.fade(this.loader, false, 0);
+ if (this.options.hasVideo && this.album[this.currentImageIndex].type === 'video') this.fade(this.video, true, this.options.imageFadeDuration);
+ else this.fade(this.image, true, this.options.imageFadeDuration);
+
+ this.updateNav();
+ this.updateDetails();
+ this.preloadNeighboringImages();
+ this.enableKeyboardNav();
+ }
+
+ updateNav() {
+ this.nav.style.display = 'block';
+ this.prev.style.display = 'none';
+ this.next.style.display = 'none';
+
+ const alwaysShowNav = ('ontouchstart' in window) && this.options.alwaysShowNavOnTouchDevices;
+ if (this.album.length <= 1) return;
+
+ if (this.options.wrapAround) {
+ this.prev.style.display = '';
+ this.next.style.display = '';
+ } else {
+ if (this.currentImageIndex > 0) this.prev.style.display = '';
+ if (this.currentImageIndex < this.album.length - 1) this.next.style.display = '';
+ }
+
+ if (alwaysShowNav) {
+ this.prev.style.opacity = '1';
+ this.next.style.opacity = '1';
+ } else {
+ this.prev.style.opacity = '';
+ this.next.style.opacity = '';
+ }
+ }
+
+ updateDetails() {
+ const media = this.album[this.currentImageIndex];
+ if (!media) return;
+
+ if (media.title) {
+ if (this.options.sanitizeTitle) this.caption.textContent = media.title;
+ else this.caption.innerHTML = media.title;
+ this.fade(this.caption, true, 200);
+ this.fade(this.closeButton, true, 200);
+ } else {
+ this.caption.textContent = '';
+ this.caption.style.display = 'none';
+ }
+
+ this.outerContainer.classList.remove('animating');
+ this.fade(this.dataContainer, true, this.options.resizeDuration);
+ }
+
+ preloadNeighboringImages() {
+ const next = this.album[this.currentImageIndex + 1];
+ const prev = this.album[this.currentImageIndex - 1];
+ if (next && next.type === 'image') {
+ const preloadNext = new Image();
+ preloadNext.src = next.link;
+ }
+ if (prev && prev.type === 'image') {
+ const preloadPrev = new Image();
+ preloadPrev.src = prev.link;
+ }
+ }
+
+ enableKeyboardNav() {
+ this.disableKeyboardNav();
+ this.lightbox.addEventListener('keyup', this.boundOnKeyUp);
+ this.overlay.addEventListener('keyup', this.boundOnKeyUp);
+ }
+
+ disableKeyboardNav() {
+ this.lightbox?.removeEventListener('keyup', this.boundOnKeyUp);
+ this.overlay?.removeEventListener('keyup', this.boundOnKeyUp);
+ }
+
+ keyboardAction(event) {
+ switch (event.key) {
+ case 'Escape':
+ event.stopPropagation();
+ this.end();
+ break;
+ case 'ArrowLeft':
+ if (this.currentImageIndex !== 0) this.changeImage(this.currentImageIndex - 1);
+ else if (this.options.wrapAround && this.album.length > 1) this.changeImage(this.album.length - 1);
+ break;
+ case 'ArrowRight':
+ if (this.currentImageIndex !== this.album.length - 1) this.changeImage(this.currentImageIndex + 1);
+ else if (this.options.wrapAround && this.album.length > 1) this.changeImage(0);
+ break;
+ }
+ }
+
+ onWheel(event) {
+ const media = this.album[this.currentImageIndex];
+ if (!media || media.type === 'video') return;
+ event.preventDefault();
+
+ const rect = this.image.getBoundingClientRect();
+ const oldZoom = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
+ const oldTranslateX = parseFloat(this.image.style.getPropertyValue('--translate-x') || '0');
+ const oldTranslateY = parseFloat(this.image.style.getPropertyValue('--translate-y') || '0');
+ const maxZoom = Math.max(media.width / Math.max(this.image.width, 1), media.height / Math.max(this.image.height, 1), 1);
+ const newZoom = Math.min(Math.max(oldZoom + (-Math.sign(event.deltaY) / 10), 1), maxZoom);
+
+ const offsetX = event.clientX - rect.left;
+ const offsetY = event.clientY - rect.top;
+ let translateX = oldTranslateX + (newZoom - oldZoom) * ((this.image.width / 2) - offsetX);
+ let translateY = oldTranslateY + (newZoom - oldZoom) * ((this.image.height / 2) - offsetY);
+ const maxTranslateX = (newZoom - 1) * this.image.width / 2;
+ const maxTranslateY = (newZoom - 1) * this.image.height / 2;
+
+ translateX = Math.max(Math.min(translateX, maxTranslateX), -maxTranslateX);
+ translateY = Math.max(Math.min(translateY, maxTranslateY), -maxTranslateY);
+
+ this.container.classList.toggle('moveable', newZoom > 1);
+ this.image.style.setProperty('--scale', String(newZoom));
+ this.image.style.setProperty('--translate-x', `${translateX}px`);
+ this.image.style.setProperty('--translate-y', `${translateY}px`);
+ }
+
+ onDragStart(event) {
+ const scale = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
+ if (scale <= 1) return;
+
+ this.gMouseDownOffsetX = event.clientX - parseFloat(this.image.style.getPropertyValue('--translate-x') || '0');
+ this.gMouseDownOffsetY = event.clientY - parseFloat(this.image.style.getPropertyValue('--translate-y') || '0');
+ this.container.classList.add('moving');
+ window.addEventListener('mousemove', this.boundOnDragMove);
+ }
+
+ onDragMove(event) {
+ const zoom = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
+ let translateX = event.clientX - this.gMouseDownOffsetX;
+ let translateY = event.clientY - this.gMouseDownOffsetY;
+ const maxTranslateX = (zoom - 1) * this.image.width / 2;
+ const maxTranslateY = (zoom - 1) * this.image.height / 2;
+
+ translateX = Math.max(Math.min(translateX, maxTranslateX), -maxTranslateX);
+ translateY = Math.max(Math.min(translateY, maxTranslateY), -maxTranslateY);
+
+ this.image.style.setProperty('--translate-x', `${translateX}px`);
+ this.image.style.setProperty('--translate-y', `${translateY}px`);
+ }
+
+ onDragEnd() {
+ window.removeEventListener('mousemove', this.boundOnDragMove);
+ this.container?.classList.remove('moving');
+ }
+
+ resetImageTransform() {
+ this.image.style.setProperty('--scale', '1');
+ this.image.style.setProperty('--translate-x', '0px');
+ this.image.style.setProperty('--translate-y', '0px');
+ }
+
+ hideElements(elements) {
+ elements.forEach((element) => {
+ if (element) element.style.display = 'none';
+ });
+ }
+
+ fade(element, show, duration, done) {
+ if (!element) return;
+
+ const safeDuration = duration || 0;
+ element.style.transition = `opacity ${safeDuration}ms`;
+ if (show) {
+ element.style.display = element === this.lightbox ? 'flex' : 'block';
+ requestAnimationFrame(() => {
+ element.style.opacity = element === this.overlay ? '0.8' : '1';
+ });
+ } else {
+ element.style.opacity = '0';
+ window.setTimeout(() => {
+ element.style.display = 'none';
+ }, safeDuration);
+ }
+
+ if (typeof done === 'function') {
+ window.setTimeout(done, safeDuration);
+ }
+ }
+
+ end() {
+ this.disableKeyboardNav();
+ this.video.pause();
+ this.video.removeAttribute('src');
+ this.container.classList.remove('lb-video-nav', 'moveable', 'moving');
+ window.removeEventListener('resize', this.boundOnResize);
+ window.removeEventListener('mousemove', this.boundOnDragMove);
+ this.fade(this.lightbox, false, this.options.fadeDuration);
+ this.fade(this.overlay, false, this.options.fadeDuration);
+
+ if (this.options.disableScrolling) document.body.classList.remove('lb-disable-scrolling');
+ this.options.onClosing();
+ }
+}
+
+const lightbox = new Lightbox();
+
+export const options = defaults;
+export default lightbox;
diff --git a/src/scripts/spot.js b/src/scripts/spot.js
index 3922ce8..2fcc06c 100755
--- a/src/scripts/spot.js
+++ b/src/scripts/spot.js
@@ -1,3 +1,6 @@
+import { copyArray, getElem, setElem } from './common.js';
+import Lang from './lang.js';
+
export default class Spot {
constructor(asGlobals) {
@@ -6,6 +9,11 @@ export default class Spot {
this.consts.title = 'Spotty';
this.consts.default_page = 'project';
//this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
+
+ this.translator = new Lang({
+ translations: this.consts.lang || {},
+ prefix: this.consts.lang_prefix || ''
+ });
this.pages = {};
@@ -104,9 +112,7 @@ export default class Spot {
else {
let oResponse = await oRequest.json();
bLoading = false;
- if(oResponse.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) {
- oResponse.desc = this.lang(oResponse.desc.substr(this.consts.lang_prefix.length));
- }
+ oResponse.desc = this.translator.parse(oResponse.desc);
if(oResponse.result == this.consts.error) return Promise.reject(oResponse.desc);
else return oResponse.data;
@@ -119,22 +125,7 @@ export default class Spot {
}
lang(sKey, asParams = []) {
- if(sKey == '') return '';
- if(typeof asParams !== 'object') asParams = [asParams];
-
- var sLang = '';
- if(sKey in this.consts.lang) {
- sLang = this.consts.lang[sKey];
- for(let i in asParams) {
- sLang = sLang.replace('$'+i, asParams[i]);
- }
- }
- else {
- console.log('missing translation: '+sKey);
- sLang = sKey;
- }
-
- return sLang;
+ return this.translator.lang(sKey, asParams);
}
isMobile() {
@@ -313,4 +304,4 @@ export default class Spot {
checkClearance(sClearance) {
return (this.vars(['user', 'clearance']) >= sClearance);
}
-}
\ No newline at end of file
+}
{{ log }}.