import PhotoSwipeLightbox from 'https://dpangzi.com/library/photoswipe/photoswipe.esm.min.js';
import PhotoSwipe from 'https://dpangzi.com/library/photoswipe/photoswipe.esm.min.js';
export class Gallery {
/**
* @param {NodeListOf<Element>|HTMLImageElement[]} images
* @param {Object} [options]
* @param {boolean} [options.preloadAll=false]
* @param {number} [options.preloadNeighbors=1]
* @param {string|number} [options.galleryId=1]
*/
constructor(images, options = {}) {
this.images = Array.from(images);
this.items = [];
this.options = Object.assign({
preloadAll: false,
preloadNeighbors: 1,
galleryId: 1
}, options);
this.lightbox = null;
this._isClosedByNavigation = false;
this._isClosing = false;
this._clickHandlers = new Map(); // 存储事件处理器,用于清理
this.init();
this.checkHash();
window.addEventListener('hashchange', () => this.handleHashChange());
}
init() {
// 清理旧的事件监听器(如果存在)
this.cleanup();
this.images.forEach((img, index) => {
// Prevent duplicate binding
if (img.dataset.pswpBound) {
// 如果已经绑定过,先移除旧的标记和事件监听器
delete img.dataset.pswpBound;
// 移除可能存在的旧事件监听器
const oldHandler = this._clickHandlers.get(img);
if (oldHandler) {
img.removeEventListener('click', oldHandler);
this._clickHandlers.delete(img);
}
}
img.dataset.pswpBound = "true";
// Determine image sources
// Priority: data-origin > data-src > src
const originalSrc = this.getImageSrc(img);
// Item configuration
const item = {
src: originalSrc,
w: 0,
h: 0,
element: img,
index: index,
loading: false
};
this.items.push(item);
// Bind click event and store handler for cleanup
const clickHandler = (e) => this.handleClick(e, index);
this._clickHandlers.set(img, clickHandler);
img.addEventListener('click', clickHandler);
img.style.cursor = 'zoom-in';
if (this.options.preloadAll) {
this.preloadImage(item);
}
});
}
/**
* 清理事件监听器和相关资源
*/
cleanup() {
// 移除所有存储的事件监听器
this._clickHandlers.forEach((handler, img) => {
img.removeEventListener('click', handler);
delete img.dataset.pswpBound;
});
this._clickHandlers.clear();
// 关闭可能打开的 lightbox
if (this.lightbox && this.lightbox.pswp) {
this.lightbox.close();
}
// 重置状态
this.items = [];
this.lightbox = null;
this._isClosedByNavigation = false;
this._isClosing = false;
}
/**
* Get the highest priority image source
* @param {HTMLImageElement} img
* @returns {string}
*/
getImageSrc(img) {
if (img.dataset.origin && img.dataset.origin.trim() !== '') {
return img.dataset.origin;
}
if (img.dataset.src && img.dataset.src.trim() !== '') {
return img.dataset.src;
}
return img.src;
}
preloadImage(item) {
if (item.w > 0 && item.h > 0) return;
if (item.loading) return;
item.loading = true;
const preloadImg = new Image();
preloadImg.src = item.src;
preloadImg.onload = () => {
item.w = preloadImg.naturalWidth;
item.h = preloadImg.naturalHeight;
item.loading = false;
};
preloadImg.onerror = () => {
item.loading = false;
};
}
preloadNeighbors(currentIndex) {
const count = this.options.preloadNeighbors;
// Always preload current
this.preloadImage(this.items[currentIndex]);
// Preload neighbors
for (let i = 1; i <= count; i++) {
const nextIndex = currentIndex + i;
const prevIndex = currentIndex - i;
if (nextIndex < this.items.length) {
this.preloadImage(this.items[nextIndex]);
}
if (prevIndex >= 0) {
this.preloadImage(this.items[prevIndex]);
}
}
}
handleClick(e, index) {
e.preventDefault();
const item = this.items[index];
if (item.w > 0 && item.h > 0) {
this.open(index);
return;
}
const img = new Image();
img.src = item.src;
img.onload = () => {
item.w = img.naturalWidth;
item.h = img.naturalHeight;
this.open(index);
};
img.onerror = () => {
this.open(index);
};
}
open(index, fromHash = false) {
// If already open, just update index
if (this.lightbox && this.lightbox.pswp) {
this.lightbox.pswp.goTo(index);
return;
}
this._isClosing = false;
const lightbox = new PhotoSwipeLightbox({
dataSource: this.items,
pswpModule: PhotoSwipe,
index: index,
bgOpacity: 0.9,
showHideAnimationType: 'zoom',
errorMsg: '<div class="pswp__error-msg">图片加载失败</div>'
});
this.lightbox = lightbox;
// Preload based on config
if (!this.options.preloadAll) {
this.preloadNeighbors(index);
lightbox.on('change', () => {
const newIndex = lightbox.currIndex;
this.preloadNeighbors(newIndex);
this.updateHash(newIndex);
});
} else {
lightbox.on('change', () => {
this.updateHash(lightbox.currIndex);
});
}
// Handle dimensions if not yet preloaded
lightbox.on('contentLoad', (e) => {
const { content } = e;
// If dimensions are already known (from preload), let it proceed
if (content.data.w > 0 && content.data.h > 0) {
return;
}
// Do NOT prevent default here.
// We want PhotoSwipe to proceed with creating the slide elements,
// even if dimensions are temporarily 0.
// e.preventDefault();
const img = new Image();
img.src = content.data.src;
img.onload = () => {
content.data.w = img.naturalWidth;
content.data.h = img.naturalHeight;
// Update item for future use
const item = lightbox.options.dataSource[content.index];
if (item) {
item.w = img.naturalWidth;
item.h = img.naturalHeight;
item.loading = false;
}
};
img.onerror = () => {
console.error('Failed to load image:', content.data.src);
};
});
lightbox.on('close', () => {
this._isClosing = true;
if (!this._isClosedByNavigation && window.location.hash.includes(`gid=${this.options.galleryId}`)) {
window.history.back();
}
this.lightbox = null;
this._isClosedByNavigation = false;
});
// If not opened from hash, push the initial state
if (!fromHash) {
this.pushHash(index);
}
lightbox.init();
}
// Hash Helper Methods
parseHash() {
const hash = window.location.hash.substring(1);
const params = {};
if (hash.length < 5) return params;
const vars = hash.split('&');
for (let i = 0; i < vars.length; i++) {
if (!vars[i]) continue;
const pair = vars[i].split('=');
if (pair.length < 2) continue;
params[pair[0]] = pair[1];
}
if (params.gid) params.gid = parseInt(params.gid, 10);
if (params.pid) params.pid = parseInt(params.pid, 10);
return params;
}
checkHash() {
const params = this.parseHash();
if (params.gid === this.options.galleryId && params.pid > 0 && params.pid <= this.items.length) {
this.open(params.pid - 1, true);
}
}
handleHashChange() {
const params = this.parseHash();
// If hash matches this gallery
if (params.gid === this.options.galleryId && params.pid > 0 && params.pid <= this.items.length) {
this.open(params.pid - 1, true);
} else {
// Hash doesn't match or is empty
if (this.lightbox && !this._isClosing) {
this._isClosedByNavigation = true;
this.lightbox.close();
}
}
}
pushHash(index) {
const hash = `&gid=${this.options.galleryId}&pid=${index + 1}`;
window.history.pushState(null, '', `#${hash}`);
}
updateHash(index) {
const hash = `&gid=${this.options.galleryId}&pid=${index + 1}`;
window.history.replaceState(null, '', `#${hash}`);
}
}
评论加载中...