@michaelyagi/shoji 0.1.0-alpha.11 → 0.1.0-alpha.13
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/dist/esm/core/Gallery.d.ts +60 -2
- package/dist/esm/core/GestureController.d.ts +42 -6
- package/dist/esm/core/dom.d.ts +1 -0
- package/dist/esm/core/index.js +237 -25
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/core/types.d.ts +23 -3
- package/dist/esm/core/zoomTransition.d.ts +26 -5
- package/dist/esm/gestures/GestureEngine.d.ts +11 -0
- package/dist/esm/index.css +46 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index2.css +14 -0
- package/dist/esm/plugins/autoplay/index.js +11 -0
- package/dist/esm/plugins/autoplay/index.js.map +1 -1
- package/dist/esm/plugins/rotateFlip/index.js +1 -1
- package/dist/esm/plugins/zoom/index.js +14 -2
- package/dist/esm/plugins/zoom/index.js.map +1 -1
- package/dist/esm/{zoomTransition-2_HG7tyJ.js → zoomTransition-BeIimqDT.js} +11 -5
- package/dist/esm/zoomTransition-BeIimqDT.js.map +1 -0
- package/dist/shoji.css +60 -2
- package/dist/shoji.js +271 -30
- package/dist/shoji.js.map +1 -1
- package/dist/shoji.min.css +1 -1
- package/dist/shoji.min.js +1 -1
- package/dist/shoji.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/esm/zoomTransition-2_HG7tyJ.js.map +0 -1
|
@@ -30,6 +30,8 @@ export declare class Gallery {
|
|
|
30
30
|
private autoHidden;
|
|
31
31
|
private hoveredControlCount;
|
|
32
32
|
private isClosing;
|
|
33
|
+
/** True while a vertical drag has hidden controls past its own distance threshold (`setControlsHiddenForDrag`) — `onActivity()` defers to it, since the drag's own continuous pointermove stream would otherwise immediately re-reveal what it just hid on every single move. */
|
|
34
|
+
private controlsHiddenByDrag;
|
|
33
35
|
private itemList;
|
|
34
36
|
private scannedElements;
|
|
35
37
|
private activeIndex;
|
|
@@ -48,7 +50,18 @@ export declare class Gallery {
|
|
|
48
50
|
private pluginCleanups;
|
|
49
51
|
private readonly onContainerClick;
|
|
50
52
|
private readonly onOuterClick;
|
|
51
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle
|
|
55
|
+
* clock. `autoHideDelay: 0` = "never show controls" — a no-op here.
|
|
56
|
+
* `autoHideDelay: false` = "always visible" — also a no-op: nothing to
|
|
57
|
+
* reveal (already shown) or reschedule (no timer ever runs). Also a no-op
|
|
58
|
+
* once `isClosing` — a real bug, reported from real usage: moving the
|
|
59
|
+
* mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)
|
|
60
|
+
* re-showed the just-hidden controls mid-animation, since these listeners
|
|
61
|
+
* stay wired for the whole close sequence. Same reasoning for
|
|
62
|
+
* `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`
|
|
63
|
+
* stream would otherwise re-reveal what it just hid, every single frame.
|
|
64
|
+
*/
|
|
52
65
|
private readonly onActivity;
|
|
53
66
|
/** x */
|
|
54
67
|
private controlsHiddenAtGestureStart;
|
|
@@ -105,7 +118,18 @@ export declare class Gallery {
|
|
|
105
118
|
*/
|
|
106
119
|
getOriginElement(index: number): HTMLElement | null;
|
|
107
120
|
private scheduleAutoHide;
|
|
108
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Forces the same fade §2.8's idle timer would eventually trigger —
|
|
123
|
+
* public so a plugin can hide controls on its own trigger. Same
|
|
124
|
+
* `isControlActive()` guard as the timer. Also a no-op under
|
|
125
|
+
* `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's
|
|
126
|
+
* tap-to-toggle-chrome behavior called this directly and ignored `false`
|
|
127
|
+
* entirely, since only the idle timer checked it. `false` has to hold for
|
|
128
|
+
* every caller, not just the timer — `mobileSettings.controls: false` is
|
|
129
|
+
* affected the same way, by design. `forceHideControls()`/
|
|
130
|
+
* `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't
|
|
131
|
+
* check this — direct user actions with their own feedback, not auto-hide.
|
|
132
|
+
*/
|
|
109
133
|
hideControls(): void;
|
|
110
134
|
private showControls;
|
|
111
135
|
/**
|
|
@@ -177,6 +201,40 @@ export declare class Gallery {
|
|
|
177
201
|
*/
|
|
178
202
|
private applyMobileControlsSetting;
|
|
179
203
|
close(): void;
|
|
204
|
+
/**
|
|
205
|
+
* DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed
|
|
206
|
+
* vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own
|
|
207
|
+
* `dragStart`, so the whole close continues as one motion from exactly
|
|
208
|
+
* where the drag left off (see `GestureController.
|
|
209
|
+
* takeFrozenDragTransform`).
|
|
210
|
+
*/
|
|
211
|
+
private closeFromSwipe;
|
|
212
|
+
/**
|
|
213
|
+
* `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a
|
|
214
|
+
* button-close, which has no drag to continue from. Controls fade and the
|
|
215
|
+
* zoom-out run concurrently, both starting the instant `forceHideControls()`
|
|
216
|
+
* runs — not sequenced one after the other. (A previous version waited for
|
|
217
|
+
* the controls' own fade to fully finish before starting the zoom-out, for
|
|
218
|
+
* a button-close specifically — requested directly, to avoid stationary
|
|
219
|
+
* chrome hovering over an already-shrinking photo. Reversed on later,
|
|
220
|
+
* explicit feedback: waiting read as two distinct steps rather than one
|
|
221
|
+
* motion; starting both together still avoids stationary chrome, since
|
|
222
|
+
* the chrome is disappearing too, just without the pause.)
|
|
223
|
+
*/
|
|
224
|
+
private beginClose;
|
|
225
|
+
/** Forces the same fade §2.8's idle timer would eventually trigger, bypassing `hideControls()`'s own `isControlActive()` hover guard — the most common close path (clicking close) is hovering a control at this exact instant, and a deliberate close should hide regardless. No-op if already hidden. */
|
|
226
|
+
private forceHideControls;
|
|
227
|
+
/**
|
|
228
|
+
* DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide
|
|
229
|
+
* past the same distance a release would close, reveal again on retreat.
|
|
230
|
+
* `hidden: false` only reveals if visible when *this* gesture started
|
|
231
|
+
* (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already
|
|
232
|
+
* separately hidden (idle, or `autoHideDelay: 0`) before the drag began.
|
|
233
|
+
* `dragCloseThreshold` (bus event) fires on every crossing regardless of
|
|
234
|
+
* that guard — Autoplay (§4-autoplay) needs to know about the crossing
|
|
235
|
+
* itself, not just whether controls visibly moved.
|
|
236
|
+
*/
|
|
237
|
+
private setControlsHiddenForDrag;
|
|
180
238
|
/** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
|
|
181
239
|
private resolveAspectRatio;
|
|
182
240
|
/**
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { GestureEngineOptions } from '../gestures/GestureEngine';
|
|
2
2
|
import { SlideManager } from './SlideManager';
|
|
3
|
+
import { FrozenDragTransform } from './zoomTransition';
|
|
3
4
|
/**
|
|
4
|
-
* A real interactive control
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A real interactive control, plus `.shoji-caption` — a click/drag starting
|
|
6
|
+
* there should select/scroll its text natively, not get captured as a
|
|
7
|
+
* gesture. Also used by `Gallery.ts`'s `isBackdropClick`, which used to have
|
|
8
|
+
* its own narrower list (missing select/input/textarea/a[href]) — a
|
|
9
|
+
* plugin-mounted non-button control got misread as a backdrop click and
|
|
10
|
+
* closed the gallery. One shared selector so that can't recur.
|
|
8
11
|
*/
|
|
9
|
-
export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
12
|
+
export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption";
|
|
10
13
|
/** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */
|
|
11
14
|
export interface GestureControllerHost {
|
|
12
15
|
dialog: HTMLElement;
|
|
@@ -16,6 +19,10 @@ export interface GestureControllerHost {
|
|
|
16
19
|
next(): void;
|
|
17
20
|
prev(): void;
|
|
18
21
|
close(): void;
|
|
22
|
+
/** Same effect as `close()`, but for a completed vertical swipe — skips the separate controls-fade pause in front of the zoom-out (see `finishVerticalDrag`). */
|
|
23
|
+
closeFromSwipe(frozenDrag: FrozenDragTransform): void;
|
|
24
|
+
/** Live, reversible — crossing the close threshold mid-drag hides the toolbar/nav/counter/caption (a "let go now and this closes" cue); dragging back under it before release reveals them again. See `applyVerticalDragFeedback`. */
|
|
25
|
+
setControlsHiddenForDrag(hidden: boolean): void;
|
|
19
26
|
/** `closable: false` (GalleryOptions) — false suspends vertical drag-to-close entirely: no live feedback, release never calls `close()`. */
|
|
20
27
|
canClose(): boolean;
|
|
21
28
|
onActivity(): void;
|
|
@@ -42,6 +49,10 @@ export interface GestureRelayCallbacks {
|
|
|
42
49
|
export declare class GestureController {
|
|
43
50
|
private readonly host;
|
|
44
51
|
private readonly engine;
|
|
52
|
+
/** Same distance `GestureEngine` itself uses to decide a release would complete the close — reused here so the live controls-hide cue in `applyVerticalDragFeedback` actually means what it visually claims. */
|
|
53
|
+
private readonly controlsHideThreshold;
|
|
54
|
+
private controlsHiddenForDrag;
|
|
55
|
+
private lastDragDelta;
|
|
45
56
|
constructor(host: GestureControllerHost, relay: GestureRelayCallbacks, options?: Partial<GestureEngineOptions>);
|
|
46
57
|
destroy(): void;
|
|
47
58
|
/** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */
|
|
@@ -50,8 +61,33 @@ export declare class GestureController {
|
|
|
50
61
|
/** `completed` decides intent; `canGoNext`/`canGoPrev` guard whether that's actually possible (`loop: false` at an end item). */
|
|
51
62
|
private finishHorizontalDrag;
|
|
52
63
|
private settleDragOffset;
|
|
53
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Purely presentational — scales/fades the *image* as it's dragged away,
|
|
66
|
+
* without closing until release decides. Applied to `host.slides.element`,
|
|
67
|
+
* not `host.dialog` — requested directly: toolbar/nav/counter/caption are
|
|
68
|
+
* siblings, not descendants, so they stay anchored, not moving with it.
|
|
69
|
+
*/
|
|
54
70
|
private applyVerticalDragFeedback;
|
|
55
71
|
private clearVerticalDragFeedback;
|
|
56
72
|
private finishVerticalDrag;
|
|
73
|
+
/**
|
|
74
|
+
* Reads the drag's last live appearance, instantly resets `.shoji-slides`
|
|
75
|
+
* back to neutral (nothing left on it to visibly snap — `Gallery` bakes
|
|
76
|
+
* these same values onto the photo itself in the same synchronous tick,
|
|
77
|
+
* so the rendered result is unchanged, just re-homed), and returns them
|
|
78
|
+
* for that hand-off. Translate is the raw, unclamped drag distance —
|
|
79
|
+
* requested directly: the close animation must continue from exactly
|
|
80
|
+
* where the drag left off, full stop, not recenter first. (A previous
|
|
81
|
+
* version clamped this to the same 160px the dim/scale feedback ramps
|
|
82
|
+
* over, to bound how far away the close animation could start — but
|
|
83
|
+
* clamping is itself an instant correction: for any drag past that
|
|
84
|
+
* distance, release visibly snapped the photo from wherever it actually
|
|
85
|
+
* was back to the clamped point, before the real shrink-to-thumbnail
|
|
86
|
+
* motion continued from there. That snap — not the shrink itself — is
|
|
87
|
+
* what reads as "jumps to a small image in the middle of the screen."
|
|
88
|
+
* Reported from real usage, confirmed on video: released past the clamp,
|
|
89
|
+
* the photo visibly jumped from off-screen back to near-center in a
|
|
90
|
+
* single frame.)
|
|
91
|
+
*/
|
|
92
|
+
private takeFrozenDragTransform;
|
|
57
93
|
}
|
package/dist/esm/core/dom.d.ts
CHANGED
package/dist/esm/core/index.js
CHANGED
|
@@ -1,24 +1,69 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
import { w as waitForTransitionEnd, c as containedBox, z as zoomIn, a as zoomOut } from "../zoomTransition-
|
|
4
|
+
import { w as waitForTransitionEnd, c as containedBox, z as zoomIn, a as zoomOut } from "../zoomTransition-BeIimqDT.js";
|
|
5
5
|
let lockCount = 0;
|
|
6
|
-
let savedOverflow = "";
|
|
7
6
|
let savedHtmlOverflow = "";
|
|
7
|
+
let savedHtmlPaddingRight = "";
|
|
8
|
+
let savedHtmlPaddingLeft = "";
|
|
9
|
+
let savedScrollX = 0;
|
|
10
|
+
let savedScrollY = 0;
|
|
11
|
+
let styleObserver = null;
|
|
12
|
+
const LIGHTBOX_SELECTOR = ".shoji-outer";
|
|
13
|
+
function isRtl() {
|
|
14
|
+
return getComputedStyle(document.documentElement).direction === "rtl";
|
|
15
|
+
}
|
|
16
|
+
function onTouchMove(event) {
|
|
17
|
+
const target = event.target;
|
|
18
|
+
const insideLightbox = target instanceof Element && target.closest(LIGHTBOX_SELECTOR) !== null;
|
|
19
|
+
if (!insideLightbox) event.preventDefault();
|
|
20
|
+
}
|
|
21
|
+
function onStyleMutation() {
|
|
22
|
+
if (lockCount === 0) return;
|
|
23
|
+
const html = document.documentElement;
|
|
24
|
+
if (getComputedStyle(html).overflow !== "hidden") {
|
|
25
|
+
html.style.overflow = "hidden";
|
|
26
|
+
styleObserver == null ? void 0 : styleObserver.takeRecords();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
function lockBodyScroll() {
|
|
9
30
|
if (lockCount === 0) {
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
savedScrollX = window.scrollX;
|
|
32
|
+
savedScrollY = window.scrollY;
|
|
33
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
34
|
+
const rtl = isRtl();
|
|
35
|
+
savedHtmlPaddingRight = document.documentElement.style.paddingRight;
|
|
36
|
+
savedHtmlPaddingLeft = document.documentElement.style.paddingLeft;
|
|
37
|
+
if (scrollbarWidth > 0) {
|
|
38
|
+
if (rtl) {
|
|
39
|
+
const currentPaddingLeft = parseFloat(getComputedStyle(document.documentElement).paddingLeft) || 0;
|
|
40
|
+
document.documentElement.style.paddingLeft = `${currentPaddingLeft + scrollbarWidth}px`;
|
|
41
|
+
} else {
|
|
42
|
+
const currentPaddingRight = parseFloat(getComputedStyle(document.documentElement).paddingRight) || 0;
|
|
43
|
+
document.documentElement.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
12
46
|
savedHtmlOverflow = document.documentElement.style.overflow;
|
|
13
47
|
document.documentElement.style.overflow = "hidden";
|
|
48
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
49
|
+
styleObserver = new MutationObserver(onStyleMutation);
|
|
50
|
+
styleObserver.observe(document.documentElement, {
|
|
51
|
+
attributes: true,
|
|
52
|
+
attributeFilter: ["style"]
|
|
53
|
+
});
|
|
14
54
|
}
|
|
15
55
|
lockCount++;
|
|
16
56
|
}
|
|
17
57
|
function unlockBodyScroll() {
|
|
18
58
|
lockCount = Math.max(0, lockCount - 1);
|
|
19
59
|
if (lockCount === 0) {
|
|
20
|
-
|
|
60
|
+
styleObserver == null ? void 0 : styleObserver.disconnect();
|
|
61
|
+
styleObserver = null;
|
|
62
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
21
63
|
document.documentElement.style.overflow = savedHtmlOverflow;
|
|
64
|
+
document.documentElement.style.paddingRight = savedHtmlPaddingRight;
|
|
65
|
+
document.documentElement.style.paddingLeft = savedHtmlPaddingLeft;
|
|
66
|
+
window.scrollTo({ left: savedScrollX, top: savedScrollY, behavior: "instant" });
|
|
22
67
|
}
|
|
23
68
|
}
|
|
24
69
|
class EventBus {
|
|
@@ -108,6 +153,7 @@ function buildLightboxDom(slides, labels) {
|
|
|
108
153
|
outer.appendChild(dialog);
|
|
109
154
|
return {
|
|
110
155
|
outer,
|
|
156
|
+
backdrop,
|
|
111
157
|
dialog,
|
|
112
158
|
counter,
|
|
113
159
|
caption,
|
|
@@ -181,6 +227,8 @@ class GestureEngine {
|
|
|
181
227
|
// along the locked axis, at the moment direction locked — subtracted so onDragStart's delta is 0
|
|
182
228
|
__publicField(this, "pinching", false);
|
|
183
229
|
__publicField(this, "pinchStartDistance", 0);
|
|
230
|
+
/** True once `setPointerCapture` has actually been called for the gesture currently in progress — see `suppressRetargetedClick`'s own doc comment for why this needs tracking separately from `direction`. */
|
|
231
|
+
__publicField(this, "capturedThisGesture", false);
|
|
184
232
|
// -Infinity, not 0: `event.timeStamp` is time-since-navigation-start, so a
|
|
185
233
|
// real first tap is always some large positive number and 0 would seem
|
|
186
234
|
// like a safe "no previous tap" sentinel in practice — but not always: a
|
|
@@ -206,6 +254,7 @@ class GestureEngine {
|
|
|
206
254
|
if (this.pointers.size === 1) {
|
|
207
255
|
this.primaryPointerId = event.pointerId;
|
|
208
256
|
this.direction = null;
|
|
257
|
+
this.capturedThisGesture = false;
|
|
209
258
|
} else if (this.pointers.size === 2) {
|
|
210
259
|
this.direction = null;
|
|
211
260
|
this.pinching = true;
|
|
@@ -240,7 +289,10 @@ class GestureEngine {
|
|
|
240
289
|
if (Math.max(absX, absY) < this.options.lockThreshold) return;
|
|
241
290
|
this.direction = absX > absY ? "horizontal" : "vertical";
|
|
242
291
|
this.dragStartDistance = this.direction === "horizontal" ? dx : dy;
|
|
243
|
-
if (((_d = (_c = this.callbacks).shouldCapture) == null ? void 0 : _d.call(_c)) ?? true)
|
|
292
|
+
if (((_d = (_c = this.callbacks).shouldCapture) == null ? void 0 : _d.call(_c)) ?? true) {
|
|
293
|
+
this.target.setPointerCapture(event.pointerId);
|
|
294
|
+
this.capturedThisGesture = true;
|
|
295
|
+
}
|
|
244
296
|
(_f = (_e = this.callbacks).onDragStart) == null ? void 0 : _f.call(_e, this.direction, event);
|
|
245
297
|
}
|
|
246
298
|
if (this.direction === "horizontal") event.preventDefault();
|
|
@@ -307,9 +359,30 @@ class GestureEngine {
|
|
|
307
359
|
const velocity = totalDelta / elapsed;
|
|
308
360
|
const completed = !cancelled && (Math.abs(totalDelta) >= this.options.swipeThreshold || Math.abs(velocity) >= this.options.swipeVelocity);
|
|
309
361
|
(_d = (_c = this.callbacks).onDragEnd) == null ? void 0 : _d.call(_c, this.direction, totalDelta, velocity, completed, cancelled);
|
|
362
|
+
if (this.capturedThisGesture) this.suppressRetargetedClick();
|
|
310
363
|
}
|
|
311
364
|
this.primaryPointerId = null;
|
|
312
365
|
this.direction = null;
|
|
366
|
+
this.capturedThisGesture = false;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* A captured pointer's release still fires a real `click`, retargeted to
|
|
370
|
+
* `target` regardless of where the pointer visually ends up — misread by
|
|
371
|
+
* Gallery's click-outside-to-close as landing nowhere recognizable.
|
|
372
|
+
* Consumes exactly one `click` on `target` in the capture phase, then
|
|
373
|
+
* removes itself; a fallback timeout also removes it in case no `click`
|
|
374
|
+
* ever comes, so it can't swallow a later, unrelated one.
|
|
375
|
+
*/
|
|
376
|
+
suppressRetargetedClick() {
|
|
377
|
+
let done = false;
|
|
378
|
+
const cleanup = (event) => {
|
|
379
|
+
if (done) return;
|
|
380
|
+
done = true;
|
|
381
|
+
event == null ? void 0 : event.stopPropagation();
|
|
382
|
+
this.target.removeEventListener("click", cleanup, { capture: true });
|
|
383
|
+
};
|
|
384
|
+
this.target.addEventListener("click", cleanup, { capture: true });
|
|
385
|
+
setTimeout(cleanup, 500);
|
|
313
386
|
}
|
|
314
387
|
registerTap(x, y, event) {
|
|
315
388
|
var _a, _b, _c, _d;
|
|
@@ -335,13 +408,17 @@ function centerOf(a, b) {
|
|
|
335
408
|
const DRAG_SETTLE_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
336
409
|
const DRAG_FEEDBACK_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing), opacity var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
337
410
|
const VERTICAL_FEEDBACK_DISTANCE = 160;
|
|
338
|
-
const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
411
|
+
const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption";
|
|
339
412
|
function shouldIgnoreGesture(event) {
|
|
340
413
|
return event.composedPath().some((node) => node instanceof Element && node.matches(INTERACTIVE_CONTROL_SELECTOR));
|
|
341
414
|
}
|
|
342
415
|
class GestureController {
|
|
343
416
|
constructor(host, relay, options) {
|
|
344
417
|
__publicField(this, "engine");
|
|
418
|
+
/** Same distance `GestureEngine` itself uses to decide a release would complete the close — reused here so the live controls-hide cue in `applyVerticalDragFeedback` actually means what it visually claims. */
|
|
419
|
+
__publicField(this, "controlsHideThreshold");
|
|
420
|
+
__publicField(this, "controlsHiddenForDrag", false);
|
|
421
|
+
__publicField(this, "lastDragDelta", 0);
|
|
345
422
|
/** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */
|
|
346
423
|
__publicField(this, "onDragMove", (direction, delta) => {
|
|
347
424
|
if (this.host.isZoomed()) return;
|
|
@@ -360,6 +437,7 @@ class GestureController {
|
|
|
360
437
|
}
|
|
361
438
|
});
|
|
362
439
|
this.host = host;
|
|
440
|
+
this.controlsHideThreshold = (options == null ? void 0 : options.swipeThreshold) ?? 50;
|
|
363
441
|
this.engine = new GestureEngine(
|
|
364
442
|
host.dialog,
|
|
365
443
|
{
|
|
@@ -407,28 +485,70 @@ class GestureController {
|
|
|
407
485
|
}
|
|
408
486
|
waitForTransitionEnd(settleEl, onSettled);
|
|
409
487
|
}
|
|
410
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* Purely presentational — scales/fades the *image* as it's dragged away,
|
|
490
|
+
* without closing until release decides. Applied to `host.slides.element`,
|
|
491
|
+
* not `host.dialog` — requested directly: toolbar/nav/counter/caption are
|
|
492
|
+
* siblings, not descendants, so they stay anchored, not moving with it.
|
|
493
|
+
*/
|
|
411
494
|
applyVerticalDragFeedback(delta) {
|
|
412
495
|
const progress = Math.min(Math.abs(delta) / VERTICAL_FEEDBACK_DISTANCE, 1);
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
496
|
+
const slides = this.host.slides.element;
|
|
497
|
+
this.lastDragDelta = delta;
|
|
498
|
+
slides.style.transition = "";
|
|
499
|
+
slides.style.transform = `translateY(${delta}px) scale(${1 - progress * 0.15})`;
|
|
500
|
+
slides.style.opacity = String(1 - progress * 0.6);
|
|
501
|
+
const pastThreshold = Math.abs(delta) >= this.controlsHideThreshold;
|
|
502
|
+
if (pastThreshold !== this.controlsHiddenForDrag) {
|
|
503
|
+
this.controlsHiddenForDrag = pastThreshold;
|
|
504
|
+
this.host.setControlsHiddenForDrag(pastThreshold);
|
|
505
|
+
}
|
|
417
506
|
}
|
|
418
507
|
clearVerticalDragFeedback(animate) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
508
|
+
const slides = this.host.slides.element;
|
|
509
|
+
slides.style.transition = animate ? DRAG_FEEDBACK_TRANSITION : "";
|
|
510
|
+
slides.style.transform = "";
|
|
511
|
+
slides.style.opacity = "";
|
|
423
512
|
}
|
|
424
513
|
finishVerticalDrag(completed) {
|
|
514
|
+
const wasHiddenForDrag = this.controlsHiddenForDrag;
|
|
515
|
+
this.controlsHiddenForDrag = false;
|
|
425
516
|
if (completed && this.host.canClose()) {
|
|
426
|
-
this.
|
|
427
|
-
this.host.close();
|
|
517
|
+
this.host.closeFromSwipe(this.takeFrozenDragTransform());
|
|
428
518
|
} else {
|
|
429
519
|
this.clearVerticalDragFeedback(true);
|
|
520
|
+
if (wasHiddenForDrag) this.host.setControlsHiddenForDrag(false);
|
|
430
521
|
}
|
|
431
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Reads the drag's last live appearance, instantly resets `.shoji-slides`
|
|
525
|
+
* back to neutral (nothing left on it to visibly snap — `Gallery` bakes
|
|
526
|
+
* these same values onto the photo itself in the same synchronous tick,
|
|
527
|
+
* so the rendered result is unchanged, just re-homed), and returns them
|
|
528
|
+
* for that hand-off. Translate is the raw, unclamped drag distance —
|
|
529
|
+
* requested directly: the close animation must continue from exactly
|
|
530
|
+
* where the drag left off, full stop, not recenter first. (A previous
|
|
531
|
+
* version clamped this to the same 160px the dim/scale feedback ramps
|
|
532
|
+
* over, to bound how far away the close animation could start — but
|
|
533
|
+
* clamping is itself an instant correction: for any drag past that
|
|
534
|
+
* distance, release visibly snapped the photo from wherever it actually
|
|
535
|
+
* was back to the clamped point, before the real shrink-to-thumbnail
|
|
536
|
+
* motion continued from there. That snap — not the shrink itself — is
|
|
537
|
+
* what reads as "jumps to a small image in the middle of the screen."
|
|
538
|
+
* Reported from real usage, confirmed on video: released past the clamp,
|
|
539
|
+
* the photo visibly jumped from off-screen back to near-center in a
|
|
540
|
+
* single frame.)
|
|
541
|
+
*/
|
|
542
|
+
takeFrozenDragTransform() {
|
|
543
|
+
const progress = Math.min(Math.abs(this.lastDragDelta) / VERTICAL_FEEDBACK_DISTANCE, 1);
|
|
544
|
+
const frozen = {
|
|
545
|
+
translateY: this.lastDragDelta,
|
|
546
|
+
scale: 1 - progress * 0.15,
|
|
547
|
+
opacity: 1 - progress * 0.6
|
|
548
|
+
};
|
|
549
|
+
this.clearVerticalDragFeedback(false);
|
|
550
|
+
return frozen;
|
|
551
|
+
}
|
|
432
552
|
}
|
|
433
553
|
class LiveRegion {
|
|
434
554
|
constructor() {
|
|
@@ -1344,7 +1464,7 @@ const DEFAULT_LOCALE = {
|
|
|
1344
1464
|
function isBackdropClick(event) {
|
|
1345
1465
|
return !event.composedPath().some(
|
|
1346
1466
|
(node) => node instanceof Element && node.matches(
|
|
1347
|
-
`.shoji-slide-img, .shoji-slide-provider-video, .shoji-counter,
|
|
1467
|
+
`.shoji-slide-img, .shoji-slide-provider-video, .shoji-counter, ${INTERACTIVE_CONTROL_SELECTOR}`
|
|
1348
1468
|
)
|
|
1349
1469
|
);
|
|
1350
1470
|
}
|
|
@@ -1373,6 +1493,8 @@ class Gallery {
|
|
|
1373
1493
|
__publicField(this, "autoHidden", false);
|
|
1374
1494
|
__publicField(this, "hoveredControlCount", 0);
|
|
1375
1495
|
__publicField(this, "isClosing", false);
|
|
1496
|
+
/** True while a vertical drag has hidden controls past its own distance threshold (`setControlsHiddenForDrag`) — `onActivity()` defers to it, since the drag's own continuous pointermove stream would otherwise immediately re-reveal what it just hid on every single move. */
|
|
1497
|
+
__publicField(this, "controlsHiddenByDrag", false);
|
|
1376
1498
|
__publicField(this, "itemList", []);
|
|
1377
1499
|
__publicField(this, "scannedElements", []);
|
|
1378
1500
|
__publicField(this, "activeIndex", 0);
|
|
@@ -1406,9 +1528,21 @@ class Gallery {
|
|
|
1406
1528
|
__publicField(this, "onOuterClick", (event) => {
|
|
1407
1529
|
if (this.closable && isBackdropClick(event)) this.close();
|
|
1408
1530
|
});
|
|
1409
|
-
/**
|
|
1531
|
+
/**
|
|
1532
|
+
* DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle
|
|
1533
|
+
* clock. `autoHideDelay: 0` = "never show controls" — a no-op here.
|
|
1534
|
+
* `autoHideDelay: false` = "always visible" — also a no-op: nothing to
|
|
1535
|
+
* reveal (already shown) or reschedule (no timer ever runs). Also a no-op
|
|
1536
|
+
* once `isClosing` — a real bug, reported from real usage: moving the
|
|
1537
|
+
* mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)
|
|
1538
|
+
* re-showed the just-hidden controls mid-animation, since these listeners
|
|
1539
|
+
* stay wired for the whole close sequence. Same reasoning for
|
|
1540
|
+
* `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`
|
|
1541
|
+
* stream would otherwise re-reveal what it just hid, every single frame.
|
|
1542
|
+
*/
|
|
1410
1543
|
__publicField(this, "onActivity", () => {
|
|
1411
|
-
if (this.autoHideDelay === 0)
|
|
1544
|
+
if (this.autoHideDelay === 0 || this.autoHideDelay === false || this.isClosing || this.controlsHiddenByDrag)
|
|
1545
|
+
return;
|
|
1412
1546
|
this.showControls();
|
|
1413
1547
|
this.scheduleAutoHide();
|
|
1414
1548
|
});
|
|
@@ -1538,6 +1672,11 @@ class Gallery {
|
|
|
1538
1672
|
this.transition = new SlideTransition(this.slides);
|
|
1539
1673
|
const dom = buildLightboxDom(this.slides.element, this.locale);
|
|
1540
1674
|
this.dom = dom;
|
|
1675
|
+
if (this.options.backdropOpacity != null) {
|
|
1676
|
+
const clamped = Math.min(Math.max(this.options.backdropOpacity, 0), 1);
|
|
1677
|
+
dom.outer.style.setProperty("--shoji-backdrop-opacity", String(clamped));
|
|
1678
|
+
}
|
|
1679
|
+
if (this.autoHideDelay === 0) dom.dialog.classList.add("shoji-cursor-visible");
|
|
1541
1680
|
dom.outer.appendChild(this.liveRegion.element);
|
|
1542
1681
|
dom.closeButton.addEventListener("click", () => this.close());
|
|
1543
1682
|
dom.prevButton.addEventListener("click", () => this.prev());
|
|
@@ -1580,6 +1719,8 @@ class Gallery {
|
|
|
1580
1719
|
next: () => this.navigate(this.nextIndex(), 1, false),
|
|
1581
1720
|
prev: () => this.navigate(this.prevIndex(), -1, false),
|
|
1582
1721
|
close: () => this.close(),
|
|
1722
|
+
closeFromSwipe: (frozenDrag) => this.closeFromSwipe(frozenDrag),
|
|
1723
|
+
setControlsHiddenForDrag: (hidden) => this.setControlsHiddenForDrag(hidden),
|
|
1583
1724
|
canClose: () => this.closable,
|
|
1584
1725
|
onActivity: () => this.onActivity(),
|
|
1585
1726
|
isZoomed: () => {
|
|
@@ -1761,15 +1902,28 @@ class Gallery {
|
|
|
1761
1902
|
this.autoHideTimer = null;
|
|
1762
1903
|
}
|
|
1763
1904
|
if (!this.opened) return;
|
|
1905
|
+
if (this.autoHideDelay === false) return;
|
|
1764
1906
|
if (this.autoHideDelay === 0) {
|
|
1765
1907
|
this.hideControls();
|
|
1766
1908
|
return;
|
|
1767
1909
|
}
|
|
1768
1910
|
this.autoHideTimer = setTimeout(() => this.hideControls(), this.autoHideDelay);
|
|
1769
1911
|
}
|
|
1770
|
-
/**
|
|
1912
|
+
/**
|
|
1913
|
+
* Forces the same fade §2.8's idle timer would eventually trigger —
|
|
1914
|
+
* public so a plugin can hide controls on its own trigger. Same
|
|
1915
|
+
* `isControlActive()` guard as the timer. Also a no-op under
|
|
1916
|
+
* `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's
|
|
1917
|
+
* tap-to-toggle-chrome behavior called this directly and ignored `false`
|
|
1918
|
+
* entirely, since only the idle timer checked it. `false` has to hold for
|
|
1919
|
+
* every caller, not just the timer — `mobileSettings.controls: false` is
|
|
1920
|
+
* affected the same way, by design. `forceHideControls()`/
|
|
1921
|
+
* `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't
|
|
1922
|
+
* check this — direct user actions with their own feedback, not auto-hide.
|
|
1923
|
+
*/
|
|
1771
1924
|
hideControls() {
|
|
1772
|
-
if (!this.dom || this.autoHidden || this.isControlActive())
|
|
1925
|
+
if (!this.dom || this.autoHidden || this.isControlActive() || this.autoHideDelay === false)
|
|
1926
|
+
return;
|
|
1773
1927
|
this.autoHidden = true;
|
|
1774
1928
|
this.dom.dialog.classList.add("shoji-controls-hidden");
|
|
1775
1929
|
this.bus.emit("controls:hide", {});
|
|
@@ -1996,6 +2150,31 @@ class Gallery {
|
|
|
1996
2150
|
}
|
|
1997
2151
|
}
|
|
1998
2152
|
close() {
|
|
2153
|
+
this.beginClose();
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed
|
|
2157
|
+
* vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own
|
|
2158
|
+
* `dragStart`, so the whole close continues as one motion from exactly
|
|
2159
|
+
* where the drag left off (see `GestureController.
|
|
2160
|
+
* takeFrozenDragTransform`).
|
|
2161
|
+
*/
|
|
2162
|
+
closeFromSwipe(frozenDrag) {
|
|
2163
|
+
this.beginClose(frozenDrag);
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a
|
|
2167
|
+
* button-close, which has no drag to continue from. Controls fade and the
|
|
2168
|
+
* zoom-out run concurrently, both starting the instant `forceHideControls()`
|
|
2169
|
+
* runs — not sequenced one after the other. (A previous version waited for
|
|
2170
|
+
* the controls' own fade to fully finish before starting the zoom-out, for
|
|
2171
|
+
* a button-close specifically — requested directly, to avoid stationary
|
|
2172
|
+
* chrome hovering over an already-shrinking photo. Reversed on later,
|
|
2173
|
+
* explicit feedback: waiting read as two distinct steps rather than one
|
|
2174
|
+
* motion; starting both together still avoids stationary chrome, since
|
|
2175
|
+
* the chrome is disappearing too, just without the pause.)
|
|
2176
|
+
*/
|
|
2177
|
+
beginClose(frozenDrag) {
|
|
1999
2178
|
var _a, _b;
|
|
2000
2179
|
if (this.destroyed || !this.opened || this.isClosing) return;
|
|
2001
2180
|
this.bus.emit("beforeClose", {});
|
|
@@ -2006,11 +2185,41 @@ class Gallery {
|
|
|
2006
2185
|
const naturalSize = this.resolveNaturalSize(this.activeIndex);
|
|
2007
2186
|
if (media && origin && (((_b = this.slides) == null ? void 0 : _b.isActiveReady()) || naturalSize)) {
|
|
2008
2187
|
const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);
|
|
2009
|
-
|
|
2188
|
+
this.forceHideControls();
|
|
2189
|
+
if (this.dom) this.dom.backdrop.style.opacity = "0";
|
|
2190
|
+
zoomOut(
|
|
2191
|
+
{ origin, target: media, aspectRatio, naturalSize, dragStart: frozenDrag },
|
|
2192
|
+
() => this.finishClose()
|
|
2193
|
+
);
|
|
2010
2194
|
} else {
|
|
2011
2195
|
this.finishClose();
|
|
2012
2196
|
}
|
|
2013
2197
|
}
|
|
2198
|
+
/** Forces the same fade §2.8's idle timer would eventually trigger, bypassing `hideControls()`'s own `isControlActive()` hover guard — the most common close path (clicking close) is hovering a control at this exact instant, and a deliberate close should hide regardless. No-op if already hidden. */
|
|
2199
|
+
forceHideControls() {
|
|
2200
|
+
if (!this.dom || this.autoHidden) return;
|
|
2201
|
+
this.autoHidden = true;
|
|
2202
|
+
this.dom.dialog.classList.add("shoji-controls-hidden");
|
|
2203
|
+
this.bus.emit("controls:hide", {});
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide
|
|
2207
|
+
* past the same distance a release would close, reveal again on retreat.
|
|
2208
|
+
* `hidden: false` only reveals if visible when *this* gesture started
|
|
2209
|
+
* (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already
|
|
2210
|
+
* separately hidden (idle, or `autoHideDelay: 0`) before the drag began.
|
|
2211
|
+
* `dragCloseThreshold` (bus event) fires on every crossing regardless of
|
|
2212
|
+
* that guard — Autoplay (§4-autoplay) needs to know about the crossing
|
|
2213
|
+
* itself, not just whether controls visibly moved.
|
|
2214
|
+
*/
|
|
2215
|
+
setControlsHiddenForDrag(hidden) {
|
|
2216
|
+
var _a;
|
|
2217
|
+
this.controlsHiddenByDrag = hidden;
|
|
2218
|
+
if (hidden) this.forceHideControls();
|
|
2219
|
+
else if (!this.controlsHiddenAtGestureStart) this.showControls();
|
|
2220
|
+
(_a = this.dom) == null ? void 0 : _a.dialog.classList.toggle("shoji-controls-hidden-for-drag", hidden);
|
|
2221
|
+
this.bus.emit("dragCloseThreshold", { hidden });
|
|
2222
|
+
}
|
|
2014
2223
|
/** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
|
|
2015
2224
|
resolveAspectRatio(index, origin) {
|
|
2016
2225
|
const item = this.itemList[index];
|
|
@@ -2041,7 +2250,7 @@ class Gallery {
|
|
|
2041
2250
|
* instead of a second `close`/`afterClose` emission on a torn-down gallery.
|
|
2042
2251
|
*/
|
|
2043
2252
|
finishClose() {
|
|
2044
|
-
var _a, _b;
|
|
2253
|
+
var _a, _b, _c;
|
|
2045
2254
|
if (!this.opened) return;
|
|
2046
2255
|
this.isClosing = false;
|
|
2047
2256
|
this.opened = false;
|
|
@@ -2049,13 +2258,16 @@ class Gallery {
|
|
|
2049
2258
|
document.removeEventListener("keydown", this.onKeydown);
|
|
2050
2259
|
this.focusTrap.deactivate();
|
|
2051
2260
|
(_a = this.dom) == null ? void 0 : _a.outer.classList.remove("shoji-open");
|
|
2261
|
+
if (this.dom) this.dom.backdrop.style.opacity = "";
|
|
2052
2262
|
if (this.autoHideTimer !== null) {
|
|
2053
2263
|
clearTimeout(this.autoHideTimer);
|
|
2054
2264
|
this.autoHideTimer = null;
|
|
2055
2265
|
}
|
|
2056
2266
|
this.autoHidden = false;
|
|
2057
2267
|
this.hoveredControlCount = 0;
|
|
2268
|
+
this.controlsHiddenByDrag = false;
|
|
2058
2269
|
(_b = this.dom) == null ? void 0 : _b.dialog.classList.remove("shoji-controls-hidden");
|
|
2270
|
+
(_c = this.dom) == null ? void 0 : _c.dialog.classList.remove("shoji-controls-hidden-for-drag");
|
|
2059
2271
|
this.bus.emit("close", {});
|
|
2060
2272
|
this.bus.emit("afterClose", {});
|
|
2061
2273
|
}
|