hexo-theme-gnix 9.0.0 → 10.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 +4 -2
- package/include/hexo/feed.js +5 -5
- package/include/hexo/filter.js +25 -1
- package/include/hexo/generator/archive.js +116 -0
- package/include/hexo/generator/home.js +64 -0
- package/include/hexo/generator/index.js +82 -0
- package/include/hexo/generator/md_generator.js +87 -0
- package/include/hexo/generator/page.js +55 -0
- package/include/hexo/generator/tag.js +84 -0
- package/include/hexo/helper.js +38 -0
- package/include/hexo/i18n.js +183 -0
- package/include/util/article_font.js +132 -0
- package/include/util/i18n.js +280 -0
- package/include/util/theme.js +84 -0
- package/languages/en.yml +28 -0
- package/languages/zh-CN.yml +28 -0
- package/layout/archive.jsx +131 -127
- package/layout/common/article.jsx +283 -16
- package/layout/common/article_info.jsx +339 -0
- package/layout/common/article_media.jsx +11 -4
- package/layout/common/comment.jsx +15 -7
- package/layout/common/footer.jsx +6 -5
- package/layout/common/head.jsx +121 -32
- package/layout/common/navbar.jsx +195 -65
- package/layout/common/theme_selector.jsx +16 -14
- package/layout/layout.jsx +43 -5
- package/layout/misc/open_graph.jsx +162 -66
- package/layout/misc/paginator.jsx +2 -8
- package/layout/plugin/cookie_consent.jsx +252 -53
- package/layout/plugin/swup.jsx +1 -1
- package/layout/search/insight.jsx +1 -1
- package/layout/tag.jsx +3 -2
- package/layout/tags.jsx +81 -73
- package/package.json +5 -5
- package/scripts/index.js +1 -0
- package/source/css/archive.css +225 -180
- package/source/css/default.css +1162 -98
- package/source/css/responsive.css +426 -0
- package/source/css/shiki/shiki.css +12 -2081
- package/source/css/tags.css +183 -0
- package/source/css/twikoo.css +1049 -1045
- package/source/img/favicon.svg +1 -6
- package/source/img/og_image.webp +0 -0
- package/source/js/article-font-utils.js +99 -0
- package/source/js/busuanzi.js +91 -24
- package/source/js/components/chat.js +169 -50
- package/source/js/components/image-carousel.js +152 -108
- package/source/js/components/sidenote.js +210 -0
- package/source/js/components/text-image-section.js +78 -90
- package/source/js/components/theme-stacked.js +65 -33
- package/source/js/components/tree.js +30 -16
- package/source/js/decrypt.js +7 -2
- package/source/js/main.js +428 -5
- package/source/js/swup.js +39 -0
- package/source/js/theme-selector.js +26 -16
- package/include/hexo/generator.js +0 -53
- package/layout/misc/article_licensing.jsx +0 -99
- package/source/css/responsive/desktop.css +0 -36
- package/source/css/responsive/mobile.css +0 -29
- package/source/css/responsive/tablet.css +0 -43
- package/source/css/responsive/touch.css +0 -155
- package/source/img/logo.svg +0 -9
- package/source/js/archive-breadcrumb.js +0 -132
- package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +0 -6
- package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +0 -1
- package/source/js/swup.bundle.js +0 -1
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
* Attributes:
|
|
12
12
|
* - autoplay: Enable automatic slide advancement
|
|
13
13
|
* - interval: Autoplay interval in ms (default: 3000)
|
|
14
|
-
* - ratio: Aspect ratio as CSS value (default:
|
|
14
|
+
* - ratio: Aspect ratio as CSS value (default: derived from first image,
|
|
15
|
+
* falls back to 3/2 while loading or if dimensions are unknown)
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
// Shared stylesheet — parsed once, reused across all carousel instances
|
|
18
19
|
let _sheet;
|
|
19
20
|
|
|
21
|
+
const DEFAULT_INTERVAL = 3000;
|
|
22
|
+
const FALLBACK_RATIO = "3 / 2";
|
|
23
|
+
const SWIPE_THRESHOLD_PX = 40;
|
|
24
|
+
|
|
20
25
|
const STYLES = `
|
|
21
26
|
:host {
|
|
22
27
|
display: block;
|
|
@@ -43,7 +48,7 @@ const STYLES = `
|
|
|
43
48
|
.slides {
|
|
44
49
|
position: relative;
|
|
45
50
|
width: 100%;
|
|
46
|
-
aspect-ratio: var(--carousel-ratio, 3/2);
|
|
51
|
+
aspect-ratio: var(--carousel-ratio, 3 / 2);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
.slide {
|
|
@@ -174,6 +179,9 @@ const STYLES = `
|
|
|
174
179
|
}
|
|
175
180
|
`;
|
|
176
181
|
|
|
182
|
+
const CHEVRON_LEFT = `<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>`;
|
|
183
|
+
const CHEVRON_RIGHT = `<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>`;
|
|
184
|
+
|
|
177
185
|
class ImageCarousel extends HTMLElement {
|
|
178
186
|
constructor() {
|
|
179
187
|
super();
|
|
@@ -188,19 +196,21 @@ class ImageCarousel extends HTMLElement {
|
|
|
188
196
|
this._isVisible = true;
|
|
189
197
|
this._handleVisibility = () => {
|
|
190
198
|
if (document.hidden) this._stopAutoplay();
|
|
191
|
-
else if (this._isVisible
|
|
199
|
+
else if (this._isVisible) this._maybeStartAutoplay();
|
|
192
200
|
};
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
connectedCallback() {
|
|
196
204
|
this._images = this._collectImages();
|
|
197
|
-
this.
|
|
205
|
+
if (!this._images.length) return;
|
|
206
|
+
|
|
207
|
+
this._resolveRatio();
|
|
208
|
+
this._render();
|
|
209
|
+
|
|
198
210
|
if (this._images.length > 1) {
|
|
199
211
|
this._setupListeners();
|
|
200
212
|
this._observeVisibility();
|
|
201
|
-
|
|
202
|
-
if (this.hasAttribute("autoplay") && this._images.length > 1) {
|
|
203
|
-
this._startAutoplay();
|
|
213
|
+
this._maybeStartAutoplay();
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
216
|
|
|
@@ -210,6 +220,23 @@ class ImageCarousel extends HTMLElement {
|
|
|
210
220
|
document.removeEventListener("visibilitychange", this._handleVisibility);
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
static get observedAttributes() {
|
|
224
|
+
return ["autoplay", "interval", "ratio"];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
228
|
+
if (oldValue === newValue || !this._images.length) return;
|
|
229
|
+
if (name === "autoplay") {
|
|
230
|
+
newValue !== null ? this._startAutoplay() : this._stopAutoplay();
|
|
231
|
+
} else if (name === "interval" && this._timer) {
|
|
232
|
+
this._startAutoplay();
|
|
233
|
+
} else if (name === "ratio") {
|
|
234
|
+
this._resolveRatio();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── content & layout ──────────────────────────────────────────────
|
|
239
|
+
|
|
213
240
|
_collectImages() {
|
|
214
241
|
return Array.from(this.querySelectorAll("img")).map((img) => ({
|
|
215
242
|
src: img.src || img.getAttribute("src"),
|
|
@@ -218,57 +245,53 @@ class ImageCarousel extends HTMLElement {
|
|
|
218
245
|
}));
|
|
219
246
|
}
|
|
220
247
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
_stopAutoplay() {
|
|
233
|
-
if (this._timer) {
|
|
234
|
-
clearInterval(this._timer);
|
|
235
|
-
this._timer = null;
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the carousel aspect ratio.
|
|
250
|
+
* Priority: explicit `ratio` attribute → first image's natural
|
|
251
|
+
* dimensions → fallback constant. The first image is probed via a
|
|
252
|
+
* detached Image() if the light-DOM <img> hasn't loaded yet.
|
|
253
|
+
*/
|
|
254
|
+
_resolveRatio() {
|
|
255
|
+
const explicit = this.getAttribute("ratio");
|
|
256
|
+
if (explicit) {
|
|
257
|
+
this.style.setProperty("--carousel-ratio", explicit);
|
|
258
|
+
return;
|
|
236
259
|
}
|
|
237
|
-
}
|
|
238
260
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this._currentIndex = index;
|
|
242
|
-
this._toggleSlide(prev, false);
|
|
243
|
-
this._toggleSlide(index, true);
|
|
244
|
-
}
|
|
261
|
+
// Set fallback first so the stage has dimensions while we wait.
|
|
262
|
+
this.style.setProperty("--carousel-ratio", FALLBACK_RATIO);
|
|
245
263
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const dot = this._dots[i];
|
|
249
|
-
if (dot) {
|
|
250
|
-
dot.classList.toggle("active", active);
|
|
251
|
-
dot.setAttribute("aria-selected", String(active));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
264
|
+
const first = this._images[0];
|
|
265
|
+
if (!first?.src) return;
|
|
254
266
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
const apply = (w, h) => {
|
|
268
|
+
if (w && h) this.style.setProperty("--carousel-ratio", `${w} / ${h}`);
|
|
269
|
+
};
|
|
258
270
|
|
|
259
|
-
if
|
|
260
|
-
|
|
271
|
+
// Reuse the light-DOM <img> if it's already decoded — avoids a
|
|
272
|
+
// second network request when the browser cached the image.
|
|
273
|
+
const lightImg = this.querySelector("img");
|
|
274
|
+
if (lightImg?.complete && lightImg.naturalWidth) {
|
|
275
|
+
apply(lightImg.naturalWidth, lightImg.naturalHeight);
|
|
261
276
|
return;
|
|
262
277
|
}
|
|
263
278
|
|
|
279
|
+
const probe = new Image();
|
|
280
|
+
probe.onload = () => apply(probe.naturalWidth, probe.naturalHeight);
|
|
281
|
+
if (first.srcset) probe.srcset = first.srcset;
|
|
282
|
+
probe.src = first.src;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_render() {
|
|
264
286
|
if (!_sheet) {
|
|
265
287
|
_sheet = new CSSStyleSheet();
|
|
266
288
|
_sheet.replaceSync(STYLES);
|
|
267
289
|
}
|
|
268
290
|
this.shadowRoot.adoptedStyleSheets = [_sheet];
|
|
269
|
-
this.style.setProperty("--carousel-ratio", ratio);
|
|
270
291
|
|
|
271
|
-
const
|
|
292
|
+
const multiSlide = this._images.length > 1;
|
|
293
|
+
|
|
294
|
+
const slidesHTML = this._images
|
|
272
295
|
.map(
|
|
273
296
|
(img, i) => `
|
|
274
297
|
<div class="slide${i === 0 ? " active" : ""}" role="tabpanel" aria-label="${img.alt || `Slide ${i + 1}`}">
|
|
@@ -281,18 +304,21 @@ class ImageCarousel extends HTMLElement {
|
|
|
281
304
|
)
|
|
282
305
|
.join("");
|
|
283
306
|
|
|
284
|
-
const navHTML =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
const navHTML = multiSlide
|
|
308
|
+
? `<button class="nav prev" aria-label="Previous slide">${CHEVRON_LEFT}</button>
|
|
309
|
+
<button class="nav next" aria-label="Next slide">${CHEVRON_RIGHT}</button>`
|
|
310
|
+
: "";
|
|
311
|
+
|
|
312
|
+
const dotsHTML = multiSlide
|
|
313
|
+
? `<div class="dots" role="tablist" aria-label="Slide navigation">
|
|
314
|
+
${this._images
|
|
315
|
+
.map(
|
|
316
|
+
(_img, i) =>
|
|
317
|
+
`<button class="dot${i === 0 ? " active" : ""}" data-index="${i}" role="tab" aria-label="Slide ${i + 1}" aria-selected="${i === 0}"></button>`,
|
|
318
|
+
)
|
|
319
|
+
.join("")}
|
|
320
|
+
</div>`
|
|
321
|
+
: "";
|
|
296
322
|
|
|
297
323
|
this.shadowRoot.innerHTML = `
|
|
298
324
|
<div class="carousel" role="region" aria-label="Image carousel" tabindex="0">
|
|
@@ -309,46 +335,89 @@ class ImageCarousel extends HTMLElement {
|
|
|
309
335
|
this._dots = Array.from(this.shadowRoot.querySelectorAll(".dot"));
|
|
310
336
|
}
|
|
311
337
|
|
|
338
|
+
// ─── navigation ────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
_next() {
|
|
341
|
+
this._goTo((this._currentIndex + 1) % this._images.length);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_prev() {
|
|
345
|
+
const n = this._images.length;
|
|
346
|
+
this._goTo((this._currentIndex - 1 + n) % n);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_goTo(index) {
|
|
350
|
+
const prev = this._currentIndex;
|
|
351
|
+
if (prev === index) return;
|
|
352
|
+
this._currentIndex = index;
|
|
353
|
+
this._toggleSlide(prev, false);
|
|
354
|
+
this._toggleSlide(index, true);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_toggleSlide(i, active) {
|
|
358
|
+
this._slides[i]?.classList.toggle("active", active);
|
|
359
|
+
const dot = this._dots[i];
|
|
360
|
+
if (dot) {
|
|
361
|
+
dot.classList.toggle("active", active);
|
|
362
|
+
dot.setAttribute("aria-selected", String(active));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** User triggered a navigation — restart the autoplay clock so the
|
|
367
|
+
* next auto-advance doesn't fire immediately after their action. */
|
|
368
|
+
_userNav(direction) {
|
|
369
|
+
direction === "next" ? this._next() : this._prev();
|
|
370
|
+
this._maybeStartAutoplay();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── autoplay ──────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
_maybeStartAutoplay() {
|
|
376
|
+
if (this.hasAttribute("autoplay") && this._images.length > 1) {
|
|
377
|
+
this._startAutoplay();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_startAutoplay() {
|
|
382
|
+
this._stopAutoplay();
|
|
383
|
+
const interval = parseInt(this.getAttribute("interval") || "", 10) || DEFAULT_INTERVAL;
|
|
384
|
+
this._timer = setInterval(() => this._next(), interval);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_stopAutoplay() {
|
|
388
|
+
if (this._timer) {
|
|
389
|
+
clearInterval(this._timer);
|
|
390
|
+
this._timer = null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── listeners ─────────────────────────────────────────────────────
|
|
395
|
+
|
|
312
396
|
_setupListeners() {
|
|
313
397
|
const root = this.shadowRoot;
|
|
314
398
|
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
399
|
|
|
322
|
-
root.querySelector(".
|
|
323
|
-
|
|
324
|
-
this._goTo((this._currentIndex + 1) % n);
|
|
325
|
-
});
|
|
400
|
+
root.querySelector(".prev")?.addEventListener("click", () => this._userNav("prev"));
|
|
401
|
+
root.querySelector(".next")?.addEventListener("click", () => this._userNav("next"));
|
|
326
402
|
|
|
327
403
|
root.querySelector(".dots")?.addEventListener("click", (e) => {
|
|
328
404
|
const dot = e.target.closest(".dot");
|
|
329
405
|
if (!dot) return;
|
|
330
|
-
this._resetAutoplay();
|
|
331
406
|
this._goTo(parseInt(dot.dataset.index, 10));
|
|
407
|
+
this._maybeStartAutoplay();
|
|
332
408
|
});
|
|
333
409
|
|
|
334
|
-
// Pause autoplay on
|
|
410
|
+
// Pause autoplay while the cursor sits on the carousel
|
|
335
411
|
carousel.addEventListener("mouseenter", () => this._stopAutoplay());
|
|
336
|
-
carousel.addEventListener("mouseleave", () =>
|
|
337
|
-
if (this.hasAttribute("autoplay")) this._startAutoplay();
|
|
338
|
-
});
|
|
412
|
+
carousel.addEventListener("mouseleave", () => this._maybeStartAutoplay());
|
|
339
413
|
|
|
340
414
|
// Keyboard navigation
|
|
341
415
|
carousel.addEventListener("keydown", (e) => {
|
|
342
|
-
if (e.key === "ArrowLeft")
|
|
343
|
-
|
|
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
|
-
}
|
|
416
|
+
if (e.key === "ArrowLeft") this._userNav("prev");
|
|
417
|
+
else if (e.key === "ArrowRight") this._userNav("next");
|
|
349
418
|
});
|
|
350
419
|
|
|
351
|
-
// Touch/swipe
|
|
420
|
+
// Touch / swipe
|
|
352
421
|
carousel.addEventListener(
|
|
353
422
|
"touchstart",
|
|
354
423
|
(e) => {
|
|
@@ -361,9 +430,8 @@ class ImageCarousel extends HTMLElement {
|
|
|
361
430
|
"touchend",
|
|
362
431
|
(e) => {
|
|
363
432
|
const dx = e.changedTouches[0].clientX - this._touchStartX;
|
|
364
|
-
if (Math.abs(dx) >
|
|
365
|
-
this.
|
|
366
|
-
this._goTo(dx < 0 ? (this._currentIndex + 1) % n : (this._currentIndex - 1 + n) % n);
|
|
433
|
+
if (Math.abs(dx) > SWIPE_THRESHOLD_PX) {
|
|
434
|
+
this._userNav(dx < 0 ? "next" : "prev");
|
|
367
435
|
}
|
|
368
436
|
},
|
|
369
437
|
{ passive: true },
|
|
@@ -373,36 +441,12 @@ class ImageCarousel extends HTMLElement {
|
|
|
373
441
|
_observeVisibility() {
|
|
374
442
|
this._observer = new IntersectionObserver(([entry]) => {
|
|
375
443
|
this._isVisible = entry.isIntersecting;
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
} else if (this.hasAttribute("autoplay")) {
|
|
379
|
-
this._startAutoplay();
|
|
380
|
-
}
|
|
444
|
+
if (entry.isIntersecting) this._maybeStartAutoplay();
|
|
445
|
+
else this._stopAutoplay();
|
|
381
446
|
});
|
|
382
447
|
this._observer.observe(this);
|
|
383
448
|
document.addEventListener("visibilitychange", this._handleVisibility);
|
|
384
449
|
}
|
|
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
450
|
}
|
|
407
451
|
|
|
408
452
|
customElements.define("image-carousel", ImageCarousel);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side Note Custom Element
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* <side-note>
|
|
6
|
+
* Extra context that should sit in the article margin on wide screens.
|
|
7
|
+
* </side-note>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let styleSheetInjected = false;
|
|
11
|
+
let layoutFrame = null;
|
|
12
|
+
let layoutEventsBound = false;
|
|
13
|
+
|
|
14
|
+
const boundImages = new WeakSet();
|
|
15
|
+
const SIDE_NOTE_MEDIA = "(min-width: 1280px)";
|
|
16
|
+
|
|
17
|
+
const STYLES = `
|
|
18
|
+
side-note {
|
|
19
|
+
display: block;
|
|
20
|
+
margin: 1.5rem 0;
|
|
21
|
+
padding: 0.4rem 0 0.4rem 1rem;
|
|
22
|
+
color: var(--subtext0);
|
|
23
|
+
font-size: 0.875em;
|
|
24
|
+
line-height: 1.55;
|
|
25
|
+
border-left: 2px solid var(--surface1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
side-note > :first-child {
|
|
29
|
+
margin-top: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
side-note > :last-child {
|
|
33
|
+
margin-bottom: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media screen and (min-width: 1280px) {
|
|
37
|
+
.card-content.article > .content {
|
|
38
|
+
overflow: visible;
|
|
39
|
+
display: flow-root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.card-content.article > .content > side-note {
|
|
43
|
+
float: right;
|
|
44
|
+
clear: right;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
width: 11rem;
|
|
47
|
+
margin: 0 -12.5rem 1rem 1.5rem;
|
|
48
|
+
padding: 0 0 0 0.875rem;
|
|
49
|
+
font-size: 0.8em;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.card-content.article > .content.is-side-note-layout {
|
|
53
|
+
position: relative;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.card-content.article > .content.is-side-note-layout > side-note {
|
|
57
|
+
float: none;
|
|
58
|
+
clear: none;
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: var(--side-note-top, 0);
|
|
61
|
+
right: -12.5rem;
|
|
62
|
+
margin: 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
function injectStyles() {
|
|
68
|
+
if (styleSheetInjected) return;
|
|
69
|
+
|
|
70
|
+
const styleEl = document.createElement("style");
|
|
71
|
+
styleEl.textContent = STYLES;
|
|
72
|
+
document.head.appendChild(styleEl);
|
|
73
|
+
styleSheetInjected = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getArticleContentBlocks() {
|
|
77
|
+
return document.querySelectorAll(".card-content.article > .content");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getSideNotes(content) {
|
|
81
|
+
return Array.from(content.children).filter((child) => child.tagName === "SIDE-NOTE");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resetSideNoteLayout(content, sideNotes = getSideNotes(content)) {
|
|
85
|
+
content.classList.remove("is-side-note-layout");
|
|
86
|
+
content.style.removeProperty("min-height");
|
|
87
|
+
sideNotes.forEach((sideNote) => {
|
|
88
|
+
sideNote.style.removeProperty("--side-note-top");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getSideNoteAnchor(sideNote) {
|
|
93
|
+
let anchor = sideNote.previousElementSibling;
|
|
94
|
+
|
|
95
|
+
while (anchor && anchor.tagName === "SIDE-NOTE") {
|
|
96
|
+
anchor = anchor.previousElementSibling;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return anchor || sideNote;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getContentFlowBottom(content, sideNotes) {
|
|
103
|
+
const sideNoteSet = new Set(sideNotes);
|
|
104
|
+
let bottom = 0;
|
|
105
|
+
|
|
106
|
+
Array.from(content.children).forEach((child) => {
|
|
107
|
+
if (sideNoteSet.has(child)) return;
|
|
108
|
+
bottom = Math.max(bottom, child.offsetTop + child.offsetHeight);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return bottom;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function layoutSideNotes() {
|
|
115
|
+
const isWide = window.matchMedia(SIDE_NOTE_MEDIA).matches;
|
|
116
|
+
|
|
117
|
+
getArticleContentBlocks().forEach((content) => {
|
|
118
|
+
const sideNotes = getSideNotes(content);
|
|
119
|
+
if (sideNotes.length === 0) return;
|
|
120
|
+
|
|
121
|
+
if (!isWide) {
|
|
122
|
+
resetSideNoteLayout(content, sideNotes);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
content.style.removeProperty("min-height");
|
|
127
|
+
sideNotes.forEach((sideNote) => {
|
|
128
|
+
sideNote.style.removeProperty("--side-note-top");
|
|
129
|
+
});
|
|
130
|
+
content.classList.add("is-side-note-layout");
|
|
131
|
+
|
|
132
|
+
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
133
|
+
const gap = rootFontSize;
|
|
134
|
+
let nextTop = 0;
|
|
135
|
+
|
|
136
|
+
sideNotes.forEach((sideNote) => {
|
|
137
|
+
const anchor = getSideNoteAnchor(sideNote);
|
|
138
|
+
const targetTop = anchor.offsetTop;
|
|
139
|
+
const top = Math.max(targetTop, nextTop);
|
|
140
|
+
|
|
141
|
+
sideNote.style.setProperty("--side-note-top", `${Math.round(top)}px`);
|
|
142
|
+
nextTop = top + sideNote.offsetHeight + gap;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const flowBottom = getContentFlowBottom(content, sideNotes);
|
|
146
|
+
const sideNoteBottom = Math.max(0, nextTop - gap);
|
|
147
|
+
content.style.minHeight = `${Math.ceil(Math.max(flowBottom, sideNoteBottom))}px`;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scheduleSideNoteLayout() {
|
|
152
|
+
if (layoutFrame !== null) return;
|
|
153
|
+
|
|
154
|
+
layoutFrame = window.requestAnimationFrame(() => {
|
|
155
|
+
layoutFrame = null;
|
|
156
|
+
layoutSideNotes();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function bindImages(content) {
|
|
161
|
+
if (!content) return;
|
|
162
|
+
|
|
163
|
+
content.querySelectorAll("img").forEach((img) => {
|
|
164
|
+
if (boundImages.has(img)) return;
|
|
165
|
+
boundImages.add(img);
|
|
166
|
+
img.addEventListener("load", scheduleSideNoteLayout);
|
|
167
|
+
img.addEventListener("error", scheduleSideNoteLayout);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function bindLayoutEvents() {
|
|
172
|
+
if (layoutEventsBound) return;
|
|
173
|
+
layoutEventsBound = true;
|
|
174
|
+
|
|
175
|
+
window.addEventListener("resize", scheduleSideNoteLayout, { passive: true });
|
|
176
|
+
window.addEventListener("gnix:article-font-settings-change", scheduleSideNoteLayout);
|
|
177
|
+
|
|
178
|
+
if (document.fonts?.ready) {
|
|
179
|
+
document.fonts.ready.then(scheduleSideNoteLayout).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class SideNote extends HTMLElement {
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
injectStyles();
|
|
186
|
+
this.unwrapMarkdownParagraph();
|
|
187
|
+
bindLayoutEvents();
|
|
188
|
+
bindImages(this.closest(".content"));
|
|
189
|
+
scheduleSideNoteLayout();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
unwrapMarkdownParagraph() {
|
|
193
|
+
const parent = this.parentElement;
|
|
194
|
+
if (!parent || parent.tagName !== "P") return;
|
|
195
|
+
|
|
196
|
+
const onlyContainsThisSideNote = Array.from(parent.childNodes).every((node) => {
|
|
197
|
+
return node === this || (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === "");
|
|
198
|
+
});
|
|
199
|
+
if (!onlyContainsThisSideNote) return;
|
|
200
|
+
|
|
201
|
+
parent.parentNode.insertBefore(this, parent);
|
|
202
|
+
parent.remove();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!customElements.get("side-note")) {
|
|
207
|
+
customElements.define("side-note", SideNote);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { SideNote };
|