代码
使用 Shadow DOM
展开
/**
* Gallery组件
* 使用示例:
* <gallery-component>
* <img src="image1.jpg" alt="Image 1">
* <img src="image2.jpg" alt="Image 2">
* <img src="image3.jpg" alt="Image 3">
* </gallery-component>
*/
class GalleryElement extends HTMLElement {
private readonly shadow: ShadowRoot;
private currentIndex: number = 0;
private items: Element[] = [];
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
this.render();
}
connectedCallback() {
this.buildGallery();
this.addEventListeners();
}
private render() {
const style = document.createElement('style');
style.textContent = `
.gb-gallery-container {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.gb-gallery-track {
display: flex;
transition: transform 0.3s ease;
}
.gb-gallery-item {
min-width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.gb-gallery-item img {
max-width: 100%;
max-height: 600px;
object-fit: contain;
}
.gb-gallery-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
font-size: 24px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.3s;
user-select: none;
}
.gb-gallery-nav:hover {
background: rgba(0, 0, 0, 0.8);
}
.gb-gallery-nav.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gb-gallery-nav.prev {
left: 16px;
}
.gb-gallery-nav.next {
right: 16px;
}
.gb-gallery-counter {
position: absolute;
bottom: 16px;
right: 16px;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
}
`;
const container = document.createElement('div');
container.className = 'gb-gallery-container';
container.innerHTML = `
<div class="gb-gallery-track"></div>
<button class="gb-gallery-nav prev">‹</button>
<button class="gb-gallery-nav next">›</button>
<div class="gb-gallery-counter"></div>
`;
this.shadow.append(style, container);
}
private buildGallery() {
this.items = Array.from(this.children);
const track = this.shadow.querySelector('.gb-gallery-track');
const counter = this.shadow.querySelector('.gb-gallery-counter');
if (!track || !counter) return;
// 清空容器
track.innerHTML = '';
// 创建项目
this.items.forEach((item, index) => {
// 创建项目
const itemElement = document.createElement('div');
itemElement.className = 'gb-gallery-item';
itemElement.appendChild(item.cloneNode(true));
track.appendChild(itemElement);
});
// 更新计数器
this.updateCounter();
}
private addEventListeners() {
const prevButton = this.shadow.querySelector('.gb-gallery-nav.prev');
const nextButton = this.shadow.querySelector('.gb-gallery-nav.next');
prevButton?.addEventListener('click', () => this.prev());
nextButton?.addEventListener('click', () => this.next());
}
private updateGallery() {
const track = this.shadow.querySelector('.gb-gallery-track') as HTMLElement;
const counter = this.shadow.querySelector('.gb-gallery-counter');
const prevButton = this.shadow.querySelector('.gb-gallery-nav.prev') as HTMLElement;
const nextButton = this.shadow.querySelector('.gb-gallery-nav.next') as HTMLElement;
if (!track || !counter || !prevButton || !nextButton) return;
// 更新轨道位置
track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
// 更新导航按钮状态
if (this.currentIndex === 0) {
prevButton.classList.add('disabled');
} else {
prevButton.classList.remove('disabled');
}
if (this.currentIndex === this.items.length - 1) {
nextButton.classList.add('disabled');
} else {
nextButton.classList.remove('disabled');
}
// 更新计数器
this.updateCounter();
}
private updateCounter() {
const counter = this.shadow.querySelector('.gb-gallery-counter');
if (counter && this.items.length > 0) {
counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
}
}
private prev() {
this.currentIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
this.updateGallery();
}
private next() {
this.currentIndex = (this.currentIndex + 1) % this.items.length;
this.updateGallery();
}
private goTo(index: number) {
if (index >= 0 && index < this.items.length) {
this.currentIndex = index;
this.updateGallery();
}
}
}
customElements.define('gallery-component', GalleryElement);
无 Shadow DOM
展开
/**
* Gallery组件 (无 Shadow DOM 版本)
* 使用示例:
* <gallery-no-shadow>
* <img src="image1.jpg" alt="Image 1">
* <img src="image2.jpg" alt="Image 2">
* <img src="image3.jpg" alt="Image 3">
* </gallery-no-shadow>
*/
class GalleryNoShadowElement extends HTMLElement {
private currentIndex: number = 0;
private items: Element[] = [];
private container: HTMLDivElement | null = null;
private track: HTMLDivElement | null = null;
private counter: HTMLDivElement | null = null;
private prevButton: HTMLButtonElement | null = null;
private nextButton: HTMLButtonElement | null = null;
constructor() {
super();
this.render();
}
connectedCallback() {
this.buildGallery();
this.addEventListeners();
}
private render() {
// 创建样式
const style = document.createElement('style');
style.textContent = `
.gbns-gallery-container {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
overflow: hidden;
border-radius: 8px;
}
.gbns-gallery-track {
display: flex;
transition: transform 0.3s ease;
}
.gbns-gallery-item {
min-width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.gbns-gallery-item img {
max-width: 100%;
max-height: 600px;
object-fit: contain;
}
.gbns-gallery-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
font-size: 24px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.3s;
user-select: none;
}
.gbns-gallery-nav:hover {
background: rgba(0, 0, 0, 0.8);
}
.gbns-gallery-nav.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gbns-gallery-nav.prev {
left: 16px;
}
.gbns-gallery-nav.next {
right: 16px;
}
.gbns-gallery-counter {
position: absolute;
bottom: 16px;
right: 16px;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
}
`;
// 创建容器
this.container = document.createElement('div');
this.container.className = 'gbns-gallery-container';
this.container.innerHTML = `
<div class="gbns-gallery-track"></div>
<button class="gbns-gallery-nav prev">‹</button>
<button class="gbns-gallery-nav next">›</button>
<div class="gbns-gallery-counter"></div>
`;
// 获取元素引用
this.track = this.container.querySelector('.gbns-gallery-track');
this.counter = this.container.querySelector('.gbns-gallery-counter');
this.prevButton = this.container.querySelector('.gbns-gallery-nav.prev');
this.nextButton = this.container.querySelector('.gbns-gallery-nav.next');
// 添加到组件
this.appendChild(style);
this.appendChild(this.container);
}
private buildGallery() {
if (!this.track || !this.counter) return;
// 获取原始子元素(排除容器和样式)
this.items = Array.from(this.children).filter(
(child) => child !== this.container && child.tagName !== 'STYLE'
);
// 清空容器
this.track.innerHTML = '';
// 创建项目并移除原始子元素
this.items.forEach((item) => {
const itemElement = document.createElement('div');
itemElement.className = 'gbns-gallery-item';
itemElement.appendChild(item); // 直接移动原始元素,而非克隆
this.track!.appendChild(itemElement);
});
// 更新计数器
this.updateCounter();
}
private addEventListeners() {
this.prevButton?.addEventListener('click', () => this.prev());
this.nextButton?.addEventListener('click', () => this.next());
}
private updateGallery() {
if (!this.track || !this.counter || !this.prevButton || !this.nextButton) return;
// 更新轨道位置
this.track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
// 更新导航按钮状态
if (this.currentIndex === 0) {
this.prevButton.classList.add('disabled');
} else {
this.prevButton.classList.remove('disabled');
}
if (this.currentIndex === this.items.length - 1) {
this.nextButton.classList.add('disabled');
} else {
this.nextButton.classList.remove('disabled');
}
// 更新计数器
this.updateCounter();
}
private updateCounter() {
if (this.counter && this.items.length > 0) {
this.counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
}
}
private prev() {
this.currentIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
this.updateGallery();
}
private next() {
this.currentIndex = (this.currentIndex + 1) % this.items.length;
this.updateGallery();
}
private goTo(index: number) {
if (index >= 0 && index < this.items.length) {
this.currentIndex = index;
this.updateGallery();
}
}
}
customElements.define('gallery-no-shadow', GalleryNoShadowElement);
二者区别是无Shadow DOM版可适配灯箱。
使用方法
这里以无 Shadow DOM 版为例:
<gallery-no-shadow>
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-b47w.webp" alt="2025-08-06-b47w.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-su3b.webp" alt="2025-08-06-su3b.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-71xl.webp" alt="2025-08-06-71xl.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-0ms1.webp" alt="2025-08-06-0ms1.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-vjds.webp" alt="2025-08-06-vjds.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-0fv6.webp" alt="2025-08-06-0fv6.webp">
<img src="https://blog-r2.makeinu.cn/images/1/2025-08-06-4by5.webp" alt="2025-08-06-4by5.webp">
</gallery-no-shadow>
预览






