// Manages fullscreen modal display for images and videos with zoom, pan, slideshow, and navigation controls. // Handles mouse/touch/keyboard interaction for modal lifecycle and neighboring image switching. import { utilitiesInstance } from "./Utilities.js"; import { $el } from "/scripts/ui.js"; import { toggleVideoPlayback } from "./VideoControl.js"; import { setting_VideoPlaybackOptions } from "./SettingsManager.js"; export class ModalOptions { constructor( bIsImageContainer = false, imageIndex = undefined ) { this.bIsImageContainer = bIsImageContainer; this.imageIndex = imageIndex; } } export class ModalManager { constructor(imageDrawerInstance, modalOptions = new ModalOptions()) { this.imageDrawerInstance = imageDrawerInstance; // If this modal is representing an image in imageList, // add functionality to switch to neighboring images or // enable auto mode / slideshows, among other things this._modalOptions = modalOptions; // Zoom and Pan functionality this._scale = 1; this._translateX = -50; // Initial translate X percentage this._translateY = -50; // Initial translate Y percentage this._bIsPanningActive = false; this._bHasPanned = false; this._startX = 0; this._startY = 0; this._modalContainer; this._modalContent; this.handleKeyDownFunction; this._bButtonPressed = false; } createModal(modalContent) { if (!modalContent) { return; } this._modalContent = modalContent; this._setModalContentStyle(); // Append modal content to modal container this._getOrCreateModalContainer().appendChild(this._modalContent); this._createModalControlButtons(); this.handleKeyDownFunction = (event) => { this._handleKeyDown(event); }; // Add key event listeners document.addEventListener("keydown", this.handleKeyDownFunction, { capture: true }); return this._getOrCreateModalContainer(); } createModalReadyImage(href) { return $el("img", { src: href, style: { position: 'relative', width: '99vw', height: '99vh', objectFit: 'contain', display: 'block', margin: 'auto', }, }); } createModalReadyVideo(href) { const video = $el("video", { src: href, style: { position: 'relative', width: '99vw', height: '99vh', objectFit: 'contain', display: 'block', margin: 'auto', }, }); video.controls = setting_VideoPlaybackOptions.value.controls; video.autoplay = setting_VideoPlaybackOptions.value.autoplay; video.loop = setting_VideoPlaybackOptions.value.loop; video.muted = setting_VideoPlaybackOptions.value.muted; video.volume = setting_VideoPlaybackOptions.value.defaultVolume / 100; video.playbackRate = setting_VideoPlaybackOptions.value.defaultPlaybackRate; video.addEventListener("click", (event) => { if (this._bHasPanned) { return; } event.stopPropagation(); toggleVideoPlayback(video); }); return video; } _setModalContentStyle() { this._modalContent.style.position = 'absolute'; this._modalContent.style.left = "50%"; this._modalContent.style.top = "50%"; this._modalContent.style.transform = "translate(-50%, -50%)"; this._modalContent.style.maxWidth = "99%"; this._modalContent.style.maxHeight = "99%"; this._modalContent.style.overflow = "hidden"; // Prevent drag & drop operation this._modalContent.draggable = false; } _createAndOpenModalContainer() { this._modalContainer = $el("div", { style: { display: "block", position: "fixed", zIndex: "10000", left: "0%", top: "0%", width: "100%", height: "100%", maxWidth: "100%", maxHeight: "100%", overflow: "auto", backgroundColor: "rgba(0,0,0,0.7)", } } ); this._modalContainer.addEventListener("wheel", (event) => { if (this._modalOptions.bIsImageContainer) { const bIsOverVideo = event.target.tagName === "VIDEO"; const bVideoWheelSeek = bIsOverVideo && setting_VideoPlaybackOptions.value.useWheelSeek; if (bVideoWheelSeek) { return; // Let the global EventManager handle seeking } event.preventDefault(); event.stopPropagation(); this._zoom(event); } }); this._modalContainer.addEventListener("pointerdown", (event) => { if (this._modalOptions.bIsImageContainer) { event.preventDefault(); event.stopPropagation(); this._startPan(event); } }); this._modalContainer.addEventListener("pointermove", (event) => { if (this._modalOptions.bIsImageContainer) { event.preventDefault(); event.stopPropagation(); this._pan(event); } }); this._modalContainer.addEventListener("pointerup", (event) => { if (this._modalOptions.bIsImageContainer) { event.preventDefault(); event.stopPropagation(); } this._onMouseUp(event); }); this._modalContainer.addEventListener("pointerleave", (event) => { if (this._modalOptions.bIsImageContainer) { event.preventDefault(); event.stopPropagation(); this._endPan(); } }); // Prevent drag & drop operation this._modalContainer.addEventListener("dragstart", (event) => { if (this._modalOptions.bIsImageContainer) { event.preventDefault(); event.stopPropagation(); } }); this._modalContainer.draggable = false; this._modalContainer.data = this; // Append modal container to the document body document.body.appendChild(this._modalContainer); } _getOrCreateModalContainer() { if (!this._modalContainer) { this._createAndOpenModalContainer(); } return this._modalContainer; } _createModalControlButtons() { const createButton = (text, tooltip, clickFunction) => { const button = $el("button", { title: tooltip, textContent: text, style: { background: "none", border: "none", padding: "0px", color: "white", fontWeight: "bolder", fontSize: "400%", cursor: 'pointer', position: "absolute", filter: "drop-shadow(13px 0px black)", }, onmousedown: () => { this._bButtonPressed = true; }, onclick: clickFunction, }); button.classList.add("JNodes-interactive-container"); return button; } // Count if (this._modalOptions.imageIndex != undefined) { const previousImageButton = createButton("<", "See previous item", () => { this._displayNeighbouringImage(-1); }); previousImageButton.style.left = "2.5%"; previousImageButton.style.top = "50%"; this._getOrCreateModalContainer().appendChild(previousImageButton); const nextImageButton = createButton(">", "See next item", () => { this._displayNeighbouringImage(1); }); nextImageButton.style.right = "2.5%"; nextImageButton.style.top = "50%"; this._getOrCreateModalContainer().appendChild(nextImageButton); const imageDrawerListInstance = this.imageDrawerInstance.getComponentByName("ImageDrawerList"); const currentListChildren = imageDrawerListInstance.getVisibleImageListChildren(); const countWidget = $el("label", { textContent: `${this._modalOptions.imageIndex + 1}/${currentListChildren.length}`, style: { background: "rgba(0,0,0,0.5)", color: "white", fontWeight: "bolder", fontSize: "100%", fontFamily: "arial", position: "absolute", right: "1%", top: "1%" } }); this._getOrCreateModalContainer().appendChild(countWidget); } } // Function to close the modal and destroy the class instance closeModal() { if (this._modalContainer && this._modalContainer.parentNode) { this._modalContainer.parentNode.removeChild(this._modalContainer); } document.removeEventListener("keydown", this.handleKeyDownFunction, { capture: true }); delete this; } _handleKeyDown(event) { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); this.closeModal(); } else if (event.key === "ArrowLeft") { if (this._modalOptions.bIsImageContainer){ event.preventDefault(); event.stopPropagation(); this._displayNeighbouringImage(-1); } } else if (event.key === "ArrowRight") { if (this._modalOptions.bIsImageContainer){ event.preventDefault(); event.stopPropagation(); this._displayNeighbouringImage(1); } } else if (event.key === " ") { if (this._modalContent && this._modalContent.tagName === "VIDEO") { event.preventDefault(); event.stopPropagation(); toggleVideoPlayback(this._modalContent); } } } _zoom(event) { if (!this._modalOptions.bIsImageContainer) { return; } event.preventDefault(); // Prevent the default scroll behavior event.stopPropagation(); const zoomSpeed = 0.1; const delta = event.deltaY; // Adjust the scale based on scroll direction if (delta > 0) { this._scale = Math.max(0.1, this._scale - zoomSpeed); // Zoom out } else { this._scale = Math.min(5, this._scale + zoomSpeed); // Zoom in } // Apply the scale and initial translate to the modal content this._modalContent.style.transform = `translate(${this._translateX}%, ${this._translateY}%) scale(${this._scale})`; } _startPan(event) { if (!this._modalOptions.bIsImageContainer) { return; } this._bIsPanningActive = true; this._getOrCreateModalContainer().style.cursor = "grabbing"; this._startX = event.clientX; this._startY = event.clientY; } _pan(event) { if (!this._modalOptions.bIsImageContainer) { return; } if (!this._bIsPanningActive) return; const dx = event.clientX - this._startX; const dy = event.clientY - this._startY; // Adjust translate values this._translateX += (dx / this._getOrCreateModalContainer().clientWidth) * 100; this._translateY += (dy / this._getOrCreateModalContainer().clientHeight) * 100; // Update start positions for next movement this._startX = event.clientX; this._startY = event.clientY; // Apply the transformation this._modalContent.style.transform = `translate(${this._translateX}%, ${this._translateY}%) scale(${this._scale})`; this._bHasPanned = true; } _endPan() { if (!this._modalOptions.bIsImageContainer) { return; } this._bIsPanningActive = false; this._getOrCreateModalContainer().style.cursor = "grab"; } _onMouseUp(event) { this._endPan(); if (this._bButtonPressed == true) { this._bButtonPressed = false; return; } if (this._bHasPanned) { this._bHasPanned = false; } else if (event.button == 0) { // Left mouse button only can close if (event.target.tagName === "VIDEO") { return; // Video's own click handler manages play/pause } if (!this._modalOptions.bIsImageContainer && (event.target == this._modalContent || utilitiesInstance.hasAncestor(event.target, this._modalContent)) ) { return; } this.closeModal(); } } _displayNeighbouringImage(offset = 0) { if (this._modalOptions.imageIndex !== undefined) { const imageDrawerListInstance = this.imageDrawerInstance.getComponentByName("ImageDrawerList"); const currentListChildren = imageDrawerListInstance.getVisibleImageListChildren(); let newImageIndex = this._modalOptions.imageIndex; newImageIndex += offset; // Wrap index to first or last index at the extremes newImageIndex = ((newImageIndex % currentListChildren.length) + currentListChildren.length) % currentListChildren.length; const newImage = currentListChildren[newImageIndex]; const modalManager = new ModalManager( this.imageDrawerInstance, new ModalOptions( this._modalOptions.bIsImageContainer, newImageIndex ) ); const modalContent = newImage.bIsVideoFormat ? modalManager.createModalReadyVideo(newImage.fileInfo.imageHref) : modalManager.createModalReadyImage(newImage.fileInfo.imageHref); modalManager.createModal(modalContent); this.closeModal(); } } }