@michaelyagi/shoji 0.1.0-alpha.6 → 0.1.0-alpha.7
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 +5 -3
- package/dist/esm/core/Gallery.d.ts +3 -3
- package/dist/esm/core/GestureController.d.ts +7 -0
- package/dist/esm/core/SlideManager.d.ts +13 -18
- package/dist/esm/core/index.js +136 -46
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/core/scan.d.ts +11 -1
- package/dist/esm/core/types.d.ts +24 -2
- package/dist/esm/index.css +7 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/plugins/activeThumbnail/index.js +20 -5
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -1
- package/dist/esm/plugins/video/index.js +8 -0
- package/dist/esm/plugins/video/index.js.map +1 -1
- package/dist/shoji.css +7 -0
- package/dist/shoji.js +166 -53
- package/dist/shoji.js.map +1 -1
- package/dist/shoji.min.js +1 -1
- package/dist/shoji.min.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Shoji
|
|
2
2
|
|
|
3
3
|
[](https://github.com/MichaelYagi/shoji/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@michaelyagi/shoji)
|
|
4
5
|
|
|
5
6
|
A zero-dependency, plugin-first TypeScript lightbox/gallery. Drop in two `<script>`/`<link>` tags, or install from npm — everything beyond the minimal lightbox, including every official plugin, is opt-in.
|
|
6
7
|
|
|
@@ -27,12 +28,12 @@ A zero-dependency, plugin-first TypeScript lightbox/gallery. Drop in two `<scrip
|
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
npm install shoji
|
|
31
|
+
npm install @michaelyagi/shoji@alpha
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
```js
|
|
34
|
-
import Shoji from 'shoji';
|
|
35
|
-
import 'shoji/style.css';
|
|
35
|
+
import Shoji from '@michaelyagi/shoji';
|
|
36
|
+
import '@michaelyagi/shoji/style.css';
|
|
36
37
|
|
|
37
38
|
new Shoji('#gallery', { plugins: [Shoji.Zoom] });
|
|
38
39
|
```
|
|
@@ -55,6 +56,7 @@ No items array, no options object, no plugins — every default is chosen so thi
|
|
|
55
56
|
## Docs
|
|
56
57
|
|
|
57
58
|
- **[michaelyagi.github.io/shoji](https://michaelyagi.github.io/shoji)** — the published docs site, auto-deployed from `main` (see `.github/workflows/ci.yml`'s `publish-docs` job).
|
|
59
|
+
- **[npmjs.com/package/@michaelyagi/shoji](https://www.npmjs.com/package/@michaelyagi/shoji)** — the published package, auto-published on tagged releases (see `.github/workflows/ci.yml`'s `publish-npm` job).
|
|
58
60
|
- [`docs/index.html`](docs/index.html) — the same guides/examples/API reference, as local static HTML (open directly, or run `npm run docs` first to regenerate the API reference from source).
|
|
59
61
|
- [`docs/examples/`](docs/examples/) — real, runnable, self-contained example pages (copy one wholesale into your own project).
|
|
60
62
|
- [`DESIGN.md`](DESIGN.md) — the living architecture/spec document this project is built from.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Unsubscribe } from './EventBus';
|
|
2
|
-
import { GalleryEvents, GalleryItem, GalleryOptions } from './types';
|
|
2
|
+
import { GalleryEvents, GalleryItem, GalleryItemInput, GalleryOptions } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Core lifecycle (DESIGN.md §2.2), item model / DOM scanning (§2.1), and the
|
|
5
5
|
* lightbox itself: pooled slides (§2.3), dialog semantics/focus trap/live
|
|
@@ -183,7 +183,7 @@ export declare class Gallery {
|
|
|
183
183
|
*/
|
|
184
184
|
private finishClose;
|
|
185
185
|
/** DESIGN.md §2.1 — diffs by id (fallback src), preserving the active slide. */
|
|
186
|
-
updateSlides(items:
|
|
186
|
+
updateSlides(items: GalleryItemInput[], currentIndex?: number): void;
|
|
187
187
|
/**
|
|
188
188
|
* Sugar over `updateSlides()` for the common "insert some items" case —
|
|
189
189
|
* splices `items` into a copy of the current list at `atIndex` (default:
|
|
@@ -193,7 +193,7 @@ export declare class Gallery {
|
|
|
193
193
|
* own semantics (negative counts from the end, out-of-range clamps) —
|
|
194
194
|
* no extra validation on top of that.
|
|
195
195
|
*/
|
|
196
|
-
addSlides(items:
|
|
196
|
+
addSlides(items: GalleryItemInput[], atIndex?: number): void;
|
|
197
197
|
/**
|
|
198
198
|
* Sugar over `updateSlides()` for the common "remove some items" case.
|
|
199
199
|
* Accepts a single id/index or an array mixing both: a `string` matches
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { GestureEngineOptions } from '../gestures/GestureEngine';
|
|
2
2
|
import { SlideManager } from './SlideManager';
|
|
3
|
+
/**
|
|
4
|
+
* A real interactive control. Also used by `Gallery.ts`'s `isBackdropClick`,
|
|
5
|
+
* which used to have its own narrower list (missing select/input/textarea/
|
|
6
|
+
* a[href]) — a plugin-mounted non-button control got misread as a backdrop
|
|
7
|
+
* click and closed the gallery. One shared selector so that can't recur.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
3
10
|
/** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */
|
|
4
11
|
export interface GestureControllerHost {
|
|
5
12
|
dialog: HTMLElement;
|
|
@@ -7,11 +7,14 @@ export interface SlideManagerOptions {
|
|
|
7
7
|
videoProviders: Map<string, VideoProviderRenderer>;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* DESIGN.md §2.3 — only `currentIndex ± preload` exist in the DOM.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* DESIGN.md §2.3 — only `currentIndex ± preload` exist in the DOM. Content
|
|
11
|
+
* already resident and ready is never moved between slots — the slot that
|
|
12
|
+
* already holds it just gets its own `offset` relabeled to its new pool
|
|
13
|
+
* position; only content resident nowhere yet gets built fresh into
|
|
14
|
+
* whatever's left over. O(preload) re-render cost either way, but never a
|
|
15
|
+
* DOM reparent — which matters for a provider video's `<iframe>` (§4.3):
|
|
16
|
+
* most browsers reload an iframe moved to a new parent, so a live embed can
|
|
17
|
+
* only safely change position by relabeling, never by being moved.
|
|
15
18
|
*/
|
|
16
19
|
export declare class SlideManager {
|
|
17
20
|
readonly element: HTMLElement;
|
|
@@ -20,15 +23,7 @@ export declare class SlideManager {
|
|
|
20
23
|
private readonly playVideoLabel;
|
|
21
24
|
private readonly videoProviders;
|
|
22
25
|
private dragOffsetPx;
|
|
23
|
-
/**
|
|
24
|
-
* Ready nodes keyed by item index, not slot offset — a `Slot` only
|
|
25
|
-
* remembers its *own* previous index, so it can't tell that some *other*
|
|
26
|
-
* slot already decoded the exact index it's now asked to show (the
|
|
27
|
-
* routine case: stepping forward moves the +1 slot's content into the 0
|
|
28
|
-
* slot). Without this, already-decoded content is thrown away and
|
|
29
|
-
* redecoded on every step. Trimmed to `centerIndex ± preload` each
|
|
30
|
-
* `render()`, same window the slots cover.
|
|
31
|
-
*/
|
|
26
|
+
/** Ready nodes keyed by item index — trimmed to `centerIndex ± preload` each `render()`, same window the slots cover, so an evicted entry's video/iframe resources get released even if no slot ever reclaims it. */
|
|
32
27
|
private readonly cache;
|
|
33
28
|
/**
|
|
34
29
|
* Image decodes in flight, keyed by item index, not by whichever slot
|
|
@@ -63,10 +58,10 @@ export declare class SlideManager {
|
|
|
63
58
|
/** Releases the slot's old content and swaps the new node in, caching it. `extra` rides as a second child. `ensureImageDecoding` goes through `moveIn` instead. */
|
|
64
59
|
private swapIn;
|
|
65
60
|
/**
|
|
66
|
-
*
|
|
67
|
-
* unlike `swapIn`:
|
|
68
|
-
*
|
|
69
|
-
*
|
|
61
|
+
* Inserts a just-decoded node into the slot waiting on it — skips
|
|
62
|
+
* `releaseVideo`, unlike `swapIn`: this is its first insertion anywhere,
|
|
63
|
+
* nothing outgoing to release. `render()`'s claim pass (Phase 1) never
|
|
64
|
+
* calls this — it reuses `ready` content in place, no DOM touch at all.
|
|
70
65
|
*/
|
|
71
66
|
private moveIn;
|
|
72
67
|
/** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */
|
package/dist/esm/core/index.js
CHANGED
|
@@ -162,7 +162,7 @@ class FocusTrap {
|
|
|
162
162
|
var _a;
|
|
163
163
|
document.removeEventListener("keydown", this.onKeydown, true);
|
|
164
164
|
this.container = null;
|
|
165
|
-
(_a = this.previouslyFocused) == null ? void 0 : _a.focus();
|
|
165
|
+
(_a = this.previouslyFocused) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
166
166
|
this.previouslyFocused = null;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
@@ -335,10 +335,9 @@ function centerOf(a, b) {
|
|
|
335
335
|
const DRAG_SETTLE_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
336
336
|
const DRAG_FEEDBACK_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing), opacity var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
337
337
|
const VERTICAL_FEEDBACK_DISTANCE = 160;
|
|
338
|
+
const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
338
339
|
function shouldIgnoreGesture(event) {
|
|
339
|
-
return event.composedPath().some(
|
|
340
|
-
(node) => node instanceof Element && node.matches("button, video, input, select, textarea, a[href], [data-shoji-no-drag]")
|
|
341
|
-
);
|
|
340
|
+
return event.composedPath().some((node) => node instanceof Element && node.matches(INTERACTIVE_CONTROL_SELECTOR));
|
|
342
341
|
}
|
|
343
342
|
class GestureController {
|
|
344
343
|
constructor(host, relay, options) {
|
|
@@ -464,6 +463,7 @@ function numAttr(el, name) {
|
|
|
464
463
|
const n = Number(raw);
|
|
465
464
|
return Number.isFinite(n) ? n : void 0;
|
|
466
465
|
}
|
|
466
|
+
const NO_OWN_DATA_FIELD = /* @__PURE__ */ new Set(["no-drag", "video-id", "video-provider"]);
|
|
467
467
|
function applyCommon(element, item, src) {
|
|
468
468
|
const caption = attr(element, "data-shoji-caption");
|
|
469
469
|
if (caption) item.caption = caption;
|
|
@@ -472,7 +472,7 @@ function applyCommon(element, item, src) {
|
|
|
472
472
|
for (const a of element.attributes) {
|
|
473
473
|
if (!a.name.startsWith("data-shoji-")) continue;
|
|
474
474
|
const key = a.name.slice(11);
|
|
475
|
-
if (key
|
|
475
|
+
if (!NO_OWN_DATA_FIELD.has(key) && !(key in item)) (data ?? (data = {}))[key] = a.value;
|
|
476
476
|
}
|
|
477
477
|
if (data) item.data = data;
|
|
478
478
|
}
|
|
@@ -498,8 +498,14 @@ function scanImage(element) {
|
|
|
498
498
|
}
|
|
499
499
|
function scanVideo(element) {
|
|
500
500
|
var _a;
|
|
501
|
+
const videoIdAttr = attr(element, "data-shoji-video-id");
|
|
501
502
|
const videoEl = element.querySelector("video");
|
|
502
503
|
if (videoEl) {
|
|
504
|
+
if (videoIdAttr) {
|
|
505
|
+
console.warn(
|
|
506
|
+
`Shoji: data-shoji-video-id="${videoIdAttr}" ignored — no effect on a nested <video> (always html5).`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
503
509
|
const sources = Array.from(videoEl.querySelectorAll("source")).map((s) => ({ src: s.getAttribute("src") ?? "", type: s.getAttribute("type") ?? "" })).filter((s) => s.src);
|
|
504
510
|
const src = videoEl.getAttribute("src") ?? ((_a = sources[0]) == null ? void 0 : _a.src);
|
|
505
511
|
if (!src) return void 0;
|
|
@@ -512,19 +518,75 @@ function scanVideo(element) {
|
|
|
512
518
|
}
|
|
513
519
|
const videoUrl = attr(element, "data-shoji-video");
|
|
514
520
|
if (videoUrl) {
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
sources: [{ src: videoUrl, type: guessVideoType(videoUrl) }]
|
|
520
|
-
};
|
|
521
|
+
const providerAttr = attr(element, "data-shoji-video-provider");
|
|
522
|
+
const resolved = resolveExplicitVideo(videoUrl, providerAttr, videoIdAttr);
|
|
523
|
+
const item = { src: videoUrl, video: resolved.video };
|
|
524
|
+
if (resolved.sources) item.sources = resolved.sources;
|
|
521
525
|
const poster = attr(element, "data-shoji-poster");
|
|
522
526
|
if (poster) item.poster = poster;
|
|
523
527
|
applyCommon(element, item, videoUrl);
|
|
524
528
|
return item;
|
|
525
529
|
}
|
|
530
|
+
if (videoIdAttr) {
|
|
531
|
+
console.warn(
|
|
532
|
+
`Shoji: data-shoji-video-id="${videoIdAttr}" ignored — requires a data-shoji-video URL.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
526
535
|
return void 0;
|
|
527
536
|
}
|
|
537
|
+
const KNOWN_VIDEO_PROVIDERS = /* @__PURE__ */ new Set(["youtube", "vimeo", "wistia", "html5"]);
|
|
538
|
+
function html5Fallback(url) {
|
|
539
|
+
return { video: { provider: "html5" }, sources: [{ src: url, type: guessVideoType(url) }] };
|
|
540
|
+
}
|
|
541
|
+
function resolveExplicitVideo(url, providerAttr, idAttr) {
|
|
542
|
+
if (providerAttr && !KNOWN_VIDEO_PROVIDERS.has(providerAttr)) {
|
|
543
|
+
console.warn(`Shoji: data-shoji-video-provider="${providerAttr}" isn't recognized — ignored.`);
|
|
544
|
+
providerAttr = void 0;
|
|
545
|
+
}
|
|
546
|
+
if (providerAttr === "html5") {
|
|
547
|
+
if (idAttr)
|
|
548
|
+
console.warn(`Shoji: data-shoji-video-id="${idAttr}" ignored — provider html5 has no id.`);
|
|
549
|
+
return html5Fallback(url);
|
|
550
|
+
}
|
|
551
|
+
if (providerAttr === "vimeo" || providerAttr === "wistia") {
|
|
552
|
+
if (!idAttr) {
|
|
553
|
+
console.warn(
|
|
554
|
+
`Shoji: data-shoji-video-provider="${providerAttr}" needs a data-shoji-video-id — falling back to html5.`
|
|
555
|
+
);
|
|
556
|
+
return html5Fallback(url);
|
|
557
|
+
}
|
|
558
|
+
return { video: { provider: providerAttr, id: idAttr, url } };
|
|
559
|
+
}
|
|
560
|
+
const explicitYouTube = providerAttr === "youtube";
|
|
561
|
+
const isYouTube = explicitYouTube || isYouTubeUrl(url);
|
|
562
|
+
if (!isYouTube) {
|
|
563
|
+
if (idAttr) {
|
|
564
|
+
console.warn(
|
|
565
|
+
`Shoji: data-shoji-video-id="${idAttr}" ignored — data-shoji-video="${url}" isn't a recognized YouTube URL; falling back to html5.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return html5Fallback(url);
|
|
569
|
+
}
|
|
570
|
+
const id = idAttr ?? detectYouTubeId(url);
|
|
571
|
+
if (!id) {
|
|
572
|
+
if (!explicitYouTube) return html5Fallback(url);
|
|
573
|
+
console.warn(
|
|
574
|
+
`Shoji: data-shoji-video-provider="youtube" but no id could be parsed from "${url}".`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return { video: { provider: "youtube", id, url } };
|
|
578
|
+
}
|
|
579
|
+
const YOUTUBE_HOSTS = /* @__PURE__ */ new Set(["youtu.be", "youtube.com", "youtube-nocookie.com"]);
|
|
580
|
+
function normalizeVideoHost(hostname) {
|
|
581
|
+
return hostname.replace(/^(www|m|music)\./, "");
|
|
582
|
+
}
|
|
583
|
+
function isYouTubeUrl(url) {
|
|
584
|
+
try {
|
|
585
|
+
return YOUTUBE_HOSTS.has(normalizeVideoHost(new URL(url, window.location.href).hostname));
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
528
590
|
function detectYouTubeId(url) {
|
|
529
591
|
let parsed;
|
|
530
592
|
try {
|
|
@@ -532,13 +594,37 @@ function detectYouTubeId(url) {
|
|
|
532
594
|
} catch {
|
|
533
595
|
return void 0;
|
|
534
596
|
}
|
|
535
|
-
const host = parsed.hostname
|
|
597
|
+
const host = normalizeVideoHost(parsed.hostname);
|
|
536
598
|
if (host === "youtu.be") return parsed.pathname.slice(1).split("/")[0] || void 0;
|
|
537
|
-
if (host
|
|
599
|
+
if (!YOUTUBE_HOSTS.has(host)) return void 0;
|
|
538
600
|
if (parsed.pathname === "/watch") return parsed.searchParams.get("v") ?? void 0;
|
|
539
|
-
const match = parsed.pathname.match(/^\/(?:embed|shorts)\/([^/?]+)/);
|
|
601
|
+
const match = parsed.pathname.match(/^\/(?:embed|shorts|live|v)\/([^/?]+)/);
|
|
540
602
|
return match == null ? void 0 : match[1];
|
|
541
603
|
}
|
|
604
|
+
function resolveDynamicVideoItems(items) {
|
|
605
|
+
return items.map((item) => {
|
|
606
|
+
var _a;
|
|
607
|
+
if (item.video === true) {
|
|
608
|
+
const video = isYouTubeUrl(item.src) ? { provider: "youtube", id: detectYouTubeId(item.src) } : { provider: "html5" };
|
|
609
|
+
if (video.provider === "youtube" && !video.id) {
|
|
610
|
+
console.warn(
|
|
611
|
+
`Shoji: item "${item.id ?? item.src}" has video: true, but no id could be parsed from src ("${item.src}") — the embed won't load. Use an explicit video.id instead.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return { ...item, video };
|
|
615
|
+
}
|
|
616
|
+
const resolved = item;
|
|
617
|
+
if (((_a = resolved.video) == null ? void 0 : _a.provider) !== "youtube" || resolved.video.id) return resolved;
|
|
618
|
+
const id = detectYouTubeId(resolved.src);
|
|
619
|
+
if (!id) {
|
|
620
|
+
console.warn(
|
|
621
|
+
`Shoji: item "${resolved.id ?? resolved.src}" has video.provider: 'youtube' with no id, and src ("${resolved.src}") isn't a recognized YouTube URL — the embed won't load. Set video.id explicitly.`
|
|
622
|
+
);
|
|
623
|
+
return resolved;
|
|
624
|
+
}
|
|
625
|
+
return { ...resolved, video: { ...resolved.video, id } };
|
|
626
|
+
});
|
|
627
|
+
}
|
|
542
628
|
function guessVideoType(url) {
|
|
543
629
|
const clean = url.split(/[?#]/)[0] ?? url;
|
|
544
630
|
const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
|
|
@@ -555,15 +641,7 @@ class SlideManager {
|
|
|
555
641
|
__publicField(this, "playVideoLabel");
|
|
556
642
|
__publicField(this, "videoProviders");
|
|
557
643
|
__publicField(this, "dragOffsetPx", 0);
|
|
558
|
-
/**
|
|
559
|
-
* Ready nodes keyed by item index, not slot offset — a `Slot` only
|
|
560
|
-
* remembers its *own* previous index, so it can't tell that some *other*
|
|
561
|
-
* slot already decoded the exact index it's now asked to show (the
|
|
562
|
-
* routine case: stepping forward moves the +1 slot's content into the 0
|
|
563
|
-
* slot). Without this, already-decoded content is thrown away and
|
|
564
|
-
* redecoded on every step. Trimmed to `centerIndex ± preload` each
|
|
565
|
-
* `render()`, same window the slots cover.
|
|
566
|
-
*/
|
|
644
|
+
/** Ready nodes keyed by item index — trimmed to `centerIndex ± preload` each `render()`, same window the slots cover, so an evicted entry's video/iframe resources get released even if no slot ever reclaims it. */
|
|
567
645
|
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
568
646
|
/**
|
|
569
647
|
* Image decodes in flight, keyed by item index, not by whichever slot
|
|
@@ -628,36 +706,45 @@ class SlideManager {
|
|
|
628
706
|
}
|
|
629
707
|
/** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready. */
|
|
630
708
|
render(items, centerIndex, onLoad, openPlaceholderSrc) {
|
|
631
|
-
var _a;
|
|
632
709
|
for (const [index, entry] of this.cache) {
|
|
633
710
|
if (index < centerIndex - this.preload || index > centerIndex + this.preload) {
|
|
634
711
|
releaseVideoNode(entry.node);
|
|
635
712
|
this.cache.delete(index);
|
|
636
713
|
}
|
|
637
714
|
}
|
|
638
|
-
const targets = this.slots.map((
|
|
639
|
-
const
|
|
715
|
+
const targets = this.slots.map((_, i) => {
|
|
716
|
+
const offset = i - this.preload;
|
|
717
|
+
const index = centerIndex + offset;
|
|
640
718
|
const item = index >= 0 && index < items.length ? items[index] : void 0;
|
|
641
|
-
return {
|
|
719
|
+
return { offset, index, item };
|
|
642
720
|
});
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (!
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
721
|
+
const claimedIndices = /* @__PURE__ */ new Set();
|
|
722
|
+
const claimedSlots = /* @__PURE__ */ new Set();
|
|
723
|
+
for (const target of targets) {
|
|
724
|
+
if (!target.item) continue;
|
|
725
|
+
const owner = this.slots.find((s) => s.assignedIndex === target.index && s.ready);
|
|
726
|
+
if (!owner) continue;
|
|
727
|
+
claimedIndices.add(target.index);
|
|
728
|
+
claimedSlots.add(owner);
|
|
729
|
+
if (owner.offset === target.offset) continue;
|
|
730
|
+
owner.offset = target.offset;
|
|
731
|
+
onLoad(target.index);
|
|
651
732
|
}
|
|
652
|
-
|
|
733
|
+
const freeSlots = this.slots.filter((s) => !claimedSlots.has(s));
|
|
734
|
+
const freeTargets = targets.filter((t) => !t.item || !claimedIndices.has(t.index));
|
|
735
|
+
freeTargets.forEach((target, i) => {
|
|
736
|
+
var _a;
|
|
737
|
+
const slot = freeSlots[i];
|
|
738
|
+
slot.offset = target.offset;
|
|
739
|
+
const { index, item } = target;
|
|
653
740
|
if (!item) {
|
|
654
741
|
slot.assignedIndex = null;
|
|
655
742
|
slot.ready = false;
|
|
656
743
|
releaseVideo(slot.media);
|
|
657
744
|
slot.media.replaceChildren();
|
|
658
|
-
|
|
745
|
+
return;
|
|
659
746
|
}
|
|
660
|
-
if (slot.assignedIndex === index)
|
|
747
|
+
if (slot.assignedIndex === index) return;
|
|
661
748
|
slot.assignedIndex = index;
|
|
662
749
|
slot.ready = false;
|
|
663
750
|
releaseVideo(slot.media);
|
|
@@ -672,7 +759,8 @@ class SlideManager {
|
|
|
672
759
|
} else {
|
|
673
760
|
this.ensureImageDecoding(item, index, onLoad);
|
|
674
761
|
}
|
|
675
|
-
}
|
|
762
|
+
});
|
|
763
|
+
this.applyTransforms(null);
|
|
676
764
|
}
|
|
677
765
|
applyAspect(slot, item) {
|
|
678
766
|
if (item.width && item.height) {
|
|
@@ -691,10 +779,10 @@ class SlideManager {
|
|
|
691
779
|
this.cache.set(index, { node, item, extra });
|
|
692
780
|
}
|
|
693
781
|
/**
|
|
694
|
-
*
|
|
695
|
-
* unlike `swapIn`:
|
|
696
|
-
*
|
|
697
|
-
*
|
|
782
|
+
* Inserts a just-decoded node into the slot waiting on it — skips
|
|
783
|
+
* `releaseVideo`, unlike `swapIn`: this is its first insertion anywhere,
|
|
784
|
+
* nothing outgoing to release. `render()`'s claim pass (Phase 1) never
|
|
785
|
+
* calls this — it reuses `ready` content in place, no DOM touch at all.
|
|
698
786
|
*/
|
|
699
787
|
moveIn(slot, entry, index) {
|
|
700
788
|
this.applyAspect(slot, entry.item);
|
|
@@ -1172,7 +1260,9 @@ const DEFAULT_LOCALE = {
|
|
|
1172
1260
|
};
|
|
1173
1261
|
function isBackdropClick(event) {
|
|
1174
1262
|
return !event.composedPath().some(
|
|
1175
|
-
(node) => node instanceof Element && node.matches(
|
|
1263
|
+
(node) => node instanceof Element && node.matches(
|
|
1264
|
+
`.shoji-slide-img, .shoji-counter, .shoji-caption, ${INTERACTIVE_CONTROL_SELECTOR}`
|
|
1265
|
+
)
|
|
1176
1266
|
);
|
|
1177
1267
|
}
|
|
1178
1268
|
function isDangerousHtmlCaption(value) {
|
|
@@ -1297,7 +1387,7 @@ class Gallery {
|
|
|
1297
1387
|
this.activeIndex = 0;
|
|
1298
1388
|
this.scannedElements = [];
|
|
1299
1389
|
if (this.isDynamicMode) {
|
|
1300
|
-
this.itemList = options.items ?? [];
|
|
1390
|
+
this.itemList = resolveDynamicVideoItems(options.items ?? []);
|
|
1301
1391
|
} else {
|
|
1302
1392
|
const scanned = scanContainer(this.element, this.selector);
|
|
1303
1393
|
this.itemList = scanned.map((s) => s.item);
|
|
@@ -1855,7 +1945,7 @@ class Gallery {
|
|
|
1855
1945
|
if (this.destroyed) return;
|
|
1856
1946
|
const activeItem = this.itemList[this.activeIndex];
|
|
1857
1947
|
const activeKey = activeItem ? itemKey(activeItem) : void 0;
|
|
1858
|
-
this.itemList = items;
|
|
1948
|
+
this.itemList = resolveDynamicVideoItems(items);
|
|
1859
1949
|
let nextIndex = currentIndex;
|
|
1860
1950
|
if (nextIndex === void 0 && activeKey !== void 0) {
|
|
1861
1951
|
const preserved = items.findIndex((item) => itemKey(item) === activeKey);
|