346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
// 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({}) });
|