433 lines
12 KiB
JavaScript
433 lines
12 KiB
JavaScript
// 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();
|
|
}
|
|
}
|
|
} |