@michaelyagi/shoji 0.1.0-alpha.11 → 0.1.0-alpha.12
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 +196 -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 +230 -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,14 +1,18 @@
|
|
|
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
8
|
function lockBodyScroll() {
|
|
9
9
|
if (lockCount === 0) {
|
|
10
|
-
|
|
11
|
-
document.
|
|
10
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
11
|
+
savedHtmlPaddingRight = document.documentElement.style.paddingRight;
|
|
12
|
+
if (scrollbarWidth > 0) {
|
|
13
|
+
const currentPaddingRight = parseFloat(getComputedStyle(document.documentElement).paddingRight) || 0;
|
|
14
|
+
document.documentElement.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
15
|
+
}
|
|
12
16
|
savedHtmlOverflow = document.documentElement.style.overflow;
|
|
13
17
|
document.documentElement.style.overflow = "hidden";
|
|
14
18
|
}
|
|
@@ -17,8 +21,8 @@ function lockBodyScroll() {
|
|
|
17
21
|
function unlockBodyScroll() {
|
|
18
22
|
lockCount = Math.max(0, lockCount - 1);
|
|
19
23
|
if (lockCount === 0) {
|
|
20
|
-
document.body.style.overflow = savedOverflow;
|
|
21
24
|
document.documentElement.style.overflow = savedHtmlOverflow;
|
|
25
|
+
document.documentElement.style.paddingRight = savedHtmlPaddingRight;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
class EventBus {
|
|
@@ -108,6 +112,7 @@ function buildLightboxDom(slides, labels) {
|
|
|
108
112
|
outer.appendChild(dialog);
|
|
109
113
|
return {
|
|
110
114
|
outer,
|
|
115
|
+
backdrop,
|
|
111
116
|
dialog,
|
|
112
117
|
counter,
|
|
113
118
|
caption,
|
|
@@ -181,6 +186,8 @@ class GestureEngine {
|
|
|
181
186
|
// along the locked axis, at the moment direction locked — subtracted so onDragStart's delta is 0
|
|
182
187
|
__publicField(this, "pinching", false);
|
|
183
188
|
__publicField(this, "pinchStartDistance", 0);
|
|
189
|
+
/** 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`. */
|
|
190
|
+
__publicField(this, "capturedThisGesture", false);
|
|
184
191
|
// -Infinity, not 0: `event.timeStamp` is time-since-navigation-start, so a
|
|
185
192
|
// real first tap is always some large positive number and 0 would seem
|
|
186
193
|
// like a safe "no previous tap" sentinel in practice — but not always: a
|
|
@@ -206,6 +213,7 @@ class GestureEngine {
|
|
|
206
213
|
if (this.pointers.size === 1) {
|
|
207
214
|
this.primaryPointerId = event.pointerId;
|
|
208
215
|
this.direction = null;
|
|
216
|
+
this.capturedThisGesture = false;
|
|
209
217
|
} else if (this.pointers.size === 2) {
|
|
210
218
|
this.direction = null;
|
|
211
219
|
this.pinching = true;
|
|
@@ -240,7 +248,10 @@ class GestureEngine {
|
|
|
240
248
|
if (Math.max(absX, absY) < this.options.lockThreshold) return;
|
|
241
249
|
this.direction = absX > absY ? "horizontal" : "vertical";
|
|
242
250
|
this.dragStartDistance = this.direction === "horizontal" ? dx : dy;
|
|
243
|
-
if (((_d = (_c = this.callbacks).shouldCapture) == null ? void 0 : _d.call(_c)) ?? true)
|
|
251
|
+
if (((_d = (_c = this.callbacks).shouldCapture) == null ? void 0 : _d.call(_c)) ?? true) {
|
|
252
|
+
this.target.setPointerCapture(event.pointerId);
|
|
253
|
+
this.capturedThisGesture = true;
|
|
254
|
+
}
|
|
244
255
|
(_f = (_e = this.callbacks).onDragStart) == null ? void 0 : _f.call(_e, this.direction, event);
|
|
245
256
|
}
|
|
246
257
|
if (this.direction === "horizontal") event.preventDefault();
|
|
@@ -307,9 +318,30 @@ class GestureEngine {
|
|
|
307
318
|
const velocity = totalDelta / elapsed;
|
|
308
319
|
const completed = !cancelled && (Math.abs(totalDelta) >= this.options.swipeThreshold || Math.abs(velocity) >= this.options.swipeVelocity);
|
|
309
320
|
(_d = (_c = this.callbacks).onDragEnd) == null ? void 0 : _d.call(_c, this.direction, totalDelta, velocity, completed, cancelled);
|
|
321
|
+
if (this.capturedThisGesture) this.suppressRetargetedClick();
|
|
310
322
|
}
|
|
311
323
|
this.primaryPointerId = null;
|
|
312
324
|
this.direction = null;
|
|
325
|
+
this.capturedThisGesture = false;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* A captured pointer's release still fires a real `click`, retargeted to
|
|
329
|
+
* `target` regardless of where the pointer visually ends up — misread by
|
|
330
|
+
* Gallery's click-outside-to-close as landing nowhere recognizable.
|
|
331
|
+
* Consumes exactly one `click` on `target` in the capture phase, then
|
|
332
|
+
* removes itself; a fallback timeout also removes it in case no `click`
|
|
333
|
+
* ever comes, so it can't swallow a later, unrelated one.
|
|
334
|
+
*/
|
|
335
|
+
suppressRetargetedClick() {
|
|
336
|
+
let done = false;
|
|
337
|
+
const cleanup = (event) => {
|
|
338
|
+
if (done) return;
|
|
339
|
+
done = true;
|
|
340
|
+
event == null ? void 0 : event.stopPropagation();
|
|
341
|
+
this.target.removeEventListener("click", cleanup, { capture: true });
|
|
342
|
+
};
|
|
343
|
+
this.target.addEventListener("click", cleanup, { capture: true });
|
|
344
|
+
setTimeout(cleanup, 500);
|
|
313
345
|
}
|
|
314
346
|
registerTap(x, y, event) {
|
|
315
347
|
var _a, _b, _c, _d;
|
|
@@ -335,13 +367,17 @@ function centerOf(a, b) {
|
|
|
335
367
|
const DRAG_SETTLE_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
336
368
|
const DRAG_FEEDBACK_TRANSITION = "transform var(--shoji-duration) var(--shoji-momentum-easing), opacity var(--shoji-duration) var(--shoji-momentum-easing)";
|
|
337
369
|
const VERTICAL_FEEDBACK_DISTANCE = 160;
|
|
338
|
-
const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
370
|
+
const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption";
|
|
339
371
|
function shouldIgnoreGesture(event) {
|
|
340
372
|
return event.composedPath().some((node) => node instanceof Element && node.matches(INTERACTIVE_CONTROL_SELECTOR));
|
|
341
373
|
}
|
|
342
374
|
class GestureController {
|
|
343
375
|
constructor(host, relay, options) {
|
|
344
376
|
__publicField(this, "engine");
|
|
377
|
+
/** 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. */
|
|
378
|
+
__publicField(this, "controlsHideThreshold");
|
|
379
|
+
__publicField(this, "controlsHiddenForDrag", false);
|
|
380
|
+
__publicField(this, "lastDragDelta", 0);
|
|
345
381
|
/** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */
|
|
346
382
|
__publicField(this, "onDragMove", (direction, delta) => {
|
|
347
383
|
if (this.host.isZoomed()) return;
|
|
@@ -360,6 +396,7 @@ class GestureController {
|
|
|
360
396
|
}
|
|
361
397
|
});
|
|
362
398
|
this.host = host;
|
|
399
|
+
this.controlsHideThreshold = (options == null ? void 0 : options.swipeThreshold) ?? 50;
|
|
363
400
|
this.engine = new GestureEngine(
|
|
364
401
|
host.dialog,
|
|
365
402
|
{
|
|
@@ -407,28 +444,70 @@ class GestureController {
|
|
|
407
444
|
}
|
|
408
445
|
waitForTransitionEnd(settleEl, onSettled);
|
|
409
446
|
}
|
|
410
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Purely presentational — scales/fades the *image* as it's dragged away,
|
|
449
|
+
* without closing until release decides. Applied to `host.slides.element`,
|
|
450
|
+
* not `host.dialog` — requested directly: toolbar/nav/counter/caption are
|
|
451
|
+
* siblings, not descendants, so they stay anchored, not moving with it.
|
|
452
|
+
*/
|
|
411
453
|
applyVerticalDragFeedback(delta) {
|
|
412
454
|
const progress = Math.min(Math.abs(delta) / VERTICAL_FEEDBACK_DISTANCE, 1);
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
455
|
+
const slides = this.host.slides.element;
|
|
456
|
+
this.lastDragDelta = delta;
|
|
457
|
+
slides.style.transition = "";
|
|
458
|
+
slides.style.transform = `translateY(${delta}px) scale(${1 - progress * 0.15})`;
|
|
459
|
+
slides.style.opacity = String(1 - progress * 0.6);
|
|
460
|
+
const pastThreshold = Math.abs(delta) >= this.controlsHideThreshold;
|
|
461
|
+
if (pastThreshold !== this.controlsHiddenForDrag) {
|
|
462
|
+
this.controlsHiddenForDrag = pastThreshold;
|
|
463
|
+
this.host.setControlsHiddenForDrag(pastThreshold);
|
|
464
|
+
}
|
|
417
465
|
}
|
|
418
466
|
clearVerticalDragFeedback(animate) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
467
|
+
const slides = this.host.slides.element;
|
|
468
|
+
slides.style.transition = animate ? DRAG_FEEDBACK_TRANSITION : "";
|
|
469
|
+
slides.style.transform = "";
|
|
470
|
+
slides.style.opacity = "";
|
|
423
471
|
}
|
|
424
472
|
finishVerticalDrag(completed) {
|
|
473
|
+
const wasHiddenForDrag = this.controlsHiddenForDrag;
|
|
474
|
+
this.controlsHiddenForDrag = false;
|
|
425
475
|
if (completed && this.host.canClose()) {
|
|
426
|
-
this.
|
|
427
|
-
this.host.close();
|
|
476
|
+
this.host.closeFromSwipe(this.takeFrozenDragTransform());
|
|
428
477
|
} else {
|
|
429
478
|
this.clearVerticalDragFeedback(true);
|
|
479
|
+
if (wasHiddenForDrag) this.host.setControlsHiddenForDrag(false);
|
|
430
480
|
}
|
|
431
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Reads the drag's last live appearance, instantly resets `.shoji-slides`
|
|
484
|
+
* back to neutral (nothing left on it to visibly snap — `Gallery` bakes
|
|
485
|
+
* these same values onto the photo itself in the same synchronous tick,
|
|
486
|
+
* so the rendered result is unchanged, just re-homed), and returns them
|
|
487
|
+
* for that hand-off. Translate is the raw, unclamped drag distance —
|
|
488
|
+
* requested directly: the close animation must continue from exactly
|
|
489
|
+
* where the drag left off, full stop, not recenter first. (A previous
|
|
490
|
+
* version clamped this to the same 160px the dim/scale feedback ramps
|
|
491
|
+
* over, to bound how far away the close animation could start — but
|
|
492
|
+
* clamping is itself an instant correction: for any drag past that
|
|
493
|
+
* distance, release visibly snapped the photo from wherever it actually
|
|
494
|
+
* was back to the clamped point, before the real shrink-to-thumbnail
|
|
495
|
+
* motion continued from there. That snap — not the shrink itself — is
|
|
496
|
+
* what reads as "jumps to a small image in the middle of the screen."
|
|
497
|
+
* Reported from real usage, confirmed on video: released past the clamp,
|
|
498
|
+
* the photo visibly jumped from off-screen back to near-center in a
|
|
499
|
+
* single frame.)
|
|
500
|
+
*/
|
|
501
|
+
takeFrozenDragTransform() {
|
|
502
|
+
const progress = Math.min(Math.abs(this.lastDragDelta) / VERTICAL_FEEDBACK_DISTANCE, 1);
|
|
503
|
+
const frozen = {
|
|
504
|
+
translateY: this.lastDragDelta,
|
|
505
|
+
scale: 1 - progress * 0.15,
|
|
506
|
+
opacity: 1 - progress * 0.6
|
|
507
|
+
};
|
|
508
|
+
this.clearVerticalDragFeedback(false);
|
|
509
|
+
return frozen;
|
|
510
|
+
}
|
|
432
511
|
}
|
|
433
512
|
class LiveRegion {
|
|
434
513
|
constructor() {
|
|
@@ -1344,7 +1423,7 @@ const DEFAULT_LOCALE = {
|
|
|
1344
1423
|
function isBackdropClick(event) {
|
|
1345
1424
|
return !event.composedPath().some(
|
|
1346
1425
|
(node) => node instanceof Element && node.matches(
|
|
1347
|
-
`.shoji-slide-img, .shoji-slide-provider-video, .shoji-counter,
|
|
1426
|
+
`.shoji-slide-img, .shoji-slide-provider-video, .shoji-counter, ${INTERACTIVE_CONTROL_SELECTOR}`
|
|
1348
1427
|
)
|
|
1349
1428
|
);
|
|
1350
1429
|
}
|
|
@@ -1373,6 +1452,8 @@ class Gallery {
|
|
|
1373
1452
|
__publicField(this, "autoHidden", false);
|
|
1374
1453
|
__publicField(this, "hoveredControlCount", 0);
|
|
1375
1454
|
__publicField(this, "isClosing", false);
|
|
1455
|
+
/** 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. */
|
|
1456
|
+
__publicField(this, "controlsHiddenByDrag", false);
|
|
1376
1457
|
__publicField(this, "itemList", []);
|
|
1377
1458
|
__publicField(this, "scannedElements", []);
|
|
1378
1459
|
__publicField(this, "activeIndex", 0);
|
|
@@ -1406,9 +1487,21 @@ class Gallery {
|
|
|
1406
1487
|
__publicField(this, "onOuterClick", (event) => {
|
|
1407
1488
|
if (this.closable && isBackdropClick(event)) this.close();
|
|
1408
1489
|
});
|
|
1409
|
-
/**
|
|
1490
|
+
/**
|
|
1491
|
+
* DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle
|
|
1492
|
+
* clock. `autoHideDelay: 0` = "never show controls" — a no-op here.
|
|
1493
|
+
* `autoHideDelay: false` = "always visible" — also a no-op: nothing to
|
|
1494
|
+
* reveal (already shown) or reschedule (no timer ever runs). Also a no-op
|
|
1495
|
+
* once `isClosing` — a real bug, reported from real usage: moving the
|
|
1496
|
+
* mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)
|
|
1497
|
+
* re-showed the just-hidden controls mid-animation, since these listeners
|
|
1498
|
+
* stay wired for the whole close sequence. Same reasoning for
|
|
1499
|
+
* `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`
|
|
1500
|
+
* stream would otherwise re-reveal what it just hid, every single frame.
|
|
1501
|
+
*/
|
|
1410
1502
|
__publicField(this, "onActivity", () => {
|
|
1411
|
-
if (this.autoHideDelay === 0)
|
|
1503
|
+
if (this.autoHideDelay === 0 || this.autoHideDelay === false || this.isClosing || this.controlsHiddenByDrag)
|
|
1504
|
+
return;
|
|
1412
1505
|
this.showControls();
|
|
1413
1506
|
this.scheduleAutoHide();
|
|
1414
1507
|
});
|
|
@@ -1538,6 +1631,11 @@ class Gallery {
|
|
|
1538
1631
|
this.transition = new SlideTransition(this.slides);
|
|
1539
1632
|
const dom = buildLightboxDom(this.slides.element, this.locale);
|
|
1540
1633
|
this.dom = dom;
|
|
1634
|
+
if (this.options.backdropOpacity != null) {
|
|
1635
|
+
const clamped = Math.min(Math.max(this.options.backdropOpacity, 0), 1);
|
|
1636
|
+
dom.outer.style.setProperty("--shoji-backdrop-opacity", String(clamped));
|
|
1637
|
+
}
|
|
1638
|
+
if (this.autoHideDelay === 0) dom.dialog.classList.add("shoji-cursor-visible");
|
|
1541
1639
|
dom.outer.appendChild(this.liveRegion.element);
|
|
1542
1640
|
dom.closeButton.addEventListener("click", () => this.close());
|
|
1543
1641
|
dom.prevButton.addEventListener("click", () => this.prev());
|
|
@@ -1580,6 +1678,8 @@ class Gallery {
|
|
|
1580
1678
|
next: () => this.navigate(this.nextIndex(), 1, false),
|
|
1581
1679
|
prev: () => this.navigate(this.prevIndex(), -1, false),
|
|
1582
1680
|
close: () => this.close(),
|
|
1681
|
+
closeFromSwipe: (frozenDrag) => this.closeFromSwipe(frozenDrag),
|
|
1682
|
+
setControlsHiddenForDrag: (hidden) => this.setControlsHiddenForDrag(hidden),
|
|
1583
1683
|
canClose: () => this.closable,
|
|
1584
1684
|
onActivity: () => this.onActivity(),
|
|
1585
1685
|
isZoomed: () => {
|
|
@@ -1761,15 +1861,28 @@ class Gallery {
|
|
|
1761
1861
|
this.autoHideTimer = null;
|
|
1762
1862
|
}
|
|
1763
1863
|
if (!this.opened) return;
|
|
1864
|
+
if (this.autoHideDelay === false) return;
|
|
1764
1865
|
if (this.autoHideDelay === 0) {
|
|
1765
1866
|
this.hideControls();
|
|
1766
1867
|
return;
|
|
1767
1868
|
}
|
|
1768
1869
|
this.autoHideTimer = setTimeout(() => this.hideControls(), this.autoHideDelay);
|
|
1769
1870
|
}
|
|
1770
|
-
/**
|
|
1871
|
+
/**
|
|
1872
|
+
* Forces the same fade §2.8's idle timer would eventually trigger —
|
|
1873
|
+
* public so a plugin can hide controls on its own trigger. Same
|
|
1874
|
+
* `isControlActive()` guard as the timer. Also a no-op under
|
|
1875
|
+
* `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's
|
|
1876
|
+
* tap-to-toggle-chrome behavior called this directly and ignored `false`
|
|
1877
|
+
* entirely, since only the idle timer checked it. `false` has to hold for
|
|
1878
|
+
* every caller, not just the timer — `mobileSettings.controls: false` is
|
|
1879
|
+
* affected the same way, by design. `forceHideControls()`/
|
|
1880
|
+
* `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't
|
|
1881
|
+
* check this — direct user actions with their own feedback, not auto-hide.
|
|
1882
|
+
*/
|
|
1771
1883
|
hideControls() {
|
|
1772
|
-
if (!this.dom || this.autoHidden || this.isControlActive())
|
|
1884
|
+
if (!this.dom || this.autoHidden || this.isControlActive() || this.autoHideDelay === false)
|
|
1885
|
+
return;
|
|
1773
1886
|
this.autoHidden = true;
|
|
1774
1887
|
this.dom.dialog.classList.add("shoji-controls-hidden");
|
|
1775
1888
|
this.bus.emit("controls:hide", {});
|
|
@@ -1996,6 +2109,31 @@ class Gallery {
|
|
|
1996
2109
|
}
|
|
1997
2110
|
}
|
|
1998
2111
|
close() {
|
|
2112
|
+
this.beginClose();
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed
|
|
2116
|
+
* vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own
|
|
2117
|
+
* `dragStart`, so the whole close continues as one motion from exactly
|
|
2118
|
+
* where the drag left off (see `GestureController.
|
|
2119
|
+
* takeFrozenDragTransform`).
|
|
2120
|
+
*/
|
|
2121
|
+
closeFromSwipe(frozenDrag) {
|
|
2122
|
+
this.beginClose(frozenDrag);
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a
|
|
2126
|
+
* button-close, which has no drag to continue from. Controls fade and the
|
|
2127
|
+
* zoom-out run concurrently, both starting the instant `forceHideControls()`
|
|
2128
|
+
* runs — not sequenced one after the other. (A previous version waited for
|
|
2129
|
+
* the controls' own fade to fully finish before starting the zoom-out, for
|
|
2130
|
+
* a button-close specifically — requested directly, to avoid stationary
|
|
2131
|
+
* chrome hovering over an already-shrinking photo. Reversed on later,
|
|
2132
|
+
* explicit feedback: waiting read as two distinct steps rather than one
|
|
2133
|
+
* motion; starting both together still avoids stationary chrome, since
|
|
2134
|
+
* the chrome is disappearing too, just without the pause.)
|
|
2135
|
+
*/
|
|
2136
|
+
beginClose(frozenDrag) {
|
|
1999
2137
|
var _a, _b;
|
|
2000
2138
|
if (this.destroyed || !this.opened || this.isClosing) return;
|
|
2001
2139
|
this.bus.emit("beforeClose", {});
|
|
@@ -2006,11 +2144,41 @@ class Gallery {
|
|
|
2006
2144
|
const naturalSize = this.resolveNaturalSize(this.activeIndex);
|
|
2007
2145
|
if (media && origin && (((_b = this.slides) == null ? void 0 : _b.isActiveReady()) || naturalSize)) {
|
|
2008
2146
|
const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);
|
|
2009
|
-
|
|
2147
|
+
this.forceHideControls();
|
|
2148
|
+
if (this.dom) this.dom.backdrop.style.opacity = "0";
|
|
2149
|
+
zoomOut(
|
|
2150
|
+
{ origin, target: media, aspectRatio, naturalSize, dragStart: frozenDrag },
|
|
2151
|
+
() => this.finishClose()
|
|
2152
|
+
);
|
|
2010
2153
|
} else {
|
|
2011
2154
|
this.finishClose();
|
|
2012
2155
|
}
|
|
2013
2156
|
}
|
|
2157
|
+
/** 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. */
|
|
2158
|
+
forceHideControls() {
|
|
2159
|
+
if (!this.dom || this.autoHidden) return;
|
|
2160
|
+
this.autoHidden = true;
|
|
2161
|
+
this.dom.dialog.classList.add("shoji-controls-hidden");
|
|
2162
|
+
this.bus.emit("controls:hide", {});
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide
|
|
2166
|
+
* past the same distance a release would close, reveal again on retreat.
|
|
2167
|
+
* `hidden: false` only reveals if visible when *this* gesture started
|
|
2168
|
+
* (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already
|
|
2169
|
+
* separately hidden (idle, or `autoHideDelay: 0`) before the drag began.
|
|
2170
|
+
* `dragCloseThreshold` (bus event) fires on every crossing regardless of
|
|
2171
|
+
* that guard — Autoplay (§4-autoplay) needs to know about the crossing
|
|
2172
|
+
* itself, not just whether controls visibly moved.
|
|
2173
|
+
*/
|
|
2174
|
+
setControlsHiddenForDrag(hidden) {
|
|
2175
|
+
var _a;
|
|
2176
|
+
this.controlsHiddenByDrag = hidden;
|
|
2177
|
+
if (hidden) this.forceHideControls();
|
|
2178
|
+
else if (!this.controlsHiddenAtGestureStart) this.showControls();
|
|
2179
|
+
(_a = this.dom) == null ? void 0 : _a.dialog.classList.toggle("shoji-controls-hidden-for-drag", hidden);
|
|
2180
|
+
this.bus.emit("dragCloseThreshold", { hidden });
|
|
2181
|
+
}
|
|
2014
2182
|
/** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
|
|
2015
2183
|
resolveAspectRatio(index, origin) {
|
|
2016
2184
|
const item = this.itemList[index];
|
|
@@ -2041,7 +2209,7 @@ class Gallery {
|
|
|
2041
2209
|
* instead of a second `close`/`afterClose` emission on a torn-down gallery.
|
|
2042
2210
|
*/
|
|
2043
2211
|
finishClose() {
|
|
2044
|
-
var _a, _b;
|
|
2212
|
+
var _a, _b, _c;
|
|
2045
2213
|
if (!this.opened) return;
|
|
2046
2214
|
this.isClosing = false;
|
|
2047
2215
|
this.opened = false;
|
|
@@ -2049,13 +2217,16 @@ class Gallery {
|
|
|
2049
2217
|
document.removeEventListener("keydown", this.onKeydown);
|
|
2050
2218
|
this.focusTrap.deactivate();
|
|
2051
2219
|
(_a = this.dom) == null ? void 0 : _a.outer.classList.remove("shoji-open");
|
|
2220
|
+
if (this.dom) this.dom.backdrop.style.opacity = "";
|
|
2052
2221
|
if (this.autoHideTimer !== null) {
|
|
2053
2222
|
clearTimeout(this.autoHideTimer);
|
|
2054
2223
|
this.autoHideTimer = null;
|
|
2055
2224
|
}
|
|
2056
2225
|
this.autoHidden = false;
|
|
2057
2226
|
this.hoveredControlCount = 0;
|
|
2227
|
+
this.controlsHiddenByDrag = false;
|
|
2058
2228
|
(_b = this.dom) == null ? void 0 : _b.dialog.classList.remove("shoji-controls-hidden");
|
|
2229
|
+
(_c = this.dom) == null ? void 0 : _c.dialog.classList.remove("shoji-controls-hidden-for-drag");
|
|
2059
2230
|
this.bus.emit("close", {});
|
|
2060
2231
|
this.bus.emit("afterClose", {});
|
|
2061
2232
|
}
|