From 4ae0e46a7d440e0bc385e7384d48f7768bf84906 Mon Sep 17 00:00:00 2001 From: Franzz Date: Thu, 27 Aug 2026 20:37:02 +0200 Subject: [PATCH] Convert Map & Lightbox to vue components --- src/components/Lightbox.vue | 585 +++++++++++++++++++++++++++++ src/components/Project.vue | 606 ++++-------------------------- src/components/ProjectMap.vue | 525 ++++++++++++++++++++++++++ src/components/ProjectPost.vue | 6 +- src/scripts/common.js | 7 + src/scripts/lightbox.js | 650 --------------------------------- 6 files changed, 1192 insertions(+), 1187 deletions(-) create mode 100644 src/components/Lightbox.vue create mode 100644 src/components/ProjectMap.vue delete mode 100644 src/scripts/lightbox.js diff --git a/src/components/Lightbox.vue b/src/components/Lightbox.vue new file mode 100644 index 0000000..8495ee8 --- /dev/null +++ b/src/components/Lightbox.vue @@ -0,0 +1,585 @@ + + + diff --git a/src/components/Project.vue b/src/components/Project.vue index 74f07f3..200ec65 100644 --- a/src/components/Project.vue +++ b/src/components/Project.vue @@ -1,47 +1,19 @@ + + diff --git a/src/components/ProjectPost.vue b/src/components/ProjectPost.vue index 5129c48..fcc7f60 100644 --- a/src/components/ProjectPost.vue +++ b/src/components/ProjectPost.vue @@ -71,11 +71,7 @@ relatedMarker() { //Find corresponding marker if(!this.options.longitude && !this.options.latitude && this.options.type == 'media') { - return this.project.markers.find((marker) => { - return (marker.medias || []).some((media) => { - return media.id_media == this.options.id_media; - }); - }) || null; + return this.map.findMarkerByMediaId(this.options.id_media); } else if( ['message', 'media'].includes(this.options.type) diff --git a/src/scripts/common.js b/src/scripts/common.js index 76d1395..f0181f0 100644 --- a/src/scripts/common.js +++ b/src/scripts/common.js @@ -1,5 +1,12 @@ /* Common Functions */ +//All the custom properties this reads (--space, --track-*, --trans-*, --zoom-scale...) +//are declared on :root (_common.scss), so there's no need to read them from any +//specific component's own element - document.documentElement always has them. +export function getStyleProperty(sProperty) { + return getComputedStyle(document.documentElement).getPropertyValue(sProperty).trim(); +} + export function copyTextToClipboard(text) { if(!navigator.clipboard) { let textArea = document.createElement('textarea'); diff --git a/src/scripts/lightbox.js b/src/scripts/lightbox.js deleted file mode 100644 index be3da6c..0000000 --- a/src/scripts/lightbox.js +++ /dev/null @@ -1,650 +0,0 @@ -import { icon } from '@fortawesome/fontawesome-svg-core'; -import { getIcon } from '@scripts/icons'; - -export default class Lightbox { - constructor(options = {}) { - this.album = []; - this.currentImageIndex = 0; - this.options = { - alwaysShowNavOnTouchDevices: false, - fadeDuration: 600, - imageFadeDuration: 600, - positionFromTop: 50, - resizeDuration: 700, - wrapAround: false, - disableScrolling: false, - sanitizeTitle: false, - hasVideo: true, - onMediaChange: () => {}, - onClosing: () => {} - }; - this.option(options); - this.gMouseDownOffsetX = 0; - this.gMouseDownOffsetY = 0; - this.resizeTimer = null; - this.boundOnBodyClick = this.onBodyClick.bind(this); - this.boundOnResize = this.sizeOverlay.bind(this); - this.boundOnKeyUp = this.keyboardAction.bind(this); - this.boundOnWheel = this.onWheel.bind(this); - this.boundOnDragStart = this.onDragStart.bind(this); - this.boundOnDragMove = this.onDragMove.bind(this); - this.boundOnDragEnd = this.onDragEnd.bind(this); - this.init(); - } - - option(options = {}) { - Object.assign(this.options, options); - } - - init() { - if(document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - this.build(); - this.enable(); - }, { once: true }); - } else { - this.build(); - this.enable(); - } - } - - enable() { - document.body.addEventListener('click', this.boundOnBodyClick); - } - - disable() { - document.body.removeEventListener('click', this.boundOnBodyClick); - } - - onBodyClick(event) { - const link = event.target.closest('a[data-lightbox], area[data-lightbox]'); - if(!link) return; - event.preventDefault(); - this.start(link); - } - - renderIcon(name, sClass=null) { - return icon(getIcon(name), {classes: ['app-icon', name, sClass]}).html; - } - - build() { - if(!document.getElementById('lightbox')) { - const wrapper = document.createElement('div'); - wrapper.innerHTML = ` -
- - `; - document.body.append(...wrapper.children); - } - - this.overlay = document.getElementById('lightboxOverlay'); - this.lightbox = document.getElementById('lightbox'); - this.outerContainer = this.lightbox.querySelector('.lb-outerContainer'); - this.container = this.lightbox.querySelector('.lb-container'); - this.image = this.lightbox.querySelector('.lb-image'); - this.nav = this.lightbox.querySelector('.lb-nav'); - this.loader = this.lightbox.querySelector('.lb-loader'); - this.caption = this.lightbox.querySelector('.lb-caption'); - this.closeButton = this.lightbox.querySelector('.lb-close'); - this.dataContainer = this.lightbox.querySelector('.lb-dataContainer'); - this.prev = this.lightbox.querySelector('.lb-prev'); - this.next = this.lightbox.querySelector('.lb-next'); - this.video = this.lightbox.querySelector('.lb-video'); - - this.setVisible(this.overlay, false); - this.setVisible(this.lightbox, false); - - this.containerPadding = this.getBoxMetrics(this.container, 'padding'); - this.imageBorderWidth = this.getBoxMetrics(this.image, 'border'); - this.videoBorderWidth = this.getBoxMetrics(this.video, 'border'); - - this.overlay.addEventListener('click', () => this.end()); - this.dataContainer.addEventListener('click', () => this.end()); - this.lightbox.addEventListener('click', (event) => { - if(event.target === this.lightbox) this.end(); - }); - this.outerContainer.addEventListener('click', (event) => { - if(event.target === this.outerContainer) this.end(); - event.stopPropagation(); - }); - - this.prev.addEventListener('click', (event) => { - event.preventDefault(); - if(this.currentImageIndex === 0) this.changeImage(this.album.length - 1); - else this.changeImage(this.currentImageIndex - 1); - }); - this.next.addEventListener('click', (event) => { - event.preventDefault(); - if(this.currentImageIndex === this.album.length - 1) this.changeImage(0); - else this.changeImage(this.currentImageIndex + 1); - }); - - this.loader.addEventListener('click', (event) => { - event.preventDefault(); - this.end(); - }); - this.closeButton.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - this.end(); - }); - this.closeButton.addEventListener('keyup', (event) => { - if(event.key === 'Enter' || event.key === ' ') this.end(); - }); - - this.nav.addEventListener('wheel', this.boundOnWheel, { passive: false }); - this.nav.addEventListener('mousedown', this.boundOnDragStart); - window.addEventListener('mouseup', this.boundOnDragEnd); - } - - getBoxMetrics(element, type) { - const styles = getComputedStyle(element); - return { - top: parseInt(styles[`${type}-top-width`], 10) || 0, - right: parseInt(styles[`${type}-right-width`], 10) || 0, - bottom: parseInt(styles[`${type}-bottom-width`], 10) || 0, - left: parseInt(styles[`${type}-left-width`], 10) || 0 - }; - } - - start(link) { - this.sizeOverlay(); - this.album = []; - let imageNumber = 0; - const setName = link.getAttribute('data-lightbox'); - - const links = [...document.querySelectorAll(`${link.tagName}[data-lightbox="${CSS.escape(setName)}"]`)]; - links.forEach((item, index) => { - this.addToAlbum(item); - if(item === link) imageNumber = index; - }); - - this.fade(this.overlay, true, this.options.fadeDuration); - this.fade(this.lightbox, true, this.options.fadeDuration); - - if(this.options.disableScrolling) document.body.classList.add('lb-disable-scrolling'); - - window.addEventListener('resize', this.boundOnResize); - this.changeImage(imageNumber); - } - - addToAlbum(link) { - const img = link.querySelector('img'); - this.album.push({ - alt: link.getAttribute('data-alt') || '', - link: link.getAttribute('href'), - title: link.getAttribute('data-title') || link.getAttribute('title') || '', - orientation: parseInt(link.getAttribute('data-orientation') || '0', 10), - type: link.getAttribute('data-type') || 'image', - id: link.getAttribute('data-id'), - width: parseInt(img?.getAttribute('width') || '0', 10), - height: parseInt(img?.getAttribute('height') || '0', 10), - set: link.getAttribute('data-lightbox') || '' - }); - } - - hasMediaAfterCurrent() { - return this.currentImageIndex < this.album.length - 1; - } - - refreshAlbum() { - const current = this.album[this.currentImageIndex]; - if(!current?.set) return; - - const links = [...document.querySelectorAll(`a[data-lightbox="${CSS.escape(current.set)}"], area[data-lightbox="${CSS.escape(current.set)}"]`)]; - if(!links.length) return; - - const existingKeys = new Set(this.album.map((media) => this.getMediaKey(media))); - links.forEach((link) => { - const key = this.getLinkMediaKey(link); - if(existingKeys.has(key)) return; - - this.addToAlbum(link); - existingKeys.add(key); - }); - - this.updateNav(); - } - - getMediaKey(media) { - return `${media.set}:${media.id}`; - } - - getLinkMediaKey(link) { - return `${link.getAttribute('data-lightbox') || ''}:${link.getAttribute('data-id')}`; - } - - getMaxSizes(mediaType) { - let maxWidth = window.innerWidth - this.containerPadding.left - this.containerPadding.right; - let maxHeight = window.innerHeight - this.containerPadding.top - this.containerPadding.bottom - this.options.positionFromTop; - const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth; - maxWidth -= border.left + border.right; - maxHeight -= border.top + border.bottom; - maxHeight -= this.getDataContainerHeight(maxWidth + this.containerPadding.left + this.containerPadding.right + border.left + border.right); - - return { - maxWidth: Math.max(maxWidth, 1), - maxHeight: Math.max(maxHeight, 1) - }; - } - - getDataContainerHeight(width = null) { - if(!this.dataContainer) return 0; - - const currentWidth = this.dataContainer.style.width; - if(width !== null) this.dataContainer.style.width = `${width}px`; - const height = Math.ceil(this.dataContainer.getBoundingClientRect().height || this.dataContainer.offsetHeight || 0); - this.dataContainer.style.width = currentWidth; - - return height; - } - - getMediaSize(media, maxWidth, maxHeight) { - if(media.width <= maxWidth && media.height <= maxHeight) { - return { - width: media.width, - height: media.height - }; - } - - const widthRatio = media.width / maxWidth; - const heightRatio = media.height / maxHeight; - - if(widthRatio > heightRatio) { - return { - width: maxWidth, - height: Math.round(media.height / widthRatio) - }; - } - - return { - width: Math.round(media.width / heightRatio), - height: maxHeight - }; - } - - fitSizeWithDataContainer(size, mediaType) { - const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth; - const maxOuterHeight = Math.max(window.innerHeight - this.options.positionFromTop, 1); - let fittedSize = size; - - for(let i = 0; i < 5; i++) { - const containerWidth = fittedSize.width + this.containerPadding.left + this.containerPadding.right + border.left + border.right; - const containerHeight = fittedSize.height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom; - const dataHeight = this.getDataContainerHeight(containerWidth); - const overflow = Math.ceil(containerHeight + dataHeight - maxOuterHeight); - if(overflow <= 0 || fittedSize.height <= 1) break; - - const height = Math.max(fittedSize.height - overflow, 1); - fittedSize = { - width: Math.max(Math.round(fittedSize.width * (height / fittedSize.height)), 1), - height - }; - } - - return fittedSize; - } - - updateSize(index) { - const media = this.album[index]; - const maxSizes = this.getMaxSizes(media.type); - const maxWidth = this.options.maxWidth ? Math.min(this.options.maxWidth, maxSizes.maxWidth) : maxSizes.maxWidth; - const maxHeight = this.options.maxHeight ? Math.min(this.options.maxHeight, maxSizes.maxHeight) : maxSizes.maxHeight; - const size = this.fitSizeWithDataContainer(this.getMediaSize(media, maxWidth, maxHeight), media.type); - - const target = media.type === 'video' ? this.video : this.image; - target.width = size.width; - target.height = size.height; - this.sizeContainer(size.width, size.height, media.type); - } - - changeImage(index) { - const media = this.album[index]; - if(!media) return; - - this.updateDetails(media, false); - this.hideElements([this.dataContainer]); - this.disableKeyboardNav(); - this.fade(this.overlay, true, this.options.fadeDuration); - this.fade(this.loader, true, 200); - this.hideElements([this.image, this.video, this.nav, this.prev, this.next]); - this.resetImageTransform(); - this.outerContainer.classList.add('animating'); - this.container.classList.remove('moveable', 'moving', 'lb-video-nav'); - this.currentImageIndex = index; - - this.options.onMediaChange(media); - - if(media.type === 'video') { - this.image.removeAttribute('src'); - this.container.classList.add('lb-video-nav'); - this.video.onloadedmetadata = () => { - media.width = this.video.videoWidth; - media.height = this.video.videoHeight; - this.video.onloadedmetadata = null; - this.updateSize(index); - }; - this.video.src = media.link; - } else { - this.video.pause(); - this.video.removeAttribute('src'); - this.image.onload = () => { - this.image.alt = media.alt; - let width = this.image.naturalWidth; - let height = this.image.naturalHeight; - if(Math.abs(media.orientation) === 90 && width > height) { - const tmp = width; - width = height; - height = tmp; - } - media.width = width; - media.height = height; - this.image.onload = null; - this.updateSize(index); - }; - this.image.src = media.link; - } - } - - sizeOverlay() { - if(this.resizeTimer) clearTimeout(this.resizeTimer); - if(!this.album.length) return; - - this.resizeTimer = window.setTimeout(() => { - const current = this.album[this.currentImageIndex]; - if(!current) return; - if(current.type === 'image') this.changeImage(this.currentImageIndex); - else this.updateSize(this.currentImageIndex); - }, 200); - } - - sizeContainer(width, height, mediaType = 'image') { - const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth; - const newWidth = width + this.containerPadding.left + this.containerPadding.right + border.left + border.right; - const newHeight = height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom; - const dataHeight = this.getDataContainerHeight(newWidth); - - this.outerContainer.style.transition = `width ${this.options.resizeDuration}ms, height ${this.options.resizeDuration}ms`; - this.outerContainer.style.width = `${newWidth}px`; - this.outerContainer.style.height = `${newHeight + dataHeight}px`; - this.container.style.height = `${newHeight}px`; - - window.setTimeout(() => { - this.overlay.focus(); - this.showImage(); - this.outerContainer.style.transition = ''; - }, this.options.resizeDuration); - } - - showImage() { - this.fade(this.loader, false, 0); - if(this.options.hasVideo && this.album[this.currentImageIndex].type === 'video') this.fade(this.video, true, this.options.imageFadeDuration); - else this.fade(this.image, true, this.options.imageFadeDuration); - - this.updateNav(); - this.updateDetails(); - this.preloadNeighboringImages(); - this.enableKeyboardNav(); - } - - updateNav() { - this.setVisible(this.nav, true); - this.setVisible(this.prev, false); - this.setVisible(this.next, false); - - const alwaysShowNav = ('ontouchstart' in window) && this.options.alwaysShowNavOnTouchDevices; - if(this.album.length <= 1) return; - - if(this.options.wrapAround) { - this.setVisible(this.prev, true); - this.setVisible(this.next, true); - } else { - if(this.currentImageIndex > 0) this.setVisible(this.prev, true); - if(this.currentImageIndex < this.album.length - 1) this.setVisible(this.next, true); - } - - if(alwaysShowNav) { - this.prev.style.opacity = '1'; - this.next.style.opacity = '1'; - } else { - this.prev.style.opacity = ''; - this.next.style.opacity = ''; - } - } - - updateDetails(media = this.album[this.currentImageIndex], show = true) { - if(!media) return; - - if(media.title) { - if(this.options.sanitizeTitle) this.caption.textContent = media.title; - else this.caption.innerHTML = media.title; - if(show) this.fade(this.caption, true, 200); - else this.setVisible(this.caption, true); - } else { - this.caption.textContent = ''; - this.setVisible(this.caption, false); - } - - if(show) { - this.fade(this.closeButton, true, 200); - this.outerContainer.classList.remove('animating'); - this.fade(this.dataContainer, true, this.options.resizeDuration); - } else { - this.setVisible(this.closeButton, true); - this.setVisible(this.dataContainer, false); - this.dataContainer.style.transition = ''; - this.dataContainer.style.opacity = '0'; - } - } - - preloadNeighboringImages() { - const next = this.album[this.currentImageIndex + 1]; - const prev = this.album[this.currentImageIndex - 1]; - if(next && next.type === 'image') { - const preloadNext = new Image(); - preloadNext.src = next.link; - } - if(prev && prev.type === 'image') { - const preloadPrev = new Image(); - preloadPrev.src = prev.link; - } - } - - enableKeyboardNav() { - this.disableKeyboardNav(); - this.lightbox.addEventListener('keyup', this.boundOnKeyUp); - this.overlay.addEventListener('keyup', this.boundOnKeyUp); - } - - disableKeyboardNav() { - this.lightbox?.removeEventListener('keyup', this.boundOnKeyUp); - this.overlay?.removeEventListener('keyup', this.boundOnKeyUp); - } - - keyboardAction(event) { - switch(event.key) { - case 'Escape': - event.stopPropagation(); - this.end(); - break; - case 'ArrowLeft': - if(this.currentImageIndex !== 0) this.changeImage(this.currentImageIndex - 1); - else if(this.options.wrapAround && this.album.length > 1) this.changeImage(this.album.length - 1); - break; - case 'ArrowRight': - if(this.currentImageIndex !== this.album.length - 1) this.changeImage(this.currentImageIndex + 1); - else if(this.options.wrapAround && this.album.length > 1) this.changeImage(0); - break; - } - } - - onWheel(event) { - const media = this.album[this.currentImageIndex]; - if(!media || media.type === 'video') return; - event.preventDefault(); - - const rect = this.image.getBoundingClientRect(); - const oldTransform = this.getImageTransform(); - const oldZoom = oldTransform.scale; - const maxZoom = Math.max(media.width / Math.max(this.image.width, 1), media.height / Math.max(this.image.height, 1), 1); - const newZoom = Math.min(Math.max(oldZoom + (-Math.sign(event.deltaY) / 10), 1), maxZoom); - - const imageCenterX = rect.left + rect.width / 2 - oldTransform.translateX; - const imageCenterY = rect.top + rect.height / 2 - oldTransform.translateY; - const cursorX = event.clientX - imageCenterX; - const cursorY = event.clientY - imageCenterY; - const zoomRatio = newZoom / oldZoom; - const transform = this.clampImageTransform({ - scale: newZoom, - translateX: cursorX - zoomRatio * (cursorX - oldTransform.translateX), - translateY: cursorY - zoomRatio * (cursorY - oldTransform.translateY) - }); - - this.container.classList.toggle('moveable', newZoom > 1); - this.setImageTransform(transform); - } - - onDragStart(event) { - const scale = parseFloat(this.image.style.getPropertyValue('--scale') || '1'); - if(scale <= 1) return; - - this.gMouseDownOffsetX = event.clientX - parseFloat(this.image.style.getPropertyValue('--translate-x') || '0'); - this.gMouseDownOffsetY = event.clientY - parseFloat(this.image.style.getPropertyValue('--translate-y') || '0'); - this.container.classList.add('moving'); - window.addEventListener('mousemove', this.boundOnDragMove); - } - - onDragMove(event) { - const zoom = parseFloat(this.image.style.getPropertyValue('--scale') || '1'); - const transform = this.clampImageTransform({ - scale: zoom, - translateX: event.clientX - this.gMouseDownOffsetX, - translateY: event.clientY - this.gMouseDownOffsetY - }); - - this.setImageTransform(transform); - } - - onDragEnd() { - window.removeEventListener('mousemove', this.boundOnDragMove); - this.container?.classList.remove('moving'); - } - - resetImageTransform() { - this.setImageTransform({scale: 1, translateX: 0, translateY: 0}); - } - - getImageTransform() { - return { - scale: parseFloat(this.image.style.getPropertyValue('--scale') || '1'), - translateX: parseFloat(this.image.style.getPropertyValue('--translate-x') || '0'), - translateY: parseFloat(this.image.style.getPropertyValue('--translate-y') || '0') - }; - } - - clampImageTransform(transform) { - const maxTranslateX = (transform.scale - 1) * this.image.width / 2; - const maxTranslateY = (transform.scale - 1) * this.image.height / 2; - - return { - scale: transform.scale, - translateX: Math.max(Math.min(transform.translateX, maxTranslateX), -maxTranslateX), - translateY: Math.max(Math.min(transform.translateY, maxTranslateY), -maxTranslateY) - }; - } - - setImageTransform(transform) { - if(!this.image) return; - this.image.style.setProperty('--scale', String(transform.scale)); - this.image.style.setProperty('--translate-x', `${transform.translateX}px`); - this.image.style.setProperty('--translate-y', `${transform.translateY}px`); - } - - hideElements(elements) { - elements.forEach((element) => { - this.setVisible(element, false); - }); - } - - setVisible(element, visible) { - if(!element) return; - element.style.visibility = visible ? 'visible' : 'hidden'; - element.style.pointerEvents = visible ? '' : 'none'; - } - - fade(element, show, duration, done) { - if(!element) return; - - const safeDuration = duration || 0; - element.style.transition = `opacity ${safeDuration}ms`; - if(show) { - this.setVisible(element, true); - requestAnimationFrame(() => { - element.style.opacity = element === this.overlay ? '0.8' : '1'; - }); - } else { - element.style.opacity = '0'; - element.style.pointerEvents = 'none'; - window.setTimeout(() => { - this.setVisible(element, false); - }, safeDuration); - } - - if(typeof done === 'function') { - window.setTimeout(done, safeDuration); - } - } - - end(dispose = false) { - this.disableKeyboardNav(); - this.video?.pause(); - this.video?.removeAttribute('src'); - this.container?.classList.remove('lb-video-nav', 'moveable', 'moving'); - window.removeEventListener('resize', this.boundOnResize); - window.removeEventListener('mousemove', this.boundOnDragMove); - - if(dispose) { - this.disable(); - if(this.resizeTimer) clearTimeout(this.resizeTimer); - window.removeEventListener('mouseup', this.boundOnDragEnd); - this.lightbox?.remove(); - this.overlay?.remove(); - this.album = []; - } - else { - this.fade(this.lightbox, false, this.options.fadeDuration); - this.fade(this.overlay, false, this.options.fadeDuration); - this.options.onClosing(); - } - - if(this.options.disableScrolling) document.body.classList.remove('lb-disable-scrolling'); - } -}