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