Files
JaredTherriault-ComfyUI-JNodes/web/js/common/ModalManager.js
T

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();
}
}
}