diff --git a/biome.json b/biome.json index ac6d601..520cf49 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,12 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "ignore": [ + "packages/**/dist/*", + "packages/**/*.mjs", + "packages/**/*.d.ts" + ] + }, "linter": { "rules": { "correctness": { @@ -9,5 +16,28 @@ "noParameterAssign": "off" } } + }, + "css": { + "formatter": { + "enabled": true, + "quoteStyle": "double", + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true + }, + "parser": { + "cssModules": true + } + }, + "json": { + "formatter": { + "enabled": false, + "trailingCommas": "none" + }, + "linter": { + "enabled": false + } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a746972..ab235c6 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,8 @@ "author": "Biati Digital", "scripts": { "prepare": "npm run build --workspaces --if-present", - "lint": "npm run lint:scripts && npm run lint:styles", - "lint:scripts": "eslint ./packages --ext .ts", - "lint:styles": "stylelint './**/*.css' --allow-empty-input", + "lint": "npx @biomejs/biome ci .", + "format": "npx @biomejs/biome format --write .", "build": "npm run build --workspaces --if-present", "watch": "npm run watch --workspaces --if-present", "change:beta": "changeset pre enter beta", @@ -29,20 +28,14 @@ "private": true, "license": "GPLV3", "devDependencies": { + "@biomejs/biome": "^1.9.4", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.1", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "eslint": "^9.10.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", - "stylelint": "^16.2.1", - "stylelint-config-recommended": "^14.0.0", "ts-node": "^10.9.2", "typescript": "^5.0.2", "vite": "^5.1.6", "vite-plugin-dts": "^4.2.1", "vite-plugin-static-copy": "^1.0.1" } -} +} \ No newline at end of file diff --git a/packages/drag-navigation/package.json b/packages/drag-navigation/package.json index 1bf8d4f..6804a77 100644 --- a/packages/drag-navigation/package.json +++ b/packages/drag-navigation/package.json @@ -10,7 +10,7 @@ }, "bugs": "https://github.com/biati-digital/glightbox/issues", "type": "module", - "main": "./dist/index.cjs.js", + "main": "./dist/index.es.js", "module": "./dist/index.es.js", "types": "./dist/index.d.ts", "files": [ diff --git a/packages/drag-navigation/src/drag.ts b/packages/drag-navigation/src/drag.ts index 31a2b8c..8dcfe4f 100644 --- a/packages/drag-navigation/src/drag.ts +++ b/packages/drag-navigation/src/drag.ts @@ -1,114 +1,129 @@ -import type { PluginOptions, PluginType } from '@glightbox/plugin-core'; -import { GLightboxPlugin } from '@glightbox/plugin-core'; +import type { PluginOptions, PluginType } from "@glightbox/plugin-core"; +import { GLightboxPlugin } from "@glightbox/plugin-core"; export interface DragOptions extends PluginOptions { - dragToleranceX?: number; + dragToleranceX?: number; } export default class DragNavigation extends GLightboxPlugin { - name = 'drag'; - type: PluginType = 'other'; - options: DragOptions = {}; - isDown = false; - slider: HTMLElement | null = null; - actveSlide: HTMLElement | null = null; - startX = 0; - scrollLeft = 0; - activeSlideIndex = 0; - movedAmount = 0; - movedDirection = ''; - defaults: DragOptions = { - dragToleranceX: 10 - }; - mouseDownEvent: ((e: MouseEvent) => void) | null = null; - mouseLeaveEvent: ((e: MouseEvent) => void) | null = null; - mouseUpEvent: ((e: MouseEvent) => void) | null = null; - mouseMoveEvent: ((e: MouseEvent) => void) | null = null; + name = "drag"; + type: PluginType = "other"; + options: DragOptions = {}; + isDown = false; + slider: HTMLElement | null = null; + actveSlide: HTMLElement | null = null; + startX = 0; + scrollLeft = 0; + activeSlideIndex = 0; + movedAmount = 0; + movedDirection = ""; + defaults: DragOptions = { + dragToleranceX: 10, + }; + mouseDownEvent: ((e: MouseEvent) => void) | null = null; + mouseLeaveEvent: ((e: MouseEvent) => void) | null = null; + mouseUpEvent: ((e: MouseEvent) => void) | null = null; + mouseMoveEvent: ((e: MouseEvent) => void) | null = null; - constructor(options: Partial = {}) { - super(); - this.options = { ...this.defaults, ...options }; - } + constructor(options: Partial = {}) { + super(); + this.options = { ...this.defaults, ...options }; + } - init(): void { - const slider = document.querySelector('.gl-slider'); - if (!slider) { - return; - } + init(): void { + const slider = document.querySelector(".gl-slider"); + if (!slider) { + return; + } - this.mouseDownEvent = this.onMouseDown.bind(this); - this.mouseLeaveEvent = this.onMouseLeave.bind(this); - this.mouseUpEvent = this.onMouseUp.bind(this); - this.mouseMoveEvent = this.onMouseMove.bind(this); + this.mouseDownEvent = this.onMouseDown.bind(this); + this.mouseLeaveEvent = this.onMouseLeave.bind(this); + this.mouseUpEvent = this.onMouseUp.bind(this); + this.mouseMoveEvent = this.onMouseMove.bind(this); - slider.addEventListener('mousedown', this.mouseDownEvent); - slider.addEventListener('mouseleave', this.mouseLeaveEvent); - slider.addEventListener('mouseup', this.mouseUpEvent); - slider.addEventListener('mousemove', this.mouseMoveEvent); - this.slider = slider; - } + slider.addEventListener("mousedown", this.mouseDownEvent); + slider.addEventListener("mouseleave", this.mouseLeaveEvent); + slider.addEventListener("mouseup", this.mouseUpEvent); + slider.addEventListener("mousemove", this.mouseMoveEvent); + this.slider = slider; + } - destroy(): void { - this.mouseDownEvent && this.slider?.removeEventListener('mousedown', this.mouseDownEvent); - this.mouseLeaveEvent && this.slider?.removeEventListener('mouseleave', this.mouseLeaveEvent); - this.mouseUpEvent && this.slider?.removeEventListener('mouseup', this.mouseUpEvent); - this.mouseMoveEvent && this.slider?.removeEventListener('mousemove', this.mouseMoveEvent); - } + destroy(): void { + this.mouseDownEvent && + this.slider?.removeEventListener("mousedown", this.mouseDownEvent); + this.mouseLeaveEvent && + this.slider?.removeEventListener("mouseleave", this.mouseLeaveEvent); + this.mouseUpEvent && + this.slider?.removeEventListener("mouseup", this.mouseUpEvent); + this.mouseMoveEvent && + this.slider?.removeEventListener("mousemove", this.mouseMoveEvent); + } - onMouseDown(e: MouseEvent) { - if (!this.slider) { - return; - } - this.isDown = true; - this.slider?.classList.add('doing-drag'); - this.startX = e.pageX - this.slider.offsetLeft; - this.scrollLeft = this.slider.scrollLeft; - this.actveSlide = this.slider.querySelector('.visible'); - this.activeSlideIndex = Number.parseInt(this.actveSlide?.getAttribute('data-index') || '0'); - } + onMouseDown(e: MouseEvent) { + if (!this.slider) { + return; + } + this.isDown = true; + this.slider?.classList.add("doing-drag"); + this.startX = e.pageX - this.slider.offsetLeft; + this.scrollLeft = this.slider.scrollLeft; + this.actveSlide = this.slider.querySelector(".visible"); + this.activeSlideIndex = Number.parseInt( + this.actveSlide?.getAttribute("data-index") || "0", + ); + } - onMouseUp() { - this.isDown = false; - let scrollTo = this.actveSlide; - if (this.movedAmount > 10) { - const nextIndex = this.movedDirection === 'right' ? this.activeSlideIndex + 1 : this.activeSlideIndex - 1; - const next = this.slider?.querySelector(`div[data-index="${nextIndex}"]`); - if (next) { - scrollTo = next; - } - } - this.slider?.addEventListener('scrollend', this.removeDragClass); - scrollTo?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }); - } + onMouseUp() { + this.isDown = false; + let scrollTo = this.actveSlide; + if (this.movedAmount > 10) { + const nextIndex = + this.movedDirection === "right" + ? this.activeSlideIndex + 1 + : this.activeSlideIndex - 1; + const next = this.slider?.querySelector( + `div[data-index="${nextIndex}"]`, + ); + if (next) { + scrollTo = next; + } + } + this.slider?.addEventListener("scrollend", this.removeDragClass); + scrollTo?.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "start", + }); + } - onMouseMove(e: MouseEvent) { - if (!this.isDown || !this.slider) { - return; - } - e.preventDefault(); + onMouseMove(e: MouseEvent) { + if (!this.isDown || !this.slider) { + return; + } + e.preventDefault(); - const x = e.pageX - this.slider.offsetLeft; - const SCROLL_SPEED = 1; - const walk = (x - this.startX) * SCROLL_SPEED; + const x = e.pageX - this.slider.offsetLeft; + const SCROLL_SPEED = 1; + const walk = (x - this.startX) * SCROLL_SPEED; - const sliderWidth = this.slider.clientWidth; - let moved = this.scrollLeft - walk; - if (this.activeSlideIndex > 0) { - moved = moved - (sliderWidth * this.activeSlideIndex + 1); - } - const percentage = (moved / sliderWidth) * 100; - this.movedDirection = percentage > 0 ? 'right' : 'left'; - this.slider.scrollLeft = this.scrollLeft - walk; - this.movedAmount = Math.abs(percentage); - } + const sliderWidth = this.slider.clientWidth; + let moved = this.scrollLeft - walk; + if (this.activeSlideIndex > 0) { + moved = moved - (sliderWidth * this.activeSlideIndex + 1); + } + const percentage = (moved / sliderWidth) * 100; + this.movedDirection = percentage > 0 ? "right" : "left"; + this.slider.scrollLeft = this.scrollLeft - walk; + this.movedAmount = Math.abs(percentage); + } - onMouseLeave() { - this.isDown = false; - this.slider?.classList.remove('doing-drag'); - } + onMouseLeave() { + this.isDown = false; + this.slider?.classList.remove("doing-drag"); + } - removeDragClass() { - this.slider?.classList.remove('doing-drag'); - this.slider?.removeEventListener('scrollend', this.removeDragClass); - } + removeDragClass() { + this.slider?.classList.remove("doing-drag"); + this.slider?.removeEventListener("scrollend", this.removeDragClass); + } } diff --git a/packages/drag-navigation/src/index.ts b/packages/drag-navigation/src/index.ts index 5c1afcd..aceed00 100644 --- a/packages/drag-navigation/src/index.ts +++ b/packages/drag-navigation/src/index.ts @@ -1,2 +1 @@ -export { default as ImageSlide } from './drag'; - +export { default as ImageSlide } from "./drag"; diff --git a/packages/drag-navigation/vite.config.ts b/packages/drag-navigation/vite.config.ts index 5a59fe3..0fc6cfc 100644 --- a/packages/drag-navigation/vite.config.ts +++ b/packages/drag-navigation/vite.config.ts @@ -1,28 +1,25 @@ // vite.config.ts -import { resolve } from 'path'; -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; +import { resolve } from "node:path"; +import { defineConfig } from "vite"; +import dts from "vite-plugin-dts"; // https://vitejs.dev/guide/build.html#library-mode export default defineConfig({ - build: { - minify: true, - cssCodeSplit: true, - cssMinify: true, - lib: { - entry: resolve(__dirname, 'src/index.ts'), - name: 'DragPlugin', - fileName: (format, entryName) => { - if (format === 'umd') { - return `${entryName}.umd.js`; - } - if (format === 'cjs') { - return `${entryName}.cjs.js`; - } - return `${entryName}.es.js`; - }, - formats: ['es', 'cjs', 'umd'] - } - }, - plugins: [dts()] + build: { + minify: true, + cssCodeSplit: true, + cssMinify: true, + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "DragPlugin", + fileName: (format, entryName) => { + if (format === "umd") { + return `${entryName}.umd.js`; + } + return `${entryName}.es.js`; + }, + formats: ["es", "umd"], + }, + }, + plugins: [dts()], }); diff --git a/packages/glightbox/package.json b/packages/glightbox/package.json index 158ff69..c963f91 100644 --- a/packages/glightbox/package.json +++ b/packages/glightbox/package.json @@ -8,8 +8,8 @@ "type": "git" }, "type": "module", - "main": "./dist/index.cjs.js", - "import": "./dist/index.es.js", + "main": "./dist/glightbox.es.js", + "import": "./dist/glightbox.es.js", "scripts": { "dev": "vite", "watch": "vite build --watch", @@ -18,18 +18,15 @@ }, "exports": { ".": { - "import": "./dist/index.es.js", - "require": "./dist/index.cjs.js", + "import": "./dist/glightbox.es.js", "types": "./dist/index.d.ts" }, "./src": { "import": "./src/index.ts", - "require": "./src/index.ts", - "types": "./src/index.ts" + "types": "./src/index.d.ts" }, "./style": { - "import": "./dist/glightbox.css", - "require": "./dist/glightbox.css" + "import": "./dist/glightbox.css" } }, "directories": { @@ -51,10 +48,7 @@ }, "license": "GPLV3", "dependencies": { - "@glightbox/utils": "1.0.0-beta.1", - "@glightbox/plugin-core": "1.0.0-beta.3" - }, - "devDependencies": { - "vite-plugin-static-copy": "^1.0.1" + "@glightbox/plugin-core": "1.0.0-beta.3", + "@glightbox/utils": "1.0.0-beta.1" } } \ No newline at end of file diff --git a/packages/glightbox/src/glightbox.css b/packages/glightbox/src/glightbox.css index 28b9ba2..49f8a03 100644 --- a/packages/glightbox/src/glightbox.css +++ b/packages/glightbox/src/glightbox.css @@ -45,7 +45,6 @@ } .gl-single-slide { - .gl-prev, .gl-next { display: none; @@ -156,7 +155,7 @@ cursor: pointer; font: inherit; margin: 0; - opacity: var(--gl-button-opacity, .5); + opacity: var(--gl-button-opacity, 0.5); padding: 0; position: absolute; touch-action: manipulation; @@ -179,7 +178,7 @@ .gl-btn[disabled] { cursor: default; - opacity: var(--gl-button-opacity-disabled, .2); + opacity: var(--gl-button-opacity-disabled, 0.2); } .gl-btn:not([disabled]):hover { diff --git a/packages/glightbox/src/glightbox.ts b/packages/glightbox/src/glightbox.ts index e7d633c..aa88e2f 100644 --- a/packages/glightbox/src/glightbox.ts +++ b/packages/glightbox/src/glightbox.ts @@ -1,336 +1,404 @@ -import type { Plugin } from '@glightbox/plugin-core'; -import { addClass, addEvent, animate, hasClass, injectAssets, isNode, mergeObjects, removeClass } from '@glightbox/utils'; -import '@style'; -import { GLightboxDefaults } from './options'; -import type { ApiEvent, GLightboxOptions, SlideConfig } from './types'; +import type { Plugin } from "@glightbox/plugin-core"; +import { + addClass, + addEvent, + animate, + hasClass, + injectAssets, + isNode, + mergeObjects, + removeClass, +} from "@glightbox/utils"; +import "@style"; +import { GLightboxDefaults } from "./options"; +import type { ApiEvent, GLightboxOptions, SlideConfig } from "./types"; export default class GLightbox { - options: GLightboxOptions; - apiEvents: Set = new Set(); - state: Map = new Map(); - plugins: Map> = new Map(); - items: Set = new Set(); - modal: HTMLElement | null = null; - prevButton: HTMLButtonElement | null = null; - nextButton: HTMLButtonElement | null = null; - overlay: HTMLButtonElement | null = null; - slidesContainer: HTMLElement | null = null; - reduceMotion = false; - private observer: IntersectionObserver; - private eventsController: AbortController = new AbortController(); - - constructor(options: Partial = {}) { - this.options = mergeObjects(GLightboxDefaults, options); - - const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); - if ((!reduceMotion || reduceMotion.matches) && this.options.appearance?.slideEffect) { - this.reduceMotion = true; - } - - this.init(); - } - - private init(initPlugins = true): void { - if (initPlugins) { - for (const plugin of this.options.plugins) { - this.registerPlugin(plugin); - } - } - - if (this.options.setClickEvent) { - addEvent('click', { - element: '*[data-glightbox]', - signal: this.eventsController.signal, - callback: (target: HTMLElement) => { - console.log("clicked", target); - this.open(target); - } - }) - } - } - - public open(startAt: number | HTMLElement | undefined): void { - if (this.state.get('open')) { - return; - } - - if (!this.plugins.has('slide')) { - throw new Error('No slide types registered'); - } - - if (this.items.size === 0 && isNode(startAt)) { - this.setItemsFromNode(startAt as HTMLElement); - } - - let startingIndex = this.options.startAt; - if (!this.options.autoGallery) { - startingIndex = 0; - } else if (typeof startAt === 'number') { - startingIndex = startAt; - } else if (isNode(startAt)) { - startingIndex = this.getElementIndex(startAt as HTMLElement); - } - - this.state.set('focused', document.activeElement as HTMLElement); - this.build(); - this.trigger('before_open'); - this.initPlugins(); - this.showSlide(startingIndex as number, true); - this.trigger('open'); - this.state.set('open', true); - this.modal?.focus(); - addClass(document.getElementsByTagName('html')[0], 'gl-open'); - } - - public prevSlide(): void { - this.goToSlide(this.getActiveSlideIndex() - 1); - } - - public nextSlide(): void { - this.goToSlide(this.getActiveSlideIndex() + 1); - } - - public goToSlide(index = 0): void { - const total = this.getTotalSlides() - 1; - if (!this.options.loop && (index < 0 || index > this.getTotalSlides() - 1)) { - return; - } - if (index < 0) { - index = total; - } else if (index > total) { - index = 0; - } - this.showSlide(index); - } - - private async showSlide(index = 0, first = false): Promise { - const current = this.slidesContainer?.querySelector('.current'); - if (current) { - removeClass(current, 'current'); - } - - const slideNode = this.slidesContainer?.querySelectorAll('.gl-slide')[index] as HTMLElement; - const media = slideNode.querySelector('.gl-media'); - if (!slideNode || !media) { - return; - } - - if (!first) { - this.trigger('slide_before_change', { current: this.state.get('prevActiveSlideIndex'), next: index }); - } - - const effect = this.reduceMotion ? false : this.options.appearance?.slideEffect; - const openEffect = this.reduceMotion ? false : this.options.appearance?.openEffect; - const scrollAnim = effect !== 'slide' || first ? 'instant' : 'smooth'; - - slideNode.scrollIntoView({ behavior: scrollAnim, block: 'start', inline: 'start' }); - - await this.preloadSlide(index, !first); - - removeClass(media, 'gl-animation-ended'); - - if (first && openEffect) { - addClass(media, 'gl-invisible'); - if (openEffect) { - await animate(media, `gl-${openEffect}-in`); - } - addClass(media, 'gl-animation-ended'); - removeClass(media, `gl-invisible gl-${openEffect}-in`); - } else { - removeClass(media, 'gl-invisible'); - } - - this.setActiveSlideState(slideNode, index); - - this.trigger('slide_changed', { prev: this.state.get('prevActiveSlideIndex'), current: index }); - - if (this.options.preload) { - this.preloadSlide(index + 1); - this.preloadSlide(index - 1); - } - } - - private setActiveSlideState(activeNode: HTMLElement, index: number): void { - this.state.set('prevActiveSlide', this.state.get('activeSlide') ?? false); - this.state.set('prevActiveSlideIndex', this.getActiveSlideIndex()); - this.state.set('activeSlide', activeNode); - this.state.set('activeSlideIndex', index); - this.updateNavigationButtons(); - } - - private async preloadSlide(index: number, unhide = true): Promise { - if (index < 0 || index > this.items.size - 1) { - return false; - } - - const slideNode = this.slidesContainer?.querySelectorAll('.gl-slide')[index] as HTMLElement; - if (slideNode && (hasClass(slideNode, 'loaded') || hasClass(slideNode, 'preloading'))) { - return true; - } - - const slide = this.getSlideData(index); - const type = slide?.type; - const slideType = this.getRegisteredSlideType(type); - - let error = ''; - if (!type || !slideType) { - error = `Unable to handle URL: ${slide?.url}`; - } - if (error) { - this.setSlideError(slideNode, error as string); - throw new Error(error); - } - - if (slide?.url && slideType && slideNode && slideType?.build) { - addClass(slideNode, 'preloading'); - try { - if (slideType?.assets && typeof slideType?.assets === 'function') { - const slideAssets = slideType.assets(); - if (slideAssets) { - const cssAssets = slideAssets?.css || []; - const jsAssets = slideAssets?.js || []; - await this.injectAssets([...cssAssets, ...jsAssets]); - } - } - - this.trigger('slide_before_load', slide); - - await slideType.build({ - index: index, - slide: slideNode.querySelector('.gl-media') as HTMLElement, - config: { ...slide, isPreload: unhide } - }); - - slideNode.querySelector('.gl-slide-loader')?.remove(); - this.afterSlideLoaded(slideNode); - const media = slideNode.querySelector('.gl-media'); - if (media) { - addClass(media, `gl-type-${type}`); - unhide && removeClass(media, 'gl-invisible'); - } - return slideNode; - } catch (error) { - this.afterSlideLoaded(slideNode); - this.setSlideError(slideNode, error as string); - } - } - - return false; - } - - private build(): void { - if (this.state.get('build')) { - return; - } - - this.trigger('before_build'); - - const children = document.body.querySelectorAll(':scope > *'); - if (children) { - for (const el of children) { - if (el.parentNode === document.body && el.nodeName.charAt(0) !== '#' && el.hasAttribute && !el.hasAttribute('aria-hidden')) { - (el as HTMLElement).ariaHidden = 'true'; - (el as HTMLElement).dataset.glHidden = 'true'; - } - } - } - - const root = this.options?.root ?? document.body; - const lightboxHTML = this.options?.appearance?.lightboxHTML ?? ''; - root.insertAdjacentHTML('beforeend', lightboxHTML); - - this.modal = document.getElementById('gl-body'); - if (!this.modal) { - throw new Error('modal body not found'); - } - - const closeButton = this.modal.querySelector('.gl-close'); - this.prevButton = this.modal.querySelector('.gl-prev'); - this.nextButton = this.modal.querySelector('.gl-next'); - this.overlay = this.modal.querySelector('.gl-overlay'); - this.slidesContainer = document.getElementById('gl-slider'); - - addClass(this.modal, this.reduceMotion ? 'gl-reduce-motion' : 'gl-motion'); - addClass(this.modal, `gl-theme-${this.options?.appearance?.theme ?? 'base'}`); - addClass(this.modal, `gl-slide-effect-${this.options?.appearance?.slideEffect || 'none'}`); - - if (this.options?.appearance?.cssVariables) { - for (const [key, value] of Object.entries(this.options.appearance.cssVariables)) { - this.modal.style.setProperty(`--gl-${key}`, value); - } - } - - if (closeButton) { - addEvent('click', { - element: closeButton, - signal: this.eventsController.signal, - callback: () => this.close() - }); - } - if (this.nextButton) { - addEvent('click', { - element: this.nextButton, - signal: this.eventsController.signal, - callback: () => this.nextSlide() - }); - } - - if (this.prevButton) { - addEvent('click', { - element: this.prevButton, - signal: this.eventsController.signal, - callback: () => this.prevSlide() - }); - } - if (this.options.closeOnOutsideClick) { - addEvent('click', { - element: this.modal, - signal: this.eventsController.signal, - callback: (target: HTMLElement, e: Event) => { - if (target && e?.target && !(e?.target as HTMLElement)?.closest('.gl-media')) { - if (!(e.target as HTMLElement).closest('.gl-btn')) { - this.close(); - } - } - } - }); - } - - this.processVariables(this.modal); - - this.observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - removeClass(entry.target, 'visible'); - if (entry.isIntersecting && this.state.get('open')) { - const enteredIndex = Number.parseInt(entry.target?.getAttribute('data-index') ?? '0'); - addClass(entry.target, 'visible'); - - // on scroll make sure to recheck active indexes - if (enteredIndex !== this.state.get('activeSlideIndex')) { - this.setActiveSlideState(entry.target as HTMLElement, enteredIndex); - } - - if (!hasClass(entry.target, 'loaded') && !hasClass(entry.target, 'preloading')) { - this.preloadSlide(enteredIndex); - this.preloadSlide(enteredIndex + 1); - this.preloadSlide(enteredIndex - 1); - } - } - } - }, { - root: this.modal, - rootMargin: '0px', - threshold: 0.2 - }); - - let index = 0; - for (const item of this.items) { - const slideType = this.getRegisteredSlideType(item?.type); - if (item?.url && slideType) { - let slideHTML = this.options?.appearance?.slideHTML; - const loader = this.options?.appearance?.svg?.loader; - if (!slideHTML) { - slideHTML = `
+ options: GLightboxOptions; + apiEvents: Set = new Set(); + state: Map = new Map(); + plugins: Map> = new Map(); + items: Set = new Set(); + modal: HTMLElement | null = null; + prevButton: HTMLButtonElement | null = null; + nextButton: HTMLButtonElement | null = null; + overlay: HTMLButtonElement | null = null; + slidesContainer: HTMLElement | null = null; + reduceMotion = false; + private observer: IntersectionObserver; + private eventsController: AbortController; + + constructor(options: Partial = {}) { + this.options = mergeObjects(GLightboxDefaults, options); + + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + if ( + (!reduceMotion || reduceMotion.matches) && + this.options.appearance?.slideEffect + ) { + this.reduceMotion = true; + } + + this.init(); + } + + private init(initPlugins = true): void { + if (initPlugins) { + for (const plugin of this.options.plugins) { + this.registerPlugin(plugin); + } + } + + if (this.options.setClickEvent) { + addEvent("click", { + element: "*[data-glightbox]", + callback: (target: HTMLElement) => { + console.log("add event click"); + this.open(target); + }, + }); + } + } + + public open(startAt: number | HTMLElement | undefined): void { + if (this.state.get("open")) { + return; + } + + if (!this.plugins.has("slide")) { + throw new Error("No slide types registered"); + } + + if (this.items.size === 0 && isNode(startAt)) { + this.setItemsFromNode(startAt as HTMLElement); + } + + let startingIndex = this.options.startAt; + if (!this.options.autoGallery) { + startingIndex = 0; + } else if (typeof startAt === "number") { + startingIndex = startAt; + } else if (isNode(startAt)) { + startingIndex = this.getElementIndex(startAt as HTMLElement); + } + + this.state.set("focused", document.activeElement as HTMLElement); + this.build(); + this.trigger("before_open"); + this.initPlugins(); + this.showSlide(startingIndex as number, true); + this.trigger("open"); + this.state.set("open", true); + this.modal?.focus(); + addClass(document.getElementsByTagName("html")[0], "gl-open"); + } + + public prevSlide(): void { + this.goToSlide(this.getActiveSlideIndex() - 1); + } + + public nextSlide(): void { + this.goToSlide(this.getActiveSlideIndex() + 1); + } + + public goToSlide(index = 0): void { + const total = this.getTotalSlides() - 1; + if ( + !this.options.loop && + (index < 0 || index > this.getTotalSlides() - 1) + ) { + return; + } + if (index < 0) { + index = total; + } else if (index > total) { + index = 0; + } + this.showSlide(index); + } + + private async showSlide(index = 0, first = false): Promise { + const current = + this.slidesContainer?.querySelector(".current"); + if (current) { + removeClass(current, "current"); + } + + const slideNode = this.slidesContainer?.querySelectorAll(".gl-slide")[ + index + ] as HTMLElement; + const media = slideNode.querySelector(".gl-media"); + if (!slideNode || !media) { + return; + } + + if (!first) { + this.trigger("slide_before_change", { + current: this.state.get("prevActiveSlideIndex"), + next: index, + }); + } + + const effect = this.reduceMotion + ? false + : this.options.appearance?.slideEffect; + const openEffect = this.reduceMotion + ? false + : this.options.appearance?.openEffect; + const scrollAnim = effect !== "slide" || first ? "instant" : "smooth"; + + slideNode.scrollIntoView({ + behavior: scrollAnim, + block: "start", + inline: "start", + }); + + await this.preloadSlide(index, !first); + + removeClass(media, "gl-animation-ended"); + + if (first && openEffect) { + addClass(media, "gl-invisible"); + if (openEffect) { + await animate(media, `gl-${openEffect}-in`); + } + addClass(media, "gl-animation-ended"); + removeClass(media, `gl-invisible gl-${openEffect}-in`); + } else { + removeClass(media, "gl-invisible"); + } + + this.setActiveSlideState(slideNode, index); + + this.trigger("slide_changed", { + prev: this.state.get("prevActiveSlideIndex"), + current: index, + }); + + if (this.options.preload) { + this.preloadSlide(index + 1); + this.preloadSlide(index - 1); + } + } + + private setActiveSlideState(activeNode: HTMLElement, index: number): void { + this.state.set("prevActiveSlide", this.state.get("activeSlide") ?? false); + this.state.set("prevActiveSlideIndex", this.getActiveSlideIndex()); + this.state.set("activeSlide", activeNode); + this.state.set("activeSlideIndex", index); + this.updateNavigationButtons(); + } + + private async preloadSlide( + index: number, + unhide = true, + ): Promise { + if (index < 0 || index > this.items.size - 1) { + return false; + } + + const slideNode = this.slidesContainer?.querySelectorAll(".gl-slide")[ + index + ] as HTMLElement; + if ( + slideNode && + (hasClass(slideNode, "loaded") || hasClass(slideNode, "preloading")) + ) { + return true; + } + + const slide = this.getSlideData(index); + const type = slide?.type; + const slideType = this.getRegisteredSlideType(type); + + let error = ""; + if (!type || !slideType) { + error = `Unable to handle URL: ${slide?.url}`; + } + if (error) { + this.setSlideError(slideNode, error as string); + throw new Error(error); + } + + if (slide?.url && slideType && slideNode && slideType?.build) { + addClass(slideNode, "preloading"); + try { + if (slideType?.assets && typeof slideType?.assets === "function") { + const slideAssets = slideType.assets(); + if (slideAssets) { + const cssAssets = slideAssets?.css || []; + const jsAssets = slideAssets?.js || []; + await this.injectAssets([...cssAssets, ...jsAssets]); + } + } + + this.trigger("slide_before_load", slide); + + await slideType.build({ + index: index, + slide: slideNode.querySelector(".gl-media") as HTMLElement, + config: { ...slide, isPreload: unhide }, + }); + + slideNode.querySelector(".gl-slide-loader")?.remove(); + this.afterSlideLoaded(slideNode); + const media = slideNode.querySelector(".gl-media"); + if (media) { + addClass(media, `gl-type-${type}`); + unhide && removeClass(media, "gl-invisible"); + } + return slideNode; + } catch (error) { + this.afterSlideLoaded(slideNode); + this.setSlideError(slideNode, error as string); + } + } + + return false; + } + + private build(): void { + if (this.state.get("build")) { + return; + } + + this.eventsController = new AbortController(); + this.trigger("before_build"); + + const children = document.body.querySelectorAll(":scope > *"); + if (children) { + for (const el of children) { + if ( + el.parentNode === document.body && + el.nodeName.charAt(0) !== "#" && + el.hasAttribute && + !el.hasAttribute("aria-hidden") + ) { + (el as HTMLElement).ariaHidden = "true"; + (el as HTMLElement).dataset.glHidden = "true"; + } + } + } + + const root = this.options?.root ?? document.body; + const lightboxHTML = this.options?.appearance?.lightboxHTML ?? ""; + root.insertAdjacentHTML("beforeend", lightboxHTML); + + this.modal = document.getElementById("gl-body"); + if (!this.modal) { + throw new Error("modal body not found"); + } + + const closeButton = this.modal.querySelector(".gl-close"); + this.prevButton = this.modal.querySelector(".gl-prev"); + this.nextButton = this.modal.querySelector(".gl-next"); + this.overlay = this.modal.querySelector(".gl-overlay"); + this.slidesContainer = document.getElementById("gl-slider"); + + addClass(this.modal, this.reduceMotion ? "gl-reduce-motion" : "gl-motion"); + addClass( + this.modal, + `gl-theme-${this.options?.appearance?.theme ?? "base"}`, + ); + addClass( + this.modal, + `gl-slide-effect-${this.options?.appearance?.slideEffect || "none"}`, + ); + + if (this.options?.appearance?.cssVariables) { + for (const [key, value] of Object.entries( + this.options.appearance.cssVariables, + )) { + this.modal.style.setProperty(`--gl-${key}`, value); + } + } + + if (closeButton) { + addEvent("click", { + element: closeButton, + signal: this.eventsController.signal, + callback: () => this.close(), + }); + } + if (this.nextButton) { + addEvent("click", { + element: this.nextButton, + signal: this.eventsController.signal, + callback: () => this.nextSlide(), + }); + } + + if (this.prevButton) { + addEvent("click", { + element: this.prevButton, + signal: this.eventsController.signal, + callback: () => this.prevSlide(), + }); + } + if (this.options.closeOnOutsideClick) { + addEvent("click", { + element: this.modal, + signal: this.eventsController.signal, + callback: (target: HTMLElement, e: Event) => { + if ( + target && + e?.target && + !(e?.target as HTMLElement)?.closest(".gl-media") + ) { + if (!(e.target as HTMLElement).closest(".gl-btn")) { + this.close(); + } + } + }, + }); + } + + this.processVariables(this.modal); + + this.observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + removeClass(entry.target, "visible"); + if (entry.isIntersecting && this.state.get("open")) { + const enteredIndex = Number.parseInt( + entry.target?.getAttribute("data-index") ?? "0", + ); + addClass(entry.target, "visible"); + + // on scroll make sure to recheck active indexes + if (enteredIndex !== this.state.get("activeSlideIndex")) { + this.setActiveSlideState( + entry.target as HTMLElement, + enteredIndex, + ); + } + + if ( + !hasClass(entry.target, "loaded") && + !hasClass(entry.target, "preloading") + ) { + this.preloadSlide(enteredIndex); + this.preloadSlide(enteredIndex + 1); + this.preloadSlide(enteredIndex - 1); + } + } + } + }, + { + root: this.modal, + rootMargin: "0px", + threshold: 0.2, + }, + ); + + let index = 0; + for (const item of this.items) { + const slideType = this.getRegisteredSlideType(item?.type); + if (item?.url && slideType) { + let slideHTML = this.options?.appearance?.slideHTML; + const loader = this.options?.appearance?.svg?.loader; + if (!slideHTML) { + slideHTML = `
${loader} Loading... @@ -338,432 +406,446 @@ export default class GLightbox {
`; - } - - this.slidesContainer?.insertAdjacentHTML('beforeend', slideHTML); - const created = this.slidesContainer?.querySelectorAll('.gl-slide')[index]; - if (created) { - this.observer.observe(created); - } - index++; - } - } - - if (this.overlay) { - addClass(this.overlay, 'gl-overlay-in'); - } - this.state.set('build', true); - this.trigger('build'); - } - - public async close(): Promise { - if (!this.state.get('open') || !this.modal) { - return; - } - - this.runPluginsMethod('destroy'); - - removeClass(document.getElementsByTagName('html')[0], 'gl-open'); - - const hiddenElements = document.querySelectorAll('*[data-gl-hidden="true"]'); - if (hiddenElements) { - for (const el of hiddenElements) { - (el as HTMLElement).ariaHidden = 'false'; - delete (el as HTMLElement).dataset.glHidden; - } - } - - if (this.reduceMotion || this.options.appearance?.slideEffect === 'none') { - this.modal.parentNode?.removeChild(this.modal); - } else { - const currentSlide = this.state.get('activeSlide'); - const media = (currentSlide as HTMLElement).querySelector('.gl-media'); - const openEffect = this.options.appearance?.openEffect; - if (media) { - addClass(this.modal, 'gl-closing'); - if (openEffect) { - removeClass(media, 'gl-animation-ended'); - await animate(media, `gl-${openEffect}-out`); - } - } - this.modal.parentNode?.removeChild(this.modal); - } - - this.state.clear(); - this.modal = null; - this.prevButton = null; - this.nextButton = null; - this.clearAllEvents(); - this.setItems([]); - - const styles = document.querySelectorAll('.gl-css'); - if (styles) { - for (const style of styles) { - style?.parentNode?.removeChild(style); - } - } - - this.trigger('close'); - const restoreFocus = this.state.get('focused') as HTMLElement; - restoreFocus?.focus(); - } - - public destroy(): void { - this.close(); - this.clearAllEvents(); - } - - public reload(): void { - this.init(false); - } - - public setItems(items: SlideConfig[]): void { - if (!items) { - return; - } - - this.items = new Set(); - if (!items.length) { - return; - } - - const slideModules = this.plugins.get('slide'); - if (!slideModules) { - throw new Error('No slide types registered'); - } - for (const item of items) { - if (item?.type) { - if (!slideModules.has(item.type)) { - throw new Error(`Unknown slide type: ${item.type}`); - } - continue; - } - - let generalSlideTye: false | Plugin = false; - for (const [key, slideType] of slideModules) { - if (slideType.name === 'iframe') { - generalSlideTye = slideType; - continue; - } - - let matched = false; - if (slideType?.match?.(item.url.toLowerCase())) { - item.type = key; - matched = true; - } - - if (slideType?.options && typeof slideType.options?.matchFn === 'function') { - if (slideType.options?.matchFn(matched, item.url.toLowerCase())) { - item.type = key; - matched = true; - } - } - - if (matched) { - break; - } - } - if (!item?.type) { - if (generalSlideTye) { - item.type = generalSlideTye.name; - } - } - this.getItems().add(item); - } - } - - private setItemsFromNode(node: HTMLElement): void { - if (!this.options.autoGallery) { - this.setItems([this.parseConfigFromNode(node)]); - return; - } - let selector = '*[data-glightbox]'; - const gallery = node.getAttribute('data-glightbox'); - if (gallery) { - selector = `*[data-glightbox="${gallery}"]`; - } - - const items = document.querySelectorAll(selector); - if (!items) { - return; - } - const parsedItemsData: SlideConfig[] = []; - for (const item of items) { - const itemData: SlideConfig = this.parseConfigFromNode(item as HTMLElement); - parsedItemsData.push(itemData); - } - this.setItems(parsedItemsData); - } - - public getSettings(): GLightboxOptions { - return this.options; - } - - private getElementIndex(node: HTMLElement): number { - let index = 0; - let count = 0; - for (const item of this.items) { - if (item?.node === node) { - index = count; - break; - } - count++; - } - return index; - } - - public getActiveSlide(): HTMLElement | undefined { - if (this.state.has('activeSlide')) { - return this.state.get('activeSlide') as HTMLElement; - } - return; - } - - public getActiveSlideIndex(): number { - if (this.state.has('activeSlideIndex')) { - return this.state.get('activeSlideIndex') as number; - } - return 0; - } - - public getTotalSlides(): number { - return this.items.size; - } - - public getItems(): Set { - return this.items; - } - - public updateNavigationButtons(): void { - if (this.items.size === 1) { - this.modal && addClass(this.modal, 'gl-single-slide'); - return; - } - if (!this.nextButton || !this.prevButton) { - return; - } - - const loop = this.options.loop; - const currentIndex = this.getActiveSlideIndex(); - const total = this.getTotalSlides() - 1; - - this.prevButton.disabled = false; - this.nextButton.disabled = false; - - if (currentIndex === 0 && !loop) { - this.prevButton.disabled = true; - } else if (currentIndex === total && !loop) { - this.nextButton.disabled = true; - } - } - - private setSlideError(slide: HTMLElement, error: string): void { - slide.querySelector('.gl-slide-loader')?.remove(); - const media = slide.querySelector('.gl-media'); - if (media) { - addClass(media, 'gl-load-error'); - removeClass(media, 'gl-invisible'); - media.innerHTML = `
${error}
`; - } - } - - private afterSlideLoaded(slide: HTMLElement): void { - addClass(slide, 'loaded'); - removeClass(slide, 'preloading'); - } - - public on(evt: string, callback: () => void, once = false): void { - if (!evt || typeof callback !== 'function') { - throw new TypeError('Event name and callback must be defined'); - } - this.apiEvents.add({ evt, once, callback }); - } - - public once(evt: string, callback: () => void): void { - this.on(evt, callback, true); - } - - protected trigger(eventName: string, data: unknown = null): void { - for (const event of this.apiEvents) { - const { evt, once, callback } = event; - if (evt === eventName) { - callback(data); - if (once) { - this.apiEvents.delete(event); - } - } - } - } - - private parseConfigFromNode(element: HTMLElement): SlideConfig { - const slideDefaults: SlideConfig = { - node: null, - url: '', - title: '', - description: '', - width: '', - height: '', - content: '', - type: '' - }; - - let url = ''; - const data: SlideConfig = { url: '', type: '' }; - const nodeType = element.nodeName.toLowerCase(); - - if (nodeType === 'a') { - url = element.getAttribute('href') || ''; - } - if (nodeType === 'img') { - url = element.getAttribute('src') || ''; - } - if (nodeType === 'figure') { - url = element.querySelector('img')?.getAttribute('src') || ''; - } - - data.node = element; - data.url = url; - - for (const key in slideDefaults) { - let attr = 'data'; - if (this.options?.dataAttributesPrefix) { - attr += `-${this.options?.dataAttributesPrefix}`; - } - let nodeData: string | boolean | null = element.getAttribute(`${attr}-${key}`); - if (nodeData) { - if (nodeData === 'true' || nodeData === 'false') { - nodeData = nodeData === 'true'; - } - data[key] = nodeData; - } - } - if (!data.title) { - const title = element?.getAttribute('title'); - if (title) { - data.title = title; - } - } - - if (data?.description?.startsWith('.')) { - const description = document.querySelector(data.description)?.innerHTML; - if (description) { - data.description = description; - } - } - - if (!data.description) { - const nodeDesc = element.querySelector('.gl-inline-desc'); - if (nodeDesc) { - data.description = nodeDesc.innerHTML; - } - } - - return data; - } - - private getRegisteredSlideType(id: string): false | Plugin { - if (this.plugins.has('slide')) { - const slideModules = this.plugins.get('slide'); - if (slideModules?.has(id)) { - return slideModules.get(id) as Plugin; - } - } - - return false; - } - - private getSlideData(index: number) { - return [...this.items][index]; - } - - public processVariables(node: HTMLElement): void { - const variables = { - 'current-slide': '', - 'total-slides': '', - 'close-svg': this.options?.appearance?.svg?.close ?? '', - 'next-svg': this.options?.appearance?.svg?.next ?? '', - 'prev-svg': this.options?.appearance?.svg?.prev ?? '' - }; - - for (const [key, value] of Object.entries(variables)) { - const nodeInner = node.querySelector(`*[data-glightbox-${key}]`); - if (nodeInner) { - nodeInner.innerHTML = value; - } - } - return; - } - - protected registerPlugin(plugin: Plugin): void { - if (!this.plugins.has(plugin.type)) { - this.plugins.set(plugin.type, new Map()); - } - const typeModules = this.plugins.get(plugin.type); - plugin.instance = this; - typeModules?.set(plugin.name, plugin); - } - - protected initPlugins() { - this.pluginsRunEach((plugin: Plugin) => { - if (typeof plugin.init === 'function') { - plugin.init(); - } - if (typeof plugin.cssStyle === 'function') { - const css = plugin?.cssStyle(); - this.injectCSS(css); - } - }); - } - - protected runPluginsMethod(method: string): void { - this.pluginsRunEach((plugin: Plugin) => { - if (typeof plugin[method as (keyof typeof plugin)] === 'function') { - const methodFn = plugin[method as (keyof typeof plugin)]; - if (typeof methodFn === 'function') { - methodFn?.apply(plugin); - } - } - }); - } - - public pluginsRunEach(callback: (plugin: Plugin) => void): void { - for (const [type, pluginTypes] of this.plugins) { - for (const [name, plugin] of pluginTypes) { - callback(plugin); - } - } - } - - protected injectCSS(css: string): void { - if (!css) { - return; - } - const el = document.createElement('style'); - el.type = 'text/css'; - el.className = 'gl-css'; - el.innerText = css; - document.head.appendChild(el); - } - - public async injectAssets(urls: (string | string[] | { src: string; module?: boolean })[]): Promise { - if (typeof urls === 'string') { - urls = [urls]; - } - - urls.map(async (url) => { - let load = url; - if (typeof url === 'string') { - load = url as string; - } else { - load = url as { src: string; module?: boolean }; - } - await injectAssets(load); - }); - } - - private clearAllEvents(): void { - this.eventsController.abort(); - this.observer.disconnect(); - this.apiEvents.clear(); - } + } + + this.slidesContainer?.insertAdjacentHTML("beforeend", slideHTML); + const created = + this.slidesContainer?.querySelectorAll(".gl-slide")[index]; + if (created) { + this.observer.observe(created); + } + index++; + } + } + + if (this.overlay) { + addClass(this.overlay, "gl-overlay-in"); + } + this.state.set("build", true); + this.trigger("build"); + } + + public async close(): Promise { + if (!this.state.get("open") || !this.modal) { + return; + } + + this.runPluginsMethod("destroy"); + + removeClass(document.getElementsByTagName("html")[0], "gl-open"); + + const hiddenElements = document.querySelectorAll( + '*[data-gl-hidden="true"]', + ); + if (hiddenElements) { + for (const el of hiddenElements) { + (el as HTMLElement).ariaHidden = "false"; + delete (el as HTMLElement).dataset.glHidden; + } + } + + if (this.reduceMotion || this.options.appearance?.slideEffect === "none") { + this.modal.parentNode?.removeChild(this.modal); + } else { + const currentSlide = this.state.get("activeSlide"); + const media = (currentSlide as HTMLElement).querySelector( + ".gl-media", + ); + const openEffect = this.options.appearance?.openEffect; + if (media) { + addClass(this.modal, "gl-closing"); + if (openEffect) { + removeClass(media, "gl-animation-ended"); + await animate(media, `gl-${openEffect}-out`); + } + } + this.modal.parentNode?.removeChild(this.modal); + } + + this.state.clear(); + this.modal = null; + this.prevButton = null; + this.nextButton = null; + this.clearAllEvents(); + this.setItems([]); + + const styles = document.querySelectorAll(".gl-css"); + if (styles) { + for (const style of styles) { + style?.parentNode?.removeChild(style); + } + } + + this.trigger("close"); + const restoreFocus = this.state.get("focused") as HTMLElement; + restoreFocus?.focus(); + } + + public destroy(): void { + this.close(); + this.clearAllEvents(); + } + + public reload(): void { + this.init(false); + } + + public setItems(items: SlideConfig[]): void { + if (!items) { + return; + } + + this.items = new Set(); + if (!items.length) { + return; + } + + const slideModules = this.plugins.get("slide"); + if (!slideModules) { + throw new Error("No slide types registered"); + } + for (const item of items) { + if (item?.type) { + if (!slideModules.has(item.type)) { + throw new Error(`Unknown slide type: ${item.type}`); + } + continue; + } + + let generalSlideTye: false | Plugin = false; + for (const [key, slideType] of slideModules) { + if (slideType.name === "iframe") { + generalSlideTye = slideType; + continue; + } + + let matched = false; + if (slideType?.match?.(item.url.toLowerCase())) { + item.type = key; + matched = true; + } + + if ( + slideType?.options && + typeof slideType.options?.matchFn === "function" + ) { + if (slideType.options?.matchFn(matched, item.url.toLowerCase())) { + item.type = key; + matched = true; + } + } + + if (matched) { + break; + } + } + if (!item?.type) { + if (generalSlideTye) { + item.type = generalSlideTye.name; + } + } + this.getItems().add(item); + } + } + + private setItemsFromNode(node: HTMLElement): void { + if (!this.options.autoGallery) { + this.setItems([this.parseConfigFromNode(node)]); + return; + } + let selector = "*[data-glightbox]"; + const gallery = node.getAttribute("data-glightbox"); + if (gallery) { + selector = `*[data-glightbox="${gallery}"]`; + } + + const items = document.querySelectorAll(selector); + if (!items) { + return; + } + const parsedItemsData: SlideConfig[] = []; + for (const item of items) { + const itemData: SlideConfig = this.parseConfigFromNode( + item as HTMLElement, + ); + parsedItemsData.push(itemData); + } + this.setItems(parsedItemsData); + } + + public getSettings(): GLightboxOptions { + return this.options; + } + + private getElementIndex(node: HTMLElement): number { + let index = 0; + let count = 0; + for (const item of this.items) { + if (item?.node === node) { + index = count; + break; + } + count++; + } + return index; + } + + public getActiveSlide(): HTMLElement | undefined { + if (this.state.has("activeSlide")) { + return this.state.get("activeSlide") as HTMLElement; + } + return; + } + + public getActiveSlideIndex(): number { + if (this.state.has("activeSlideIndex")) { + return this.state.get("activeSlideIndex") as number; + } + return 0; + } + + public getTotalSlides(): number { + return this.items.size; + } + + public getItems(): Set { + return this.items; + } + + public updateNavigationButtons(): void { + if (this.items.size === 1) { + this.modal && addClass(this.modal, "gl-single-slide"); + return; + } + if (!this.nextButton || !this.prevButton) { + return; + } + + const loop = this.options.loop; + const currentIndex = this.getActiveSlideIndex(); + const total = this.getTotalSlides() - 1; + + this.prevButton.disabled = false; + this.nextButton.disabled = false; + + if (currentIndex === 0 && !loop) { + this.prevButton.disabled = true; + } else if (currentIndex === total && !loop) { + this.nextButton.disabled = true; + } + } + + private setSlideError(slide: HTMLElement, error: string): void { + slide.querySelector(".gl-slide-loader")?.remove(); + const media = slide.querySelector(".gl-media"); + if (media) { + addClass(media, "gl-load-error"); + removeClass(media, "gl-invisible"); + media.innerHTML = `
${error}
`; + } + } + + private afterSlideLoaded(slide: HTMLElement): void { + addClass(slide, "loaded"); + removeClass(slide, "preloading"); + } + + public on(evt: string, callback: () => void, once = false): void { + if (!evt || typeof callback !== "function") { + throw new TypeError("Event name and callback must be defined"); + } + this.apiEvents.add({ evt, once, callback }); + } + + public once(evt: string, callback: () => void): void { + this.on(evt, callback, true); + } + + protected trigger(eventName: string, data: unknown = null): void { + for (const event of this.apiEvents) { + const { evt, once, callback } = event; + if (evt === eventName) { + callback(data); + if (once) { + this.apiEvents.delete(event); + } + } + } + } + + private parseConfigFromNode(element: HTMLElement): SlideConfig { + const slideDefaults: SlideConfig = { + node: null, + url: "", + title: "", + description: "", + width: "", + height: "", + content: "", + type: "", + }; + + let url = ""; + const data: SlideConfig = { url: "", type: "" }; + const nodeType = element.nodeName.toLowerCase(); + + if (nodeType === "a") { + url = element.getAttribute("href") || ""; + } + if (nodeType === "img") { + url = element.getAttribute("src") || ""; + } + if (nodeType === "figure") { + url = element.querySelector("img")?.getAttribute("src") || ""; + } + + data.node = element; + data.url = url; + + for (const key in slideDefaults) { + let attr = "data"; + if (this.options?.dataAttributesPrefix) { + attr += `-${this.options?.dataAttributesPrefix}`; + } + let nodeData: string | boolean | null = element.getAttribute( + `${attr}-${key}`, + ); + if (nodeData) { + if (nodeData === "true" || nodeData === "false") { + nodeData = nodeData === "true"; + } + data[key] = nodeData; + } + } + if (!data.title) { + const title = element?.getAttribute("title"); + if (title) { + data.title = title; + } + } + + if (data?.description?.startsWith(".")) { + const description = document.querySelector(data.description)?.innerHTML; + if (description) { + data.description = description; + } + } + + if (!data.description) { + const nodeDesc = element.querySelector(".gl-inline-desc"); + if (nodeDesc) { + data.description = nodeDesc.innerHTML; + } + } + + return data; + } + + private getRegisteredSlideType(id: string): false | Plugin { + if (this.plugins.has("slide")) { + const slideModules = this.plugins.get("slide"); + if (slideModules?.has(id)) { + return slideModules.get(id) as Plugin; + } + } + + return false; + } + + private getSlideData(index: number) { + return [...this.items][index]; + } + + public processVariables(node: HTMLElement): void { + const variables = { + "current-slide": "", + "total-slides": "", + "close-svg": this.options?.appearance?.svg?.close ?? "", + "next-svg": this.options?.appearance?.svg?.next ?? "", + "prev-svg": this.options?.appearance?.svg?.prev ?? "", + }; + + for (const [key, value] of Object.entries(variables)) { + const nodeInner = node.querySelector(`*[data-glightbox-${key}]`); + if (nodeInner) { + nodeInner.innerHTML = value; + } + } + return; + } + + protected registerPlugin(plugin: Plugin): void { + if (!this.plugins.has(plugin.type)) { + this.plugins.set(plugin.type, new Map()); + } + const typeModules = this.plugins.get(plugin.type); + plugin.instance = this; + typeModules?.set(plugin.name, plugin); + } + + protected initPlugins() { + this.pluginsRunEach((plugin: Plugin) => { + if (typeof plugin.init === "function") { + plugin.init(); + } + if (typeof plugin.cssStyle === "function") { + const css = plugin?.cssStyle(); + this.injectCSS(css); + } + }); + } + + protected runPluginsMethod(method: string): void { + this.pluginsRunEach((plugin: Plugin) => { + if (typeof plugin[method as keyof typeof plugin] === "function") { + const methodFn = plugin[method as keyof typeof plugin]; + if (typeof methodFn === "function") { + methodFn?.apply(plugin); + } + } + }); + } + + public pluginsRunEach(callback: (plugin: Plugin) => void): void { + for (const [type, pluginTypes] of this.plugins) { + for (const [name, plugin] of pluginTypes) { + callback(plugin); + } + } + } + + protected injectCSS(css: string): void { + if (!css) { + return; + } + const el = document.createElement("style"); + el.type = "text/css"; + el.className = "gl-css"; + el.innerText = css; + document.head.appendChild(el); + } + + public async injectAssets( + urls: (string | string[] | { src: string; module?: boolean })[], + ): Promise { + if (typeof urls === "string") { + urls = [urls]; + } + + urls.map(async (url) => { + let load = url; + if (typeof url === "string") { + load = url as string; + } else { + load = url as { src: string; module?: boolean }; + } + await injectAssets(load); + }); + } + + private clearAllEvents(): void { + this.eventsController.abort(); + this.observer.disconnect(); + this.apiEvents.clear(); + } } diff --git a/packages/glightbox/src/index.ts b/packages/glightbox/src/index.ts index e9bdd5a..15acef4 100644 --- a/packages/glightbox/src/index.ts +++ b/packages/glightbox/src/index.ts @@ -1,3 +1,2 @@ -export { default as GLightbox } from './glightbox'; -export * from './types'; - +export { default as GLightbox } from "./glightbox"; +export * from "./types"; diff --git a/packages/glightbox/src/options.ts b/packages/glightbox/src/options.ts index f7f269c..331e0cb 100644 --- a/packages/glightbox/src/options.ts +++ b/packages/glightbox/src/options.ts @@ -1,35 +1,37 @@ -import type { GLightboxOptions } from './types'; +import type { GLightboxOptions } from "./types"; export const GLightboxDefaults: GLightboxOptions = { - root: null, - autoGallery: true, - setClickEvent: true, - dataAttributesPrefix: '', - items: [], - plugins: [], - appearance: { - slideEffect: 'fade', - openEffect: 'zoom', - moreText: 'See more', - moreLength: 60, - lightboxHTML: `