hexo-theme-gnix 6.2.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/include/hexo/encrypt.js +42 -0
- package/include/hexo/feed.js +329 -0
- package/include/util/common.js +7 -9
- package/languages/en.yml +6 -3
- package/languages/zh-CN.yml +6 -3
- package/layout/archive.jsx +86 -65
- package/layout/comment/twikoo.jsx +2 -11
- package/layout/comment/waline.jsx +2 -2
- package/layout/common/article.jsx +5 -8
- package/layout/common/article_cover.jsx +11 -1
- package/layout/common/article_media.jsx +2 -4
- package/layout/common/footer.jsx +11 -31
- package/layout/common/head.jsx +6 -14
- package/layout/common/navbar.jsx +4 -4
- package/layout/common/scripts.jsx +6 -6
- package/layout/common/theme_selector.jsx +5 -6
- package/layout/common/toc.jsx +8 -14
- package/layout/index.jsx +2 -4
- package/layout/misc/article_licensing.jsx +4 -2
- package/layout/misc/open_graph.jsx +4 -4
- package/layout/misc/paginator.jsx +10 -4
- package/layout/misc/structured_data.jsx +3 -4
- package/layout/plugin/busuanzi.jsx +1 -1
- package/layout/plugin/cookie_consent.jsx +40 -31
- package/layout/plugin/swup.jsx +2 -22
- package/layout/search/insight.jsx +16 -3
- package/package.json +12 -8
- package/scripts/hot-reload.js +92 -0
- package/scripts/index.js +2 -0
- package/source/css/archive.css +251 -0
- package/source/css/default.css +250 -284
- package/source/css/encrypt.css +55 -0
- package/source/css/responsive/desktop.css +0 -119
- package/source/css/responsive/mobile.css +7 -23
- package/source/css/responsive/touch.css +9 -103
- package/source/css/shiki/shiki.css +7 -22
- package/source/css/twikoo.css +290 -830
- package/source/img/og_image.webp +0 -0
- package/source/js/archive-breadcrumb.js +132 -0
- package/source/js/busuanzi.js +1 -12
- package/source/js/components/accordion.js +192 -0
- package/source/js/components/chat.js +239 -0
- package/source/js/components/device-carousel.js +260 -0
- package/source/js/components/image-carousel.js +410 -0
- package/source/js/components/text-image-section.js +180 -0
- package/source/js/components/theme-stacked.js +526 -0
- package/source/js/components/tree.js +437 -0
- package/source/js/decrypt.js +112 -0
- package/source/js/insight.js +75 -65
- package/source/js/main.js +192 -99
- package/source/js/mdit/mermaid.js +12 -4
- package/source/js/swup.bundle.js +1 -0
- package/source/js/theme-selector.js +94 -113
- package/source/img/og_image.png +0 -0
- package/source/js/host/swup/Swup.umd.min.js +0 -1
- package/source/js/host/swup/head-plugin.umd.min.js +0 -1
- package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
- package/source/js/mdit/shiki.js +0 -158
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Carousel Custom Element
|
|
3
|
+
* A seamless infinite scrolling carousel for showcasing devices
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <device-carousel></device-carousel>
|
|
7
|
+
*
|
|
8
|
+
* Or with custom devices:
|
|
9
|
+
* <device-carousel>
|
|
10
|
+
* <device-card
|
|
11
|
+
* name="Device Name"
|
|
12
|
+
* image="/path/to/image.png"
|
|
13
|
+
* specs="Spec 1<br>Spec 2"
|
|
14
|
+
* ></device-card>
|
|
15
|
+
* </device-carousel>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class DeviceCarousel extends HTMLElement {
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
this.attachShadow({ mode: "open" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connectedCallback() {
|
|
25
|
+
this.render();
|
|
26
|
+
this.setupIntersectionObserver();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get defaultDevices() {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
name: "Galaxy S24+",
|
|
33
|
+
image: "/assets/common/s24_plus.png",
|
|
34
|
+
alt: "Samsung Galaxy S24 Plus",
|
|
35
|
+
specs: "Snapdragon 8 Gen 3<br>12GB RAM + 512GB Storage",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "MacBook Air",
|
|
39
|
+
image: "/assets/common/MacBook_Air.png",
|
|
40
|
+
alt: "MacBook Air",
|
|
41
|
+
specs: "Apple M3 Chip<br>24GB Unified Memory<br>15-inch Liquid Retina",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "Legion R9000P",
|
|
45
|
+
image: "/assets/common/r9kp.webp",
|
|
46
|
+
alt: "Lenovo Legion R9000P 2021",
|
|
47
|
+
specs: 'AMD Ryzen 7 5800H<br>RTX 3060 / 16GB RAM<br>15.6" 165Hz Display',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "Galaxy Watch7",
|
|
51
|
+
image: "/assets/common/galaxy-watch7.webp",
|
|
52
|
+
alt: "Galaxy Watch 7",
|
|
53
|
+
specs: "44mm LTE Version<br>Exynos W1000<br>Health & Fitness Tracking",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "Beoplay H9i",
|
|
57
|
+
image: "/assets/common/beoplay-h9i.png",
|
|
58
|
+
alt: "Beoplay H9i",
|
|
59
|
+
specs: "Over-ear Wireless Headphones<br>Active Noise Cancellation",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "Galaxy Buds2 Pro",
|
|
63
|
+
image: "/assets/common/galaxy-buds2pro.webp",
|
|
64
|
+
alt: "Galaxy Buds2 Pro",
|
|
65
|
+
specs: "In-ear Wireless Earbuds<br>Active Noise Cancellation",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
render() {
|
|
71
|
+
const style = `
|
|
72
|
+
:host {
|
|
73
|
+
display: block;
|
|
74
|
+
--card-width: 280px;
|
|
75
|
+
--card-gap: 1.5rem;
|
|
76
|
+
--card-padding: 1.5rem;
|
|
77
|
+
--animation-duration: 40s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.showcase-container {
|
|
81
|
+
position: relative;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
margin: 1.5rem 0;
|
|
84
|
+
mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
|
|
85
|
+
-webkit-mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.showcase-track {
|
|
89
|
+
display: flex;
|
|
90
|
+
padding: 1.5rem 0;
|
|
91
|
+
width: max-content;
|
|
92
|
+
animation: scroll var(--animation-duration) linear infinite;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.showcase-track:hover {
|
|
96
|
+
animation-play-state: paused;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@keyframes scroll {
|
|
100
|
+
0% { transform: translateX(0); }
|
|
101
|
+
100% { transform: translateX(-50%); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.showcase-card {
|
|
105
|
+
flex: 0 0 auto;
|
|
106
|
+
width: var(--card-width);
|
|
107
|
+
margin-right: var(--card-gap);
|
|
108
|
+
padding: var(--card-padding);
|
|
109
|
+
border-radius: 12px;
|
|
110
|
+
background: var(--crust, #1e1e2e);
|
|
111
|
+
border: 1px solid var(--surface0, #313244);
|
|
112
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
align-items: center;
|
|
116
|
+
text-align: center;
|
|
117
|
+
box-sizing: border-box;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.showcase-card:hover {
|
|
121
|
+
transform: translateY(-2px);
|
|
122
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.showcase-label {
|
|
126
|
+
font-size: 0.75rem;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
letter-spacing: 0.1em;
|
|
130
|
+
color: var(--subtext1, #bac2de);
|
|
131
|
+
margin-bottom: 0.75rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.showcase-content {
|
|
135
|
+
margin-bottom: 0.75rem;
|
|
136
|
+
color: var(--text, #cdd6f4);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.device-image {
|
|
140
|
+
height: 120px;
|
|
141
|
+
width: auto;
|
|
142
|
+
max-width: 100%;
|
|
143
|
+
object-fit: contain;
|
|
144
|
+
transition: transform 0.3s ease;
|
|
145
|
+
display: block;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.showcase-card:hover .device-image {
|
|
149
|
+
transform: scale(1.1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.showcase-meta {
|
|
153
|
+
font-family: var(--font-mono, 'Maple Mono', 'Fira Code', monospace);
|
|
154
|
+
font-size: 0.75rem;
|
|
155
|
+
color: var(--subtext0, #a6adc8);
|
|
156
|
+
padding: 0.5rem 0.75rem;
|
|
157
|
+
background: var(--mantle, #181825);
|
|
158
|
+
border-radius: 6px;
|
|
159
|
+
word-break: break-all;
|
|
160
|
+
line-height: 1.5;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Pause animation when not visible to save resources */
|
|
164
|
+
@media (prefers-reduced-motion: reduce) {
|
|
165
|
+
.showcase-track {
|
|
166
|
+
animation: none;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
// Get devices from slots or use defaults
|
|
172
|
+
const devices = this.getDevices();
|
|
173
|
+
|
|
174
|
+
// Duplicate for seamless infinite scroll
|
|
175
|
+
const allDevices = [...devices, ...devices];
|
|
176
|
+
|
|
177
|
+
const cardsHTML = allDevices
|
|
178
|
+
.map(
|
|
179
|
+
(device) => `
|
|
180
|
+
<div class="showcase-card">
|
|
181
|
+
<div class="showcase-label">${device.name}</div>
|
|
182
|
+
<div class="showcase-content">
|
|
183
|
+
<img src="${device.image}" alt="${device.alt}" class="device-image" loading="lazy"/>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="showcase-meta">${device.specs}</div>
|
|
186
|
+
</div>
|
|
187
|
+
`,
|
|
188
|
+
)
|
|
189
|
+
.join("");
|
|
190
|
+
|
|
191
|
+
this.shadowRoot.innerHTML = `
|
|
192
|
+
<style>${style}</style>
|
|
193
|
+
<div class="showcase-container">
|
|
194
|
+
<div class="showcase-track">
|
|
195
|
+
${cardsHTML}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<slot style="display: none;"></slot>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getDevices() {
|
|
203
|
+
// Check if there are any device-card children in the slot
|
|
204
|
+
const slot = this.querySelectorAll("device-card");
|
|
205
|
+
if (slot.length > 0) {
|
|
206
|
+
return Array.from(slot).map((card) => ({
|
|
207
|
+
name: card.getAttribute("name") || "Device",
|
|
208
|
+
image: card.getAttribute("image") || "",
|
|
209
|
+
alt: card.getAttribute("alt") || card.getAttribute("name") || "Device",
|
|
210
|
+
specs: card.getAttribute("specs") || "",
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
return this.defaultDevices;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Pause animation when not visible
|
|
217
|
+
setupIntersectionObserver() {
|
|
218
|
+
const track = this.shadowRoot.querySelector(".showcase-track");
|
|
219
|
+
if (!track) return;
|
|
220
|
+
|
|
221
|
+
const observer = new IntersectionObserver(
|
|
222
|
+
(entries) => {
|
|
223
|
+
entries.forEach((entry) => {
|
|
224
|
+
if (entry.isIntersecting) {
|
|
225
|
+
track.style.animationPlayState = "running";
|
|
226
|
+
} else {
|
|
227
|
+
track.style.animationPlayState = "paused";
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
{ threshold: 0.1 },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
observer.observe(this);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static get observedAttributes() {
|
|
238
|
+
return ["speed", "card-width"];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
242
|
+
if (oldValue === newValue) return;
|
|
243
|
+
|
|
244
|
+
if (name === "speed" && this.shadowRoot) {
|
|
245
|
+
const track = this.shadowRoot.querySelector(".showcase-track");
|
|
246
|
+
if (track) {
|
|
247
|
+
track.style.setProperty("--animation-duration", `${newValue}s`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (name === "card-width" && this.shadowRoot) {
|
|
252
|
+
this.shadowRoot.host.style.setProperty("--card-width", newValue);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Register custom elements
|
|
258
|
+
customElements.define("device-carousel", DeviceCarousel);
|
|
259
|
+
|
|
260
|
+
export { DeviceCarousel };
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Carousel Custom Element
|
|
3
|
+
* A responsive image carousel with fade transitions and autoplay
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <image-carousel autoplay interval="4000">
|
|
7
|
+
* 
|
|
8
|
+
* 
|
|
9
|
+
* </image-carousel>
|
|
10
|
+
*
|
|
11
|
+
* Attributes:
|
|
12
|
+
* - autoplay: Enable automatic slide advancement
|
|
13
|
+
* - interval: Autoplay interval in ms (default: 3000)
|
|
14
|
+
* - ratio: Aspect ratio as CSS value (default: 3/2)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Shared stylesheet — parsed once, reused across all carousel instances
|
|
18
|
+
let _sheet;
|
|
19
|
+
|
|
20
|
+
const STYLES = `
|
|
21
|
+
:host {
|
|
22
|
+
display: block;
|
|
23
|
+
margin: 1.5em 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.carousel {
|
|
27
|
+
outline: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.carousel:focus-visible {
|
|
31
|
+
outline: 2px solid var(--blue, #89b4fa);
|
|
32
|
+
outline-offset: 2px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.stage {
|
|
36
|
+
position: relative;
|
|
37
|
+
border-radius: var(--radius, 12px);
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
background: var(--crust, #11111b);
|
|
40
|
+
contain: content;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.slides {
|
|
44
|
+
position: relative;
|
|
45
|
+
width: 100%;
|
|
46
|
+
aspect-ratio: var(--carousel-ratio, 3/2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.slide {
|
|
50
|
+
position: absolute;
|
|
51
|
+
inset: 0;
|
|
52
|
+
opacity: 0;
|
|
53
|
+
transition: opacity 0.7s cubic-bezier(0.25, 1, 0.5, 1);
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.slide.active {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
pointer-events: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.slide figure {
|
|
63
|
+
margin: 0;
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.slide img {
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 100%;
|
|
71
|
+
object-fit: cover;
|
|
72
|
+
display: block;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.slide figcaption {
|
|
76
|
+
position: absolute;
|
|
77
|
+
bottom: 0;
|
|
78
|
+
left: 0;
|
|
79
|
+
right: 0;
|
|
80
|
+
padding: 2rem 1.25rem 0.875rem;
|
|
81
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
|
|
82
|
+
color: #fff;
|
|
83
|
+
font-size: 0.875rem;
|
|
84
|
+
font-style: italic;
|
|
85
|
+
font-family: var(--font-serif, serif);
|
|
86
|
+
text-align: center;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.nav {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 50%;
|
|
93
|
+
transform: translateY(-50%);
|
|
94
|
+
background: rgba(0, 0, 0, 0.28);
|
|
95
|
+
backdrop-filter: blur(4px);
|
|
96
|
+
-webkit-backdrop-filter: blur(4px);
|
|
97
|
+
color: white;
|
|
98
|
+
border: none;
|
|
99
|
+
width: 2.5rem;
|
|
100
|
+
height: 2.5rem;
|
|
101
|
+
border-radius: 50%;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
opacity: 0;
|
|
107
|
+
transition: background 0.2s ease-out, opacity 0.2s ease-out;
|
|
108
|
+
z-index: 2;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nav svg {
|
|
113
|
+
width: 1.5rem;
|
|
114
|
+
height: 1.5rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.carousel:hover .nav,
|
|
118
|
+
.carousel:focus-within .nav {
|
|
119
|
+
opacity: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.nav:hover {
|
|
123
|
+
background: rgba(0, 0, 0, 0.55);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.nav:focus-visible {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
outline: 2px solid #fff;
|
|
129
|
+
outline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.nav:active {
|
|
133
|
+
transform: translateY(-50%) scale(0.92);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.prev { left: 0.75rem; }
|
|
137
|
+
.next { right: 0.75rem; }
|
|
138
|
+
|
|
139
|
+
.dots {
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
gap: 6px;
|
|
143
|
+
padding: 0.5rem 0 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.dot {
|
|
147
|
+
width: 8px;
|
|
148
|
+
height: 8px;
|
|
149
|
+
border-radius: 50%;
|
|
150
|
+
border: none;
|
|
151
|
+
background: var(--overlay0, rgba(127, 127, 127, 0.45));
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
padding: 0;
|
|
154
|
+
transition: background 0.2s ease-out, transform 0.2s ease-out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.dot:hover {
|
|
158
|
+
background: var(--overlay1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.dot:focus-visible {
|
|
162
|
+
outline: 2px solid var(--blue, #89b4fa);
|
|
163
|
+
outline-offset: 2px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.dot.active {
|
|
167
|
+
background: var(--text);
|
|
168
|
+
transform: scale(1.25);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
.slide { transition: none; }
|
|
173
|
+
.nav, .dot { transition: none; }
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
class ImageCarousel extends HTMLElement {
|
|
178
|
+
constructor() {
|
|
179
|
+
super();
|
|
180
|
+
this.attachShadow({ mode: "open" });
|
|
181
|
+
this._currentIndex = 0;
|
|
182
|
+
this._timer = null;
|
|
183
|
+
this._images = [];
|
|
184
|
+
this._slides = [];
|
|
185
|
+
this._dots = [];
|
|
186
|
+
this._touchStartX = 0;
|
|
187
|
+
this._observer = null;
|
|
188
|
+
this._isVisible = true;
|
|
189
|
+
this._handleVisibility = () => {
|
|
190
|
+
if (document.hidden) this._stopAutoplay();
|
|
191
|
+
else if (this._isVisible && this.hasAttribute("autoplay")) this._startAutoplay();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
connectedCallback() {
|
|
196
|
+
this._images = this._collectImages();
|
|
197
|
+
this.render();
|
|
198
|
+
if (this._images.length > 1) {
|
|
199
|
+
this._setupListeners();
|
|
200
|
+
this._observeVisibility();
|
|
201
|
+
}
|
|
202
|
+
if (this.hasAttribute("autoplay") && this._images.length > 1) {
|
|
203
|
+
this._startAutoplay();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
disconnectedCallback() {
|
|
208
|
+
this._stopAutoplay();
|
|
209
|
+
this._observer?.disconnect();
|
|
210
|
+
document.removeEventListener("visibilitychange", this._handleVisibility);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_collectImages() {
|
|
214
|
+
return Array.from(this.querySelectorAll("img")).map((img) => ({
|
|
215
|
+
src: img.src || img.getAttribute("src"),
|
|
216
|
+
alt: img.alt || "",
|
|
217
|
+
srcset: img.getAttribute("srcset") || "",
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_getInterval() {
|
|
222
|
+
return parseInt(this.getAttribute("interval") || "3000", 10);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_startAutoplay() {
|
|
226
|
+
this._stopAutoplay();
|
|
227
|
+
this._timer = setInterval(() => {
|
|
228
|
+
this._goTo((this._currentIndex + 1) % this._images.length);
|
|
229
|
+
}, this._getInterval());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_stopAutoplay() {
|
|
233
|
+
if (this._timer) {
|
|
234
|
+
clearInterval(this._timer);
|
|
235
|
+
this._timer = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_goTo(index) {
|
|
240
|
+
const prev = this._currentIndex;
|
|
241
|
+
this._currentIndex = index;
|
|
242
|
+
this._toggleSlide(prev, false);
|
|
243
|
+
this._toggleSlide(index, true);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_toggleSlide(i, active) {
|
|
247
|
+
this._slides[i]?.classList.toggle("active", active);
|
|
248
|
+
const dot = this._dots[i];
|
|
249
|
+
if (dot) {
|
|
250
|
+
dot.classList.toggle("active", active);
|
|
251
|
+
dot.setAttribute("aria-selected", String(active));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
render() {
|
|
256
|
+
const images = this._images;
|
|
257
|
+
const ratio = this.getAttribute("ratio") || "3/2";
|
|
258
|
+
|
|
259
|
+
if (images.length === 0) {
|
|
260
|
+
this.shadowRoot.innerHTML = "";
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!_sheet) {
|
|
265
|
+
_sheet = new CSSStyleSheet();
|
|
266
|
+
_sheet.replaceSync(STYLES);
|
|
267
|
+
}
|
|
268
|
+
this.shadowRoot.adoptedStyleSheets = [_sheet];
|
|
269
|
+
this.style.setProperty("--carousel-ratio", ratio);
|
|
270
|
+
|
|
271
|
+
const slidesHTML = images
|
|
272
|
+
.map(
|
|
273
|
+
(img, i) => `
|
|
274
|
+
<div class="slide${i === 0 ? " active" : ""}" role="tabpanel" aria-label="${img.alt || `Slide ${i + 1}`}">
|
|
275
|
+
<figure>
|
|
276
|
+
<img src="${img.src}"${img.srcset ? ` srcset="${img.srcset}"` : ""} alt="${img.alt}" loading="${i === 0 ? "eager" : "lazy"}">
|
|
277
|
+
${img.alt ? `<figcaption>${img.alt}</figcaption>` : ""}
|
|
278
|
+
</figure>
|
|
279
|
+
</div>
|
|
280
|
+
`,
|
|
281
|
+
)
|
|
282
|
+
.join("");
|
|
283
|
+
|
|
284
|
+
const navHTML =
|
|
285
|
+
images.length > 1
|
|
286
|
+
? `<button class="nav prev" aria-label="Previous slide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
287
|
+
<button class="nav next" aria-label="Next slide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg></button>`
|
|
288
|
+
: "";
|
|
289
|
+
|
|
290
|
+
const dotsHTML =
|
|
291
|
+
images.length > 1
|
|
292
|
+
? `<div class="dots" role="tablist" aria-label="Slide navigation">
|
|
293
|
+
${images.map((_img, i) => `<button class="dot${i === 0 ? " active" : ""}" data-index="${i}" role="tab" aria-label="Slide ${i + 1}" aria-selected="${i === 0}"></button>`).join("")}
|
|
294
|
+
</div>`
|
|
295
|
+
: "";
|
|
296
|
+
|
|
297
|
+
this.shadowRoot.innerHTML = `
|
|
298
|
+
<div class="carousel" role="region" aria-label="Image carousel" tabindex="0">
|
|
299
|
+
<div class="stage">
|
|
300
|
+
<div class="slides">${slidesHTML}</div>
|
|
301
|
+
${navHTML}
|
|
302
|
+
</div>
|
|
303
|
+
${dotsHTML}
|
|
304
|
+
</div>
|
|
305
|
+
<slot style="display:none"></slot>
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
this._slides = Array.from(this.shadowRoot.querySelectorAll(".slide"));
|
|
309
|
+
this._dots = Array.from(this.shadowRoot.querySelectorAll(".dot"));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_setupListeners() {
|
|
313
|
+
const root = this.shadowRoot;
|
|
314
|
+
const carousel = root.querySelector(".carousel");
|
|
315
|
+
const n = this._images.length;
|
|
316
|
+
|
|
317
|
+
root.querySelector(".prev")?.addEventListener("click", () => {
|
|
318
|
+
this._resetAutoplay();
|
|
319
|
+
this._goTo((this._currentIndex - 1 + n) % n);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
root.querySelector(".next")?.addEventListener("click", () => {
|
|
323
|
+
this._resetAutoplay();
|
|
324
|
+
this._goTo((this._currentIndex + 1) % n);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
root.querySelector(".dots")?.addEventListener("click", (e) => {
|
|
328
|
+
const dot = e.target.closest(".dot");
|
|
329
|
+
if (!dot) return;
|
|
330
|
+
this._resetAutoplay();
|
|
331
|
+
this._goTo(parseInt(dot.dataset.index, 10));
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Pause autoplay on hover
|
|
335
|
+
carousel.addEventListener("mouseenter", () => this._stopAutoplay());
|
|
336
|
+
carousel.addEventListener("mouseleave", () => {
|
|
337
|
+
if (this.hasAttribute("autoplay")) this._startAutoplay();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Keyboard navigation
|
|
341
|
+
carousel.addEventListener("keydown", (e) => {
|
|
342
|
+
if (e.key === "ArrowLeft") {
|
|
343
|
+
this._resetAutoplay();
|
|
344
|
+
this._goTo((this._currentIndex - 1 + n) % n);
|
|
345
|
+
} else if (e.key === "ArrowRight") {
|
|
346
|
+
this._resetAutoplay();
|
|
347
|
+
this._goTo((this._currentIndex + 1) % n);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Touch/swipe support
|
|
352
|
+
carousel.addEventListener(
|
|
353
|
+
"touchstart",
|
|
354
|
+
(e) => {
|
|
355
|
+
this._touchStartX = e.touches[0].clientX;
|
|
356
|
+
},
|
|
357
|
+
{ passive: true },
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
carousel.addEventListener(
|
|
361
|
+
"touchend",
|
|
362
|
+
(e) => {
|
|
363
|
+
const dx = e.changedTouches[0].clientX - this._touchStartX;
|
|
364
|
+
if (Math.abs(dx) > 40) {
|
|
365
|
+
this._resetAutoplay();
|
|
366
|
+
this._goTo(dx < 0 ? (this._currentIndex + 1) % n : (this._currentIndex - 1 + n) % n);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{ passive: true },
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_observeVisibility() {
|
|
374
|
+
this._observer = new IntersectionObserver(([entry]) => {
|
|
375
|
+
this._isVisible = entry.isIntersecting;
|
|
376
|
+
if (!entry.isIntersecting) {
|
|
377
|
+
this._stopAutoplay();
|
|
378
|
+
} else if (this.hasAttribute("autoplay")) {
|
|
379
|
+
this._startAutoplay();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
this._observer.observe(this);
|
|
383
|
+
document.addEventListener("visibilitychange", this._handleVisibility);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_resetAutoplay() {
|
|
387
|
+
if (this.hasAttribute("autoplay")) {
|
|
388
|
+
this._stopAutoplay();
|
|
389
|
+
this._startAutoplay();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
static get observedAttributes() {
|
|
394
|
+
return ["autoplay", "interval"];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
398
|
+
if (oldValue === newValue || !this._images.length) return;
|
|
399
|
+
if (name === "autoplay") {
|
|
400
|
+
newValue !== null ? this._startAutoplay() : this._stopAutoplay();
|
|
401
|
+
}
|
|
402
|
+
if (name === "interval" && this._timer) {
|
|
403
|
+
this._startAutoplay();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
customElements.define("image-carousel", ImageCarousel);
|
|
409
|
+
|
|
410
|
+
export { ImageCarousel };
|