Files
trpic/web/scripts/img-viewer.js
Hubert Chen 64918ef388 add noscript mode
auto set default status for sidebar
disable gestures and double-tap zoom
2026-01-16 16:53:24 +08:00

346 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Modified from https://github.com/mrhuo/image-viewer
class ImageViewer {
constructor(options = {}) {
// State variables
this.targetSelector = "#gallery img";
this.currentScale = 1.0;
this.maxScale = 8;
this.minScale = 0.5;
this.isDragging = false;
this.hasMoved = false;
this.downX = 0;
this.downY = 0;
this.startX = 0;
this.startY = 0;
this.currentX = 0;
this.currentY = 0;
this.imageList = [];
this.currentIndex = -1;
window.scrollTo(0, 0);
this.preview = document.getElementById("preview");
this.sidebar = document.getElementById("sidebar");
this.imageWrapper = document.getElementById("image-wrapper");
this.previewImg = document.getElementById("preview-img");
this.navControls = document.getElementById("nav-controls");
this.prevBtn = document.getElementById("prev-btn");
this.nextBtn = document.getElementById("next-btn");
this.imageCaption = document.getElementById("image-caption");
this.imageIndex = document.getElementById("image-index");
this.imageInfo = document.getElementById("image-info");
this.downloadBtn = document.getElementById("download-btn");
this.toggleSidebarBtn = document.getElementById("toggle-sidebar-btn");
this.sidebarContent = document.getElementById("sidebar-content");
this.sidebarImageName = document.getElementById("img-name");
this.sidebarImageAlt = document.getElementById("img-alt");
if (window.innerWidth > 1600) {
this.sidebar.style.width = "30vw";
}
this.imageList = Array.from(document.querySelectorAll(this.targetSelector));
this.observeImages();
this.bindClickEvents(this.imageList);
this.imageWrapper.addEventListener("wheel", this.handleWheel.bind(this));
this.imageWrapper.addEventListener("mousedown", this.startDrag.bind(this));
this.prevBtn.addEventListener("click", this.showPrevImage.bind(this));
this.nextBtn.addEventListener("click", this.showNextImage.bind(this));
document.addEventListener("keydown", this.handleKeyDown.bind(this));
this.downloadBtn.addEventListener("click", this.downloadImage.bind(this));
this.toggleSidebarBtn.addEventListener("click", this.toggleSidebar.bind(this));
}
downloadImage() {
const link = document.createElement("a");
link.download = this.previewImg.name;
link.href = this.previewImg.src;
link.click();
link.remove();
}
toggleSidebar() {
if (this.sidebar.style.width) {
this.sidebar.style.width = "";
setTimeout(() => {
this.sidebarContent.style.display = "none";
}, 100);
} else {
this.sidebar.style.width = "30vw";
setTimeout(() => {
this.sidebarContent.style.display = "block";
}, 100);
}
}
bindDragEvents() {
this.imageWrapper.addEventListener("mousemove", this.doDrag.bind(this));
this.imageWrapper.addEventListener("mouseup", this.endDrag.bind(this));
this.imageWrapper.addEventListener("mouseleave", this.endDrag.bind(this));
}
unbindDragEvents() {
this.imageWrapper.removeEventListener("mousemove", this.doDrag.bind(this));
this.imageWrapper.removeEventListener("mouseup", this.endDrag.bind(this));
this.imageWrapper.removeEventListener("mouseleave",this.endDrag.bind(this));
}
resetTransform() {
this.currentScale = 1.0;
this.currentX = 0;
this.currentY = 0;
this.updateImageTransform();
}
updateImageTransform() {
this.previewImg.style.transform = `
translate(${this.currentX}px, ${this.currentY}px)
scale(${this.currentScale})
`;
}
showImageByIndex(index) {
if (index < 0 || index >= this.imageList.length) {
return;
}
if (index === this.imageList.length - 7) {
loadMoreImages();
}
this.currentIndex = index;
const img = this.imageList[this.currentIndex];
this.resetTransform();
const highResSrc = img.dataset.highres;
const lowResSrc = img.src;
const altText = img.alt || "";
// 先显示小图
this.previewImg.src = lowResSrc;
this.previewImg.alt = altText;
this.updateInfoBar(altText);
this.updateNavButtons();
this.previewImg.onload = () => {
// 如果有高分辨率图片,异步加载并在完成后替换
if (highResSrc && highResSrc !== lowResSrc) {
this.loadHighResImage(altText, highResSrc);
}
};
this.previewImg.onerror = () => {
this.updateInfoBar("Failed to load image");
};
this.preview.style.display = "flex";
document.body.style.overflow = "hidden";
}
loadHighResImage(altText, highResSrc) {
this.imageIndex.textContent += " loading...";
const loadingIndex = this.currentIndex;
const highResImg = new Image();
highResImg.onload = () => {
// Remove current onload handler to avoid duplicate triggers
this.previewImg.onload = null;
// Check if the image index has changed
if (this.currentIndex != loadingIndex) return;
// High-resolution image loaded, replace current image
this.previewImg.src = highResSrc;
this.updateInfoBar(altText);
};
highResImg.onerror = () => {
if (this.currentIndex != loadingIndex) return;
this.updateInfoBar("Failed to load high res image");
};
// Start loading high-resolution image
highResImg.src = highResSrc;
}
updateInfoBar(altText) {
this.imageCaption.textContent = altText;
this.imageIndex.textContent = `${this.currentIndex + 1} / ${
this.imageList.length
}`;
this.sidebarImageName.textContent = this.imageList[this.currentIndex].name;
this.sidebarImageAlt.textContent = this.imageList[this.currentIndex].alt;
}
updateNavButtons() {
this.prevBtn.style.opacity = this.currentIndex <= 0 ? "0" : "1";
this.prevBtn.style.pointerEvents = this.currentIndex <= 0 ? "none" : "auto";
this.nextBtn.style.opacity =
this.currentIndex >= this.imageList.length - 1 ? "0" : "1";
this.nextBtn.style.pointerEvents =
this.currentIndex >= this.imageList.length - 1 ? "none" : "auto";
}
openPreview(e, img) {
e.stopPropagation();
const index = this.imageList.indexOf(img);
if (index !== -1) {
this.showImageByIndex(index);
}
}
closePreview() {
this.preview.style.display = "none";
document.body.style.overflow = "";
}
showNextImage() {
if (this.currentIndex < this.imageList.length - 1) {
this.showImageByIndex(this.currentIndex + 1);
}
}
showPrevImage() {
if (this.currentIndex > 0) {
this.showImageByIndex(this.currentIndex - 1);
}
}
handleKeyDown(e) {
if (this.preview.style.display !== "flex") return;
switch (e.key) {
case "Escape":
this.closePreview();
break;
case "ArrowRight":
this.showNextImage();
break;
case "ArrowLeft":
this.showPrevImage();
break;
case "b":
if (e.ctrlKey) {
this.toggleSidebar();
}
break;
}
}
handleWheel(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = this.currentScale + delta;
if (newScale >= this.minScale && newScale <= this.maxScale) {
const ratio = newScale / this.currentScale;
const rect = this.imageWrapper.getBoundingClientRect();
const offsetX = e.clientX - (rect.left + rect.width / 2);
const offsetY = e.clientY - (rect.top + rect.height / 2);
this.currentX = offsetX * (1 - ratio) + this.currentX * ratio;
this.currentY = offsetY * (1 - ratio) + this.currentY * ratio;
this.currentScale = newScale;
this.updateImageTransform();
}
}
startDrag(e) {
if (e.button !== 0) return; // Only left button
e.preventDefault();
e.stopPropagation();
this.isDragging = true;
this.hasMoved = false;
this.startX = e.clientX - this.currentX;
this.startY = e.clientY - this.currentY;
this.downX = e.clientX;
this.downY = e.clientY;
this.imageWrapper.classList.add("dragging");
this.navControls.style.display = "none";
this.bindDragEvents();
}
doDrag(e) {
if (!this.isDragging) return;
e.preventDefault();
e.stopPropagation();
const dx = e.clientX - this.downX;
const dy = e.clientY - this.downY;
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
this.hasMoved = true;
}
this.currentX = e.clientX - this.startX;
this.currentY = e.clientY - this.startY;
this.updateImageTransform();
}
endDrag(e) {
if (!this.isDragging) return;
if (e) {
e.stopPropagation();
}
this.isDragging = false;
this.imageWrapper.classList.remove("dragging");
this.unbindDragEvents();
setTimeout(() => {
if (!this.isDragging) {
this.navControls.style.display = "flex";
}
}, 500)
if (!this.hasMoved) {
this.closePreview();
}
}
observeImages() {
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
let imgsToProcess = [];
if (node.tagName === "IMG" && node.matches(this.targetSelector)) {
imgsToProcess.push(node);
} else if (node.querySelectorAll) {
imgsToProcess = Array.from(
node.querySelectorAll(this.targetSelector)
);
}
imgsToProcess.forEach((img) => {
if (!this.imageList.includes(img)) {
this.imageList.push(img);
this.bindClickEvents([img]);
}
});
});
if (mutation.removedNodes.length > 0) {
this.imageList = this.imageList.filter((img) =>
document.body.contains(img)
);
}
});
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
bindClickEvents(images) {
images.forEach((img) => {
if (!img.dataset.imageViewerBound) {
img.addEventListener("click", (e) => this.openPreview(e, img));
img.dataset.imageViewerBound = "true";
img.style.cursor = "zoom-in";
}
});
}
}
document.addEventListener('DOMContentLoaded', () => { new ImageViewer({}) });