UNPKG

lightgallery

Version:

lightGallery is a feature-rich, modular JavaScript gallery plugin for building beautiful image and video galleries for the web and the mobile

1,413 lines (1,412 loc) • 49.4 kB
/*! * lightgallery | 3.0.0 | October 9th 2026 * http://www.lightgalleryjs.com/ * Copyright (c) 2020 Sachin Neravath; * @license GPLv3 */ const SPRING_SETTLE_DAMPING = 1; const SPRING_BOUNCE_DAMPING = 0.82; const SPRING_NATURAL_FREQUENCY = 12; const DECELERATION_RATE = 0.995; function project(velocity, decelerationRate = DECELERATION_RATE) { return velocity * decelerationRate / (1 - decelerationRate); } function stepSpring(state, target, dtMs, { dampingRatio = SPRING_SETTLE_DAMPING, naturalFrequency = SPRING_NATURAL_FREQUENCY } = {}) { const t = dtMs / 1e3; const w0 = naturalFrequency; const zeta = Math.min(dampingRatio, 1); const x0 = state.position - target; const v0 = state.velocity * 1e3; const decay = Math.exp(-zeta * w0 * t); let x; let v; if (zeta < 1) { const wd = w0 * Math.sqrt(1 - zeta * zeta); const a = x0; const b = (v0 + zeta * w0 * x0) / wd; const cos = Math.cos(wd * t); const sin = Math.sin(wd * t); x = decay * (a * cos + b * sin); v = decay * ((b * wd - zeta * w0 * a) * cos - (a * wd + zeta * w0 * b) * sin); } else { const b = v0 + w0 * x0; x = decay * (x0 + b * t); v = decay * (b - w0 * (x0 + b * t)); } return { position: target + x, velocity: v / 1e3 }; } function isSpringSettled(state, target, restDelta = 0.3, restVelocity = 0.012) { return Math.abs(state.position - target) < restDelta && Math.abs(state.velocity) < restVelocity; } function parseImageSize(lgSize, viewportWidth) { if (!lgSize) { return void 0; } let size = lgSize; const responsiveSizes = lgSize.split(","); if (responsiveSizes[1]) { for (let i = 0; i < responsiveSizes.length; i++) { const candidate = responsiveSizes[i].trim(); const responsiveWidth = parseInt(candidate.split("-")[2], 10); if (responsiveWidth > viewportWidth) { size = candidate; break; } if (i === responsiveSizes.length - 1) { size = candidate; } } } const parts = size.trim().split("-"); const width = parseInt(parts[0], 10); const height = parseInt(parts[1], 10); if (!Number.isFinite(width) || !Number.isFinite(height)) { return void 0; } return { width, height }; } function fitImageSize(size, containerWidth, containerHeight) { const maxWidth = Math.min(containerWidth, size.width); const maxHeight = Math.min(containerHeight, size.height); const ratio = Math.min(maxWidth / size.width, maxHeight / size.height); return { width: size.width * ratio, height: size.height * ratio }; } const zoomDefaultIcons = { zoomIn: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path transform="translate(0, 960) scale(1, -1)" d="M512 512.667h-86v-86h-42v86h-86v42h86v86h42v-86h86v-42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z"/></svg>', zoomOut: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path transform="translate(0, 960) scale(1, -1)" d="M298 554.667h214v-42h-214v42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z"/></svg>', actualSize: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path transform="translate(0, 960) scale(1, -1)" d="M384 853.334h426.667q53 0 90.5-37.5t37.5-90.5v-426.667q0-53-37.5-90.5t-90.5-37.5h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 53 37.5 90.5t90.5 37.5zM170.667 675.334v-547.333q0-17.667 12.5-30.167t30.167-12.5h547.333q-13.333-37.667-46.333-61.5t-74.333-23.833h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 41.333 23.833 74.333t61.5 46.333zM810.667 768h-426.667q-17.667 0-30.167-12.5t-12.5-30.167v-426.667q0-17.667 12.5-30.167t30.167-12.5h426.667q17.667 0 30.167 12.5t12.5 30.167v426.667q0 17.667-12.5 30.167t-30.167 12.5z"/></svg>' }; function getPanBounds(imageWidth, imageHeight, containerWidth, containerHeight, scale) { return { maxX: Math.max(0, (imageWidth * scale - containerWidth) / 2), maxY: Math.max(0, (imageHeight * scale - containerHeight) / 2) }; } function getActualSizeScale(naturalWidth, renderedWidth) { if (!renderedWidth) { return 2; } return naturalWidth / renderedWidth || 2; } function getPointZoomPan(point, prevPan, prevScale, newScale) { const ratio = newScale / prevScale; return { x: point.x - (point.x - prevPan.x) * ratio, y: point.y - (point.y - prevPan.y) * ratio }; } function clampPanToStage(pan, bounds, stageBottomExtra) { const floorY = Math.min(-bounds.maxY + stageBottomExtra, bounds.maxY); return { x: Math.min(Math.max(pan.x, -bounds.maxX), bounds.maxX), y: Math.min(Math.max(pan.y, floorY), bounds.maxY) }; } function getPinchPan(currentMid, startMid, startPan, startScale, scale) { const projected = getPointZoomPan(startMid, startPan, startScale, scale); return { x: projected.x + (currentMid.x - startMid.x), y: projected.y + (currentMid.y - startMid.y) }; } function getPointerDistance(a, b) { return Math.sqrt( (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) ); } const PINCH_UNDER_FRICTION = 0.15; const PINCH_OVER_FRICTION = 0.05; function getPinchScale(startDistance, currentDistance, startScale, maxScale, infiniteZoom, pinchToCloseArmed = false) { if (startDistance <= 0) { return startScale; } let scale = currentDistance / startDistance * startScale; if (scale < 1 && !pinchToCloseArmed) { scale = 1 + (scale - 1) * PINCH_UNDER_FRICTION; } const cap = Math.max(maxScale, 1); if (!infiniteZoom && scale > cap) { scale = cap + (scale - cap) * PINCH_OVER_FRICTION; } return scale; } function shouldCloseOnPinch(input) { return input.pinchToClose && input.closable && input.scale < 1 && input.maxGestureScale <= 1; } function parseSrcset(srcset) { const candidates = []; if (typeof srcset !== "string") { return candidates; } for (const entry of srcset.split(",")) { const parts = entry.trim().split(/\s+/); const url = parts[0]; if (!url) { continue; } const descriptor = parts[1]; if (!descriptor) { candidates.push({ url, density: 1 }); continue; } const value = parseFloat(descriptor); if (Number.isNaN(value) || value <= 0) { continue; } if (descriptor.endsWith("w")) { candidates.push({ url, width: value }); } else if (descriptor.endsWith("x")) { candidates.push({ url, density: value }); } } return candidates; } function matchesMedia(media, viewport) { if (!media) { return true; } const condition = /^\(\s*(min|max)-width:\s*([\d.]+)px\s*\)$/.exec( media.trim() ); if (!condition) { return false; } const bound = parseFloat(condition[2]); return condition[1] === "min" ? viewport.width >= bound : viewport.width <= bound; } function getActualSizeWidth(item, viewport, naturalWidth) { if (item.width) { const declared = parseFloat(item.width); if (!Number.isNaN(declared) && declared > 0) { return declared; } } let candidates = []; const sources = Array.isArray(item.sources) ? item.sources : []; for (const source of sources) { if (matchesMedia(source.media, viewport)) { candidates = parseSrcset(source.srcset); break; } } if (!candidates.length && item.srcset) { candidates = parseSrcset(item.srcset); } const widths = candidates.map((candidate) => candidate.width).filter((width) => width !== void 0); if (widths.length) { return Math.max(...widths); } const natural = parseImageSize(item.lgSize, viewport.width); if (natural && natural.width > 0) { return natural.width; } return naturalWidth; } const VELOCITY_WINDOW_MS = 100; const MIN_DISPLACEMENT = 1; const MIN_WINDOW_SPAN_MS = 5; function pushVelocitySample(samples, sample, windowMs = VELOCITY_WINDOW_MS) { return [...samples.filter((s) => sample.t - s.t <= windowMs), sample]; } function getWindowedVelocity(samples, releaseTime, windowMs = VELOCITY_WINDOW_MS) { const recent = samples.filter((s) => releaseTime - s.t <= windowMs); if (recent.length < 2) { return { x: 0, y: 0 }; } const first = recent[0]; const last = recent[recent.length - 1]; const dt = last.t - first.t; if (dt < MIN_WINDOW_SPAN_MS) { return { x: 0, y: 0 }; } const dx = last.x - first.x; const dy = last.y - first.y; return { x: Math.abs(dx) > MIN_DISPLACEMENT ? dx / dt : 0, y: Math.abs(dy) > MIN_DISPLACEMENT ? dy / dt : 0 }; } function getRotatedVisualSize(width, height, transform) { if (!transform) { return { width, height }; } const rotate = /rotate\((-?\d+(?:\.\d+)?)deg\)/.exec(transform); const scale = /scale3d\((-?\d+(?:\.\d+)?)/.exec(transform); const fit = (scale == null ? void 0 : scale[1]) ? Math.abs(parseFloat(scale[1])) : 1; const degrees = (rotate == null ? void 0 : rotate[1]) ? parseFloat(rotate[1]) : 0; const normalized = (degrees % 360 + 360) % 360; return normalized === 90 || normalized === 270 ? { width: height * fit, height: width * fit } : { width: width * fit, height: height * fit }; } const MAX_FRAME_MS = 64; function runSprings(tracks, onFrame, onDone) { let raf = 0; let last = Date.now(); const states = tracks.map((t) => ({ position: t.from, velocity: t.velocity })); const frame = () => { const now = Date.now(); const dt = Math.min(Math.max(now - last, 0), MAX_FRAME_MS); last = now; let settled = true; tracks.forEach((t, i) => { states[i] = stepSpring(states[i], t.target, dt, t); if (!isSpringSettled(states[i], t.target)) { settled = false; } }); if (settled) { onFrame(tracks.map((t) => t.target)); onDone == null ? void 0 : onDone(); return; } onFrame(states.map((s) => s.position)); raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); } const zoomSettings = { scale: 1, zoom: true, infiniteZoom: true, actualSize: true, showZoomInOutIcons: false, actualSizeIcons: { zoomIn: "lg-zoom-in", zoomOut: "lg-zoom-out" }, enableZoomAfter: 300 }; const lGEvents = { containerResize: "lgContainerResize", beforeOpen: "lgBeforeOpen", afterOpen: "lgAfterOpen", slideItemLoad: "lgSlideItemLoad", afterSlide: "lgAfterSlide", rotateLeft: "lgRotateLeft", rotateRight: "lgRotateRight", flipHorizontal: "lgFlipHorizontal", flipVertical: "lgFlipVertical" }; var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; const ZOOM_TRANSITION_DURATION = 500; class Zoom { constructor(instance, $LG) { this.core = instance; this.$LG = $LG; this.settings = __spreadValues(__spreadValues({}, zoomSettings), this.core.settings); return this; } // Append Zoom controls. Actual size, Zoom-in, Zoom-out buildTemplates() { var _a, _b, _c, _d, _e, _f; const gestureButtons = this.core.settings.showGestureButtons !== false; let zoomIcons = gestureButtons && this.settings.showZoomInOutIcons ? `<button id="${this.core.getIdName( "lg-zoom-in" )}" type="button" aria-label="${(_b = (_a = this.settings.zoomPluginStrings) == null ? void 0 : _a.zoomIn) != null ? _b : this.core.settings.strings.zoomIn}" class="lg-zoom-in lg-icon"></button><button id="${this.core.getIdName( "lg-zoom-out" )}" type="button" aria-label="${(_d = (_c = this.settings.zoomPluginStrings) == null ? void 0 : _c.zoomOut) != null ? _d : this.core.settings.strings.zoomOut}" class="lg-zoom-out lg-icon"></button>` : ""; if (gestureButtons && this.settings.actualSize) { zoomIcons += `<button id="${this.core.getIdName( "lg-actual-size" )}" type="button" aria-label="${(_f = (_e = this.settings.zoomPluginStrings) == null ? void 0 : _e.viewActualSize) != null ? _f : this.core.settings.strings.viewActualSize}" class="${this.settings.actualSizeIcons.zoomIn} lg-icon"></button>`; } this.core.outer.addClass("lg-use-transition-for-zoom"); this.core.$toolbar.first().append(zoomIcons); } /** * @desc Enable zoom option only once the image is completely loaded * If zoomFromOrigin is true, Zoom is enabled once the dummy image has been inserted * * Zoom styles are defined under lg-zoomable CSS class. */ enableZoom(event) { let _speed = this.settings.enableZoomAfter + event.detail.delay; if (this.$LG("body").first().hasClass("lg-from-hash") && event.detail.delay) { _speed = 0; } else { this.$LG("body").first().removeClass("lg-from-hash"); } this.zoomableTimeout = setTimeout(() => { if (!this.isImageSlide(this.core.index)) { return; } this.core.getSlideItem(event.detail.index).addClass("lg-zoomable"); if (event.detail.index === this.core.index) { this.setZoomEssentials(); } }, _speed + 30); } enableZoomOnSlideItemLoad() { this.core.LGel.on( `${lGEvents.slideItemLoad}.zoom`, this.enableZoom.bind(this) ); } getDragCords(e) { return { x: e.pageX, y: e.pageY }; } getSwipeCords(e) { const x = e.touches[0].pageX; const y = e.touches[0].pageY; return { x, y }; } getDragAllowedAxises(scale, scaleDiff) { if (!this.containerRect) { return { allowX: false, allowY: false }; } const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first().get(); let height = 0; let width = 0; const rect = $image.getBoundingClientRect(); if (scale) { const visual = this.getVisualImageSize( $image, $image.offsetWidth, $image.offsetHeight ); height = visual.height * scale; width = visual.width * scale; } else if (scaleDiff) { height = rect.height + scaleDiff * rect.height; width = rect.width + scaleDiff * rect.width; } else if (this.core.currentImageSize) { const visual = this.getVisualImageSize( $image, this.core.currentImageSize.width, this.core.currentImageSize.height ); height = visual.height * this.scale; width = visual.width * this.scale; } else { height = rect.height; width = rect.width; } const allowY = height > this.containerRect.height; const allowX = width > this.containerRect.width; return { allowX, allowY }; } setZoomEssentials() { this.containerRect = this.core.$content.get().getBoundingClientRect(); } /** * @desc Image zoom * Translate the wrap and scale the image to get better user experience * * @param {String} scale - Zoom decrement/increment value */ zoomImage(scale, scaleDiff, reposition, resetToMax) { if (!this.containerRect || Math.abs(scaleDiff) <= 0) return; const offsetX = this.containerRect.width / 2 + this.containerRect.left; const offsetY = this.containerRect.height / 2 + this.containerRect.top + this.scrollTop; let originalX; let originalY; if (scale === 1) { this.positionChanged = false; } if (this.positionChanged) { const previousScale = this.scale - scaleDiff; originalX = previousScale ? this.left / previousScale : 0; originalY = previousScale ? this.top / previousScale : 0; this.pageX = offsetX - originalX; this.pageY = offsetY - originalY; this.positionChanged = false; } let x; let y; let _x = offsetX - this.pageX; let _y = offsetY - this.pageY; if (scale - scaleDiff > 1) { const scaleVal = (scale - scaleDiff) / Math.abs(scaleDiff); _x = (scaleDiff < 0 ? -_x : _x) + this.left * (scaleVal + (scaleDiff < 0 ? -1 : 1)); _y = (scaleDiff < 0 ? -_y : _y) + this.top * (scaleVal + (scaleDiff < 0 ? -1 : 1)); x = _x / scaleVal; y = _y / scaleVal; } else { const scaleVal = (scale - scaleDiff) * scaleDiff; x = _x * scaleVal; y = _y * scaleVal; } if (reposition) { const clamped = this.clampPinchPan({ x, y }, scale); x = clamped.x; y = clamped.y; } this.setZoomStyles({ x, y, scale }); this.left = x; this.top = y; if (resetToMax) { this.setZoomImageSize(); } } resetImageTranslate(index) { if (!this.isImageSlide(index)) { return; } const $image = this.core.getSlideItem(index).find(".lg-image").first(); this.imageReset = false; $image.removeClass( "reset-transition reset-transition-y reset-transition-x" ); this.core.outer.removeClass("lg-actual-size"); $image.css("width", "auto").css("height", "auto"); setTimeout(() => { $image.removeClass("no-transition"); }, 10); } setZoomImageSize(delay = ZOOM_TRANSITION_DURATION) { const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first(); setTimeout(() => { if (this.core.touchAction) { return; } const actualSizeScale = this.getCurrentImageActualSizeScale(); if (this.scale >= actualSizeScale) { $image.addClass("no-transition"); this.imageReset = true; } }, delay); setTimeout(() => { if (this.core.touchAction) { return; } const actualSizeScale = this.getCurrentImageActualSizeScale(); if (this.scale >= actualSizeScale) { const dragAllowedAxises = this.getDragAllowedAxises(this.scale); const image = $image.get(); const referenceWidth = this.getNaturalWidth(this.core.index); const referenceHeight = image.naturalWidth > 0 ? referenceWidth * image.naturalHeight / image.naturalWidth : image.naturalHeight; $image.css("width", referenceWidth + "px").css("height", referenceHeight + "px"); this.core.outer.addClass("lg-actual-size"); if (dragAllowedAxises.allowX && dragAllowedAxises.allowY) { $image.addClass("reset-transition"); } else if (dragAllowedAxises.allowX && !dragAllowedAxises.allowY) { $image.addClass("reset-transition-x"); } else if (!dragAllowedAxises.allowX && dragAllowedAxises.allowY) { $image.addClass("reset-transition-y"); } } }, delay + 50); } /** * @desc apply scale3d to image and translate to image wrap * @param {style} X,Y and scale */ setZoomStyles(style) { const $imageWrap = this.core.getSlideItem(this.core.index).find(".lg-img-wrap").first(); const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first(); const $dummyImage = this.core.outer.find(".lg-current .lg-dummy-img").first(); this.scale = style.scale; $image.css( "transform", "scale3d(" + style.scale + ", " + style.scale + ", 1)" ); $dummyImage.css( "transform", "scale3d(" + style.scale + ", " + style.scale + ", 1)" ); const transform = "translate3d(" + style.x + "px, " + style.y + "px, 0)"; $imageWrap.css("transform", transform); } /** * @param index - Index of the current slide * @param event - event will be available only if the function is called on clicking/taping the imags */ setActualSize(index, event) { if (this.zoomInProgress) { return; } this.zoomInProgress = true; const currentItem = this.core.galleryItems[this.core.index]; this.resetImageTranslate(index); setTimeout(() => { if (!currentItem.src || this.core.outer.hasClass("lg-first-slide-loading")) { return; } const scale = this.getCurrentImageActualSizeScale(); const prevScale = this.scale; if (this.core.outer.hasClass("lg-zoomed")) { this.scale = 1; } else { this.scale = this.getScale(scale); } this.setPageCords(event); this.beginZoom(this.scale); this.zoomImage(this.scale, this.scale - prevScale, true, true); }, 50); setTimeout(() => { this.core.outer.removeClass("lg-grabbing").addClass("lg-grab"); }, 60); setTimeout(() => { this.zoomInProgress = false; }, ZOOM_TRANSITION_DURATION + 110); } getNaturalWidth(index) { const $image = this.core.getSlideItem(index).find(".lg-image").first(); return getActualSizeWidth( this.core.galleryItems[index], { width: window.innerWidth }, $image.get().naturalWidth ); } getActualSizeScale(naturalWidth, width) { let scale; if (naturalWidth >= width) { scale = getActualSizeScale(naturalWidth, width); } else { scale = 1; } return scale; } getCurrentImageActualSizeScale() { var _a; const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first(); const image = $image.get(); let width = (_a = this.core.currentImageSize) == null ? void 0 : _a.width; if (!width) { if (this.core.outer.hasClass("lg-actual-size")) { if (!this.containerRect) { this.setZoomEssentials(); } const referenceWidth = this.getNaturalWidth(this.core.index); const referenceHeight = image.naturalWidth > 0 ? referenceWidth * image.naturalHeight / image.naturalWidth : image.naturalHeight; width = fitImageSize( { width: referenceWidth, height: referenceHeight }, this.containerRect.width, this.containerRect.height ).width; } else { width = image.offsetWidth; } } const naturalWidth = this.getNaturalWidth(this.core.index) || width; return this.getActualSizeScale(naturalWidth, width); } getPageCords(event) { const cords = {}; if (event) { cords.x = event.pageX || event.touches[0].pageX; cords.y = event.pageY || event.touches[0].pageY; } else { const containerRect = this.core.$content.get().getBoundingClientRect(); cords.x = containerRect.width / 2 + containerRect.left; cords.y = containerRect.height / 2 + this.scrollTop + containerRect.top; } return cords; } setPageCords(event) { const pageCords = this.getPageCords(event); this.pageX = pageCords.x; this.pageY = pageCords.y; } manageActualPixelClassNames() { const $actualSize = this.core.getElementById("lg-actual-size"); $actualSize.removeClass(this.settings.actualSizeIcons.zoomIn).addClass(this.settings.actualSizeIcons.zoomOut); } // If true, zoomed - in else zoomed out beginZoom(scale) { this.core.outer.removeClass("lg-zoom-drag-transition lg-zoom-dragging"); if (scale > 1) { this.core.outer.addClass("lg-zoomed"); this.manageActualPixelClassNames(); } else { this.resetZoom(); } return scale > 1; } getScale(scale) { const actualSizeScale = this.getCurrentImageActualSizeScale(); if (scale < 1) { scale = 1; } else if (scale > actualSizeScale) { scale = actualSizeScale; } return scale; } init() { this.core.registerDefaultIcons(zoomDefaultIcons); if (!this.settings.zoom) { return; } this.buildTemplates(); this.enableZoomOnSlideItemLoad(); let tapped = null; this.core.outer.on("dblclick.lg", (event) => { if (!this.$LG(event.target).hasClass("lg-image")) { return; } this.setActualSize(this.core.index, event); }); this.core.outer.on("touchstart.lg", (event) => { const $target = this.$LG(event.target); if (event.touches.length === 1 && $target.hasClass("lg-image")) { if (!tapped) { tapped = setTimeout(() => { tapped = null; }, 300); } else { clearTimeout(tapped); tapped = null; event.preventDefault(); this.setActualSize(this.core.index, event); } } }); this.core.LGel.on( `${lGEvents.containerResize}.zoom ${lGEvents.rotateRight}.zoom ${lGEvents.rotateLeft}.zoom ${lGEvents.flipHorizontal}.zoom ${lGEvents.flipVertical}.zoom`, () => { if (!this.core.lgOpened || !this.isImageSlide(this.core.index) || this.core.touchAction) { return; } this.stopZoomSpring(); const _LGel = this.core.getSlideItem(this.core.index).find(".lg-img-wrap").first(); this.top = 0; this.left = 0; this.setZoomEssentials(); this.setZoomSwipeStyles(_LGel, { x: 0, y: 0 }); this.positionChanged = true; } ); this.$LG(window).on(`scroll.lg.zoom.global${this.core.lgId}`, () => { if (!this.core.lgOpened) return; this.scrollTop = this.$LG(window).scrollTop(); }); this.core.getElementById("lg-zoom-out").on("click.lg", () => { if (!this.isImageSlide(this.core.index)) { return; } let timeout = 0; if (this.imageReset) { this.resetImageTranslate(this.core.index); timeout = 50; } setTimeout(() => { let scale = this.scale - this.settings.scale; if (scale < 1) { scale = 1; } this.beginZoom(scale); this.zoomImage( scale, -this.settings.scale, true, !this.settings.infiniteZoom ); }, timeout); }); this.core.getElementById("lg-zoom-in").on("click.lg", () => { this.zoomIn(); }); this.core.getElementById("lg-actual-size").on("click.lg", () => { this.setActualSize(this.core.index); }); this.core.LGel.on(`${lGEvents.beforeOpen}.zoom`, () => { this.core.outer.find(".lg-item").removeClass("lg-zoomable"); }); this.core.LGel.on(`${lGEvents.afterOpen}.zoom`, () => { this.scrollTop = this.$LG(window).scrollTop(); this.pageX = this.core.outer.width() / 2; this.pageY = this.core.outer.height() / 2 + this.scrollTop; this.scale = 1; }); this.core.LGel.on( `${lGEvents.afterSlide}.zoom`, (event) => { const { prevIndex } = event.detail; this.scale = 1; this.positionChanged = false; this.zoomInProgress = false; this.resetZoom(prevIndex); this.resetImageTranslate(prevIndex); if (this.isImageSlide(this.core.index)) { this.setZoomEssentials(); } } ); this.zoomDrag(); this.pinchZoom(); this.zoomSwipe(); this.zoomableTimeout = false; this.positionChanged = false; this.zoomInProgress = false; } zoomIn() { if (!this.isImageSlide(this.core.index)) { return; } let scale = this.scale + this.settings.scale; if (!this.settings.infiniteZoom) { scale = this.getScale(scale); } this.beginZoom(scale); this.zoomImage( scale, Math.min(this.settings.scale, scale - this.scale), true, !this.settings.infiniteZoom ); } // Reset zoom effect resetZoom(index) { this.stopZoomSpring(); this.core.outer.removeClass("lg-zoomed lg-zoom-drag-transition"); const $actualSize = this.core.getElementById("lg-actual-size"); const $item = this.core.getSlideItem( index !== void 0 ? index : this.core.index ); $actualSize.removeClass(this.settings.actualSizeIcons.zoomOut).addClass(this.settings.actualSizeIcons.zoomIn); $item.find(".lg-img-wrap").first().removeAttr("style"); $item.find(".lg-image").first().removeAttr("style"); this.scale = 1; this.left = 0; this.top = 0; this.setPageCords(); } /** Stop a running release spring; state stays at its live values. */ stopZoomSpring() { if (this.cancelZoomSpring) { this.cancelZoomSpring(); this.cancelZoomSpring = void 0; } } /** * Any gesture end must leave the image inside its pan bounds: a tap * interrupts a settling spring (stopZoomSpring at its touchstart), * and if it never turns into a drag nothing else re-clamps, the * fused pinch pan can legitimately be far outside mid-settle. * Springs home from wherever the interruption stopped it; a no-op * when already in bounds (the common tap). */ settleIntoBounds() { this.setZoomEssentials(); const rect = this.core.getSlideItem(this.core.index).find(".lg-image").first().get().getBoundingClientRect(); const actualSizeScale = this.getCurrentImageActualSizeScale(); const targetScale = this.settings.infiniteZoom ? Math.max(this.scale, 1) : Math.min(Math.max(this.scale, 1), Math.max(actualSizeScale, 1)); const sizeRatio = this.scale > 0 ? targetScale / this.scale : 1; const width = rect.width * sizeRatio; const height = rect.height * sizeRatio; const { bottom } = this.core.mediaContainerPosition; const halfX = Math.abs(width - this.containerRect.width) / 2; const halfY = Math.abs(height - this.containerRect.height) / 2; const floorY = height > this.containerRect.height ? Math.min(-halfY + bottom, halfY) : -halfY; const targetX = Math.min(Math.max(this.left, -halfX), halfX); const targetY = Math.min(Math.max(this.top, floorY), halfY); if (Math.abs(targetX - this.left) < 1 && Math.abs(targetY - this.top) < 1 && Math.abs(targetScale - this.scale) < 1e-3) { return; } this.core.outer.addClass("lg-zoom-drag-transition lg-zoom-dragging"); this.stopZoomSpring(); this.cancelZoomSpring = runSprings( [ { from: this.scale, velocity: 0, target: targetScale }, { from: this.left, velocity: 0, target: targetX }, { from: this.top, velocity: 0, target: targetY } ], ([scale, x, y]) => { this.left = x; this.top = y; this.setZoomStyles({ x, y, scale }); }, () => { this.cancelZoomSpring = void 0; this.core.outer.removeClass( "lg-zoom-dragging lg-zoom-drag-transition" ); if (targetScale > 1 && targetScale >= Math.max(actualSizeScale, 1)) { this.setZoomImageSize(0); } } ); } getTouchDistance(e) { return getPointerDistance( { x: e.touches[0].pageX, y: e.touches[0].pageY }, { x: e.touches[1].pageX, y: e.touches[1].pageY } ); } /** * Pinch midpoint relative to the stage centre, the focal anchor the * whole gesture projects through. */ getPinchMidPoint(e) { const centerX = this.containerRect.width / 2 + this.containerRect.left; const centerY = this.containerRect.height / 2 + this.containerRect.top + this.scrollTop; return { x: (e.touches[0].pageX + e.touches[1].pageX) / 2 - centerX, y: (e.touches[0].pageY + e.touches[1].pageY) / 2 - centerY }; } /** * Clamp a pan into the stage-aware bounds at the given scale, * measured from the untransformed layout size (offset dimensions * ignore transforms, so this stays correct mid-gesture). Y matches * `getPossibleSwipeDragCords`: the vacated components strip belongs * to the stage, so the pan-up floor sits where the image's bottom * edge meets the SCREEN bottom, not the content box. */ /** * Bounds-relevant size of the image: layout offsets swapped and * shrunk by the rotate plugin's wrapper transform when present, * at 90°/270° the visual width runs along the layout height, and * offsets don't see transforms. */ getVisualImageSize($image, width, height) { const rotateWrap = $image.closest(".lg-img-rotate"); return getRotatedVisualSize(width, height, rotateWrap == null ? void 0 : rotateWrap.style.transform); } clampPinchPan(pan, scale) { const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first().get(); const visual = this.getVisualImageSize( $image, $image.offsetWidth, $image.offsetHeight ); const bounds = getPanBounds( visual.width, visual.height, this.containerRect.width, this.containerRect.height, scale ); return clampPanToStage( pan, bounds, this.core.mediaContainerPosition.bottom ); } pinchZoom() { let startDist = 0; let pinchStarted = false; let initScale = 1; let startMaxScale = 1; let startPan = { x: 0, y: 0 }; let startMid = { x: 0, y: 0 }; let maxGestureScale = 1; let lastMid = { x: 0, y: 0 }; let midSamples = []; let $item = this.core.getSlideItem(this.core.index); this.core.outer.on("touchstart.lg", (e) => { $item = this.core.getSlideItem(this.core.index); if (!this.isImageSlide(this.core.index)) { return; } if (e.touches.length === 2) { e.preventDefault(); if (this.core.outer.hasClass("lg-first-slide-loading")) { return; } this.stopZoomSpring(); this.setZoomEssentials(); initScale = this.scale || 1; startPan = { x: this.left, y: this.top }; startMid = this.getPinchMidPoint(e); this.core.outer.addClass( "lg-zoom-drag-transition lg-zoom-dragging" ); this.setPageCords(e); this.resetImageTranslate(this.core.index); startMaxScale = this.getCurrentImageActualSizeScale(); this.core.touchAction = "pinch"; startDist = this.getTouchDistance(e); maxGestureScale = initScale; lastMid = startMid; midSamples = [{ x: startMid.x, y: startMid.y, t: Date.now() }]; } }); this.core.$inner.on("touchmove.lg", (e) => { if (e.touches.length === 2 && this.core.touchAction === "pinch" && (this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target))) { e.preventDefault(); const endDist = this.getTouchDistance(e); const distance = startDist - endDist; if (!pinchStarted && Math.abs(distance) > 5) { pinchStarted = true; } if (pinchStarted) { const closeArmed = this.core.settings.pinchToClose && this.core.settings.closable && maxGestureScale <= 1; const _scale = getPinchScale( startDist, endDist, initScale, startMaxScale, this.settings.infiniteZoom, closeArmed ); const scale = Math.round((_scale + Number.EPSILON) * 1e4) / 1e4; const mid = this.getPinchMidPoint(e); lastMid = mid; midSamples = pushVelocitySample(midSamples, { x: mid.x, y: mid.y, t: Date.now() }); const pan = getPinchPan( mid, startMid, startPan, initScale, scale ); this.left = pan.x; this.top = pan.y; this.setZoomStyles({ x: pan.x, y: pan.y, scale }); maxGestureScale = Math.max(maxGestureScale, scale); } } }); this.core.$inner.on("touchend.lg", (e) => { if (this.core.touchAction === "pinch" && (this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target))) { pinchStarted = false; startDist = 0; if (shouldCloseOnPinch({ scale: this.scale, maxGestureScale, pinchToClose: this.core.settings.pinchToClose, closable: this.core.settings.closable })) { this.core.outer.removeClass("lg-zoom-dragging"); this.resetZoom(); this.core.closeGallery(); } else if (this.scale <= 1) { this.core.outer.removeClass("lg-zoomed"); this.stopZoomSpring(); this.cancelZoomSpring = runSprings( [ { from: this.scale, velocity: 0, target: 1 }, { from: this.left, velocity: 0, target: 0 }, { from: this.top, velocity: 0, target: 0 } ], ([scale, x, y]) => { this.left = x; this.top = y; this.setZoomStyles({ x, y, scale }); }, () => { this.cancelZoomSpring = void 0; this.core.outer.removeClass("lg-zoom-dragging"); this.resetZoom(); } ); } else { const actualSizeScale = startMaxScale; const targetScale = Math.min( this.scale, Math.max(actualSizeScale, 1) ); const midVelocity = getWindowedVelocity( midSamples, Date.now() ); const basePan = getPinchPan( lastMid, startMid, startPan, initScale, targetScale ); const glide = { x: basePan.x + project(midVelocity.x), y: basePan.y + project(midVelocity.y) }; const pan = this.clampPinchPan(glide, targetScale); this.positionChanged = true; this.manageActualPixelClassNames(); this.core.outer.addClass("lg-zoomed"); this.stopZoomSpring(); this.cancelZoomSpring = runSprings( [ { from: this.scale, velocity: 0, target: targetScale }, { from: this.left, velocity: midVelocity.x, target: pan.x, dampingRatio: pan.x !== glide.x ? SPRING_BOUNCE_DAMPING : 1 }, { from: this.top, velocity: midVelocity.y, target: pan.y, dampingRatio: pan.y !== glide.y ? SPRING_BOUNCE_DAMPING : 1 } ], ([scale, x, y]) => { this.left = x; this.top = y; this.setZoomStyles({ x, y, scale }); }, () => { this.cancelZoomSpring = void 0; this.core.outer.removeClass( "lg-zoom-dragging lg-zoom-drag-transition" ); if (targetScale >= actualSizeScale) { this.setZoomImageSize(0); } } ); } this.core.touchAction = void 0; } }); } touchendZoom(startCoords, endCoords, allowX, allowY, velocity) { const _LGel = this.core.getSlideItem(this.core.index).find(".lg-img-wrap").first(); const possibleSwipeCords = this.getPossibleSwipeDragCords(); const current = this.getZoomSwipeCords( startCoords, endCoords, allowX, allowY, possibleSwipeCords ); const clampAxis = (value, min, max) => Math.min(Math.max(value, max), min); const targetX = allowX ? clampAxis( current.x + project(velocity.x), possibleSwipeCords.minX, possibleSwipeCords.maxX ) : this.left; const targetY = allowY ? clampAxis( current.y + project(velocity.y), possibleSwipeCords.minY, possibleSwipeCords.maxY ) : this.top; this.positionChanged = true; if (Math.abs(targetX - current.x) < 1 && Math.abs(targetY - current.y) < 1) { this.left = targetX; this.top = targetY; this.setZoomSwipeStyles(_LGel, { x: targetX, y: targetY }); this.core.outer.removeClass( "lg-zoom-dragging lg-zoom-drag-transition" ); return; } this.core.outer.addClass("lg-zoom-dragging"); this.stopZoomSpring(); this.cancelZoomSpring = runSprings( [ { from: current.x, velocity: velocity.x, target: targetX, dampingRatio: targetX !== current.x + project(velocity.x) ? SPRING_BOUNCE_DAMPING : 1 }, { from: current.y, velocity: velocity.y, target: targetY, dampingRatio: targetY !== current.y + project(velocity.y) ? SPRING_BOUNCE_DAMPING : 1 } ], ([x, y]) => { this.left = x; this.top = y; this.setZoomSwipeStyles(_LGel, { x, y }); }, () => { this.cancelZoomSpring = void 0; this.core.outer.removeClass( "lg-zoom-dragging lg-zoom-drag-transition" ); } ); } getZoomSwipeCords(startCoords, endCoords, allowX, allowY, possibleSwipeCords) { const distance = {}; if (allowY) { distance.y = this.top + (endCoords.y - startCoords.y); if (this.isBeyondPossibleTop(distance.y, possibleSwipeCords.minY)) { const diffMinY = possibleSwipeCords.minY - distance.y; distance.y = possibleSwipeCords.minY - diffMinY / 6; } else if (this.isBeyondPossibleBottom(distance.y, possibleSwipeCords.maxY)) { const diffMaxY = distance.y - possibleSwipeCords.maxY; distance.y = possibleSwipeCords.maxY + diffMaxY / 6; } } else { distance.y = this.top; } if (allowX) { distance.x = this.left + (endCoords.x - startCoords.x); if (this.isBeyondPossibleLeft(distance.x, possibleSwipeCords.minX)) { const diffMinX = possibleSwipeCords.minX - distance.x; distance.x = possibleSwipeCords.minX - diffMinX / 6; } else if (this.isBeyondPossibleRight(distance.x, possibleSwipeCords.maxX)) { const difMaxX = distance.x - possibleSwipeCords.maxX; distance.x = possibleSwipeCords.maxX + difMaxX / 6; } } else { distance.x = this.left; } return distance; } isBeyondPossibleLeft(x, minX) { return x >= minX; } isBeyondPossibleRight(x, maxX) { return x <= maxX; } isBeyondPossibleTop(y, minY) { return y >= minY; } isBeyondPossibleBottom(y, maxY) { return y <= maxY; } isImageSlide(index) { const currentItem = this.core.galleryItems[index]; return this.core.getSlideType(currentItem) === "image"; } getPossibleSwipeDragCords(scale) { const $image = this.core.getSlideItem(this.core.index).find(".lg-image").first(); const imgRect = $image.get().getBoundingClientRect(); let imageHeight = imgRect.height; let imageWidth = imgRect.width; if (scale) { imageHeight = imageHeight + scale * imageHeight; imageWidth = imageWidth + scale * imageWidth; } const { bottom } = this.core.mediaContainerPosition; const minY = (imageHeight - this.containerRect.height) / 2; const maxY = (this.containerRect.height - imageHeight) / 2 + bottom; const minX = (imageWidth - this.containerRect.width) / 2; const maxX = (this.containerRect.width - imageWidth) / 2; const possibleSwipeCords = { minY, maxY, minX, maxX }; return possibleSwipeCords; } setZoomSwipeStyles(LGel, distance) { LGel.css( "transform", "translate3d(" + distance.x + "px, " + distance.y + "px, 0)" ); } zoomSwipe() { let startCoords = {}; let endCoords = {}; let isMoved = false; let allowX = false; let allowY = false; let samples = []; let possibleSwipeCords; let _LGel; let $item = this.core.getSlideItem(this.core.index); this.core.$inner.on("touchstart.lg", (e) => { if (!this.isImageSlide(this.core.index)) { return; } $item = this.core.getSlideItem(this.core.index); if ((this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target)) && e.touches.length === 1 && this.core.outer.hasClass("lg-zoomed")) { e.preventDefault(); this.stopZoomSpring(); isMoved = false; endCoords = {}; const startPoint = this.getSwipeCords(e); samples = [{ x: startPoint.x, y: startPoint.y, t: Date.now() }]; this.core.touchAction = "zoomSwipe"; _LGel = this.core.getSlideItem(this.core.index).find(".lg-img-wrap").first(); const dragAllowedAxises = this.getDragAllowedAxises(0); allowY = dragAllowedAxises.allowY; allowX = dragAllowedAxises.allowX; startCoords = this.getSwipeCords(e); possibleSwipeCords = this.getPossibleSwipeDragCords(); this.core.outer.addClass( "lg-zoom-dragging lg-zoom-drag-transition" ); } }); this.core.$inner.on("touchmove.lg", (e) => { if (e.touches.length === 1 && this.core.touchAction === "zoomSwipe" && (this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target))) { e.preventDefault(); this.core.touchAction = "zoomSwipe"; endCoords = this.getSwipeCords(e); samples = pushVelocitySample(samples, { x: endCoords.x, y: endCoords.y, t: Date.now() }); const distance = this.getZoomSwipeCords( startCoords, endCoords, allowX, allowY, possibleSwipeCords ); if (Math.abs(endCoords.x - startCoords.x) > 15 || Math.abs(endCoords.y - startCoords.y) > 15) { isMoved = true; this.setZoomSwipeStyles(_LGel, distance); } } }); this.core.$inner.on("touchend.lg", (e) => { if (this.core.touchAction === "zoomSwipe" && (this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target))) { e.preventDefault(); this.core.touchAction = void 0; this.core.outer.removeClass("lg-zoom-dragging"); if (!isMoved) { this.settleIntoBounds(); return; } isMoved = false; this.touchendZoom( startCoords, endCoords, allowX, allowY, getWindowedVelocity(samples, Date.now()) ); } }); } zoomDrag() { let startCoords = {}; let endCoords = {}; let isDragging = false; let isMoved = false; let allowX = false; let allowY = false; let dragSamples = []; let possibleSwipeCords; let _LGel; this.core.outer.on("mousedown.lg.zoom", (e) => { if (!this.isImageSlide(this.core.index)) { return; } const $item = this.core.getSlideItem(this.core.index); if (this.$LG(e.target).hasClass("lg-item") || $item.get().contains(e.target)) { if (this.core.outer.hasClass("lg-zoomed")) { this.stopZoomSpring(); } dragSamples = [{ x: e.pageX, y: e.pageY, t: Date.now() }]; _LGel = this.core.getSlideItem(this.core.index).find(".lg-img-wrap").first(); const dragAllowedAxises = this.getDragAllowedAxises(0); allowY = dragAllowedAxises.allowY; allowX = dragAllowedAxises.allowX; if (this.core.outer.hasClass("lg-zoomed")) { if (this.$LG(e.target).hasClass("lg-object") && (allowX || allowY)) { e.preventDefault(); startCoords = this.getDragCords(e); possibleSwipeCords = this.getPossibleSwipeDragCords(); isDragging = true; this.core.outer.removeClass("lg-grab").addClass( "lg-grabbing lg-zoom-drag-transition lg-zoom-dragging" ); } } } }); this.$LG(window).on( `mousemove.lg.zoom.global${this.core.lgId}`, (e) => { if (isDragging) { isMoved = true; endCoords = this.getDragCords(e); dragSamples = pushVelocitySample(dragSamples, { x: endCoords.x, y: endCoords.y, t: Date.now() }); const distance = this.getZoomSwipeCords( startCoords, endCoords, allowX, allowY, possibleSwipeCords ); this.setZoomSwipeStyles(_LGel, distance); } } ); this.$LG(window).on(`mouseup.lg.zoom.global${this.core.lgId}`, (e) => { if (isDragging) { isDragging = false; this.core.outer.removeClass("lg-zoom-dragging"); if (isMoved && (startCoords.x !== endCoords.x || startCoords.y !== endCoords.y)) { endCoords = this.getDragCords(e); this.touchendZoom( startCoords, endCoords, allowX, allowY, getWindowedVelocity(dragSamples, Date.now()) ); } else { this.settleIntoBounds(); } isMoved = false; } this.core.outer.removeClass("lg-grabbing").addClass("lg-grab"); }); } closeGallery() { if (this.imageReset) { this.resetImageTranslate(this.core.index); } this.resetZoom(); this.zoomInProgress = false; } destroy() { this.$LG(window).off(`.lg.zoom.global${this.core.lgId}`); this.core.LGel.off(".lg.zoom"); this.core.LGel.off(".zoom"); clearTimeout(this.zoomableTimeout); this.zoomableTimeout = false; } } export { Zoom as default }; //# sourceMappingURL=lg-zoom.es5.js.map