zkit-ui 2.0.4 → 2.0.6
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 +56 -0
- package/android/build.gradle +100 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewLayoutSupport.java +450 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewMediaItem.java +36 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewPagerAdapter.java +140 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementGeometry.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementState.java +25 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSourceViewRegistry.java +115 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewTransitionSnapshotView.java +60 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java +338 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewView.kt +305 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerOverlayView.java +636 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerPageView.java +357 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerTransitionController.java +223 -0
- package/android/src/main/res/layout/native_image_preview_player_view.xml +9 -0
- package/dist/all.d.ts +2 -0
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js +24 -22
- package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +4 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +150 -0
- package/dist/ui/NativeImagePreview/context.d.ts +14 -0
- package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/context.js +38 -0
- package/dist/ui/NativeImagePreview/index.d.ts +3 -0
- package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/index.js +5 -0
- package/dist/ui/NativeImagePreview/types.d.ts +68 -0
- package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/types.js +2 -0
- package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
- package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/utils.js +83 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/media.js +90 -0
- package/dist/ui/NativeImagePreview/web/styles.css +137 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/transition.js +77 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/types.js +3 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
- package/expo-module.config.json +11 -0
- package/ios/NativeImagePreview/NativeImagePreviewImageLoader.swift +73 -0
- package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
- package/ios/NativeImagePreview/NativeImagePreviewLayoutSupport.swift +143 -0
- package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
- package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
- package/ios/NativeImagePreview/NativeImagePreviewVideoPlayerView.swift +437 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerConfiguration.swift +51 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerController.swift +745 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerPageView.swift +623 -0
- package/ios/NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift +312 -0
- package/ios/NativeImagePreviewModule.swift +46 -0
- package/ios/NativeImagePreviewView.swift +252 -0
- package/package.json +16 -2
- package/src/all.ts +18 -0
- package/src/services/ImagePreviewService/README.md +19 -0
- package/src/types.d.ts +1 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
- package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +201 -0
- package/src/ui/NativeImagePreview/README.md +111 -0
- package/src/ui/NativeImagePreview/context.tsx +22 -0
- package/src/ui/NativeImagePreview/index.tsx +18 -0
- package/src/ui/NativeImagePreview/types.ts +94 -0
- package/src/ui/NativeImagePreview/utils.ts +108 -0
- package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
- package/src/ui/NativeImagePreview/web/media.ts +91 -0
- package/src/ui/NativeImagePreview/web/styles.css +137 -0
- package/src/ui/NativeImagePreview/web/transition.ts +111 -0
- package/src/ui/NativeImagePreview/web/types.ts +88 -0
- package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
- package/zkit-ui.podspec +13 -1
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
NativeImagePreviewWebChangeMeta,
|
|
5
|
+
NativeImagePreviewWebCloseMeta,
|
|
6
|
+
NativeImagePreviewWebCloseReason,
|
|
7
|
+
NativeImagePreviewWebController,
|
|
8
|
+
NativeImagePreviewWebImageSize,
|
|
9
|
+
NativeImagePreviewWebObjectFit,
|
|
10
|
+
NativeImagePreviewWebOpenOptions,
|
|
11
|
+
NativeImagePreviewWebRect,
|
|
12
|
+
NativeImagePreviewWebResolvedItem,
|
|
13
|
+
NativeImagePreviewWebSourceVisibility,
|
|
14
|
+
} from './types';
|
|
15
|
+
import {
|
|
16
|
+
clamp,
|
|
17
|
+
isUsableRect,
|
|
18
|
+
normalizeRect,
|
|
19
|
+
rectFromDOMRect,
|
|
20
|
+
rectFromElement,
|
|
21
|
+
sharedGeometryFor,
|
|
22
|
+
targetGeometryFor,
|
|
23
|
+
type SharedElementGeometry,
|
|
24
|
+
} from './geometry';
|
|
25
|
+
import { normalizeItems, previewSourceFor, resolveMediaSize, waitForElementImage } from './media';
|
|
26
|
+
import { animateSharedElement } from './transition';
|
|
27
|
+
|
|
28
|
+
type Point = {
|
|
29
|
+
id: number;
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type SlideState = {
|
|
35
|
+
scale: number;
|
|
36
|
+
translateX: number;
|
|
37
|
+
translateY: number;
|
|
38
|
+
lastTapAt: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type GestureMode = 'none' | 'pan' | 'pinch' | 'swipe' | 'dismiss';
|
|
42
|
+
|
|
43
|
+
const MAX_SCALE = 5;
|
|
44
|
+
const DOUBLE_TAP_SCALE = 2.5;
|
|
45
|
+
const DRAG_DISMISS_DISTANCE = 72;
|
|
46
|
+
const DRAG_DISMISS_MIN_FLING_DISTANCE = 24;
|
|
47
|
+
const DRAG_DISMISS_VELOCITY = 520;
|
|
48
|
+
const SWIPE_DISTANCE = 52;
|
|
49
|
+
const SWIPE_MIN_FLING_DISTANCE = 22;
|
|
50
|
+
const SWIPE_VELOCITY = 480;
|
|
51
|
+
const MODE_LOCK_DISTANCE = 8;
|
|
52
|
+
|
|
53
|
+
export class NativeImagePreviewWebViewer implements NativeImagePreviewWebController {
|
|
54
|
+
private root: HTMLElement | null = null;
|
|
55
|
+
private backdrop: HTMLElement | null = null;
|
|
56
|
+
private viewport: HTMLElement | null = null;
|
|
57
|
+
private track: HTMLElement | null = null;
|
|
58
|
+
private closeButton: HTMLButtonElement | null = null;
|
|
59
|
+
private counter: HTMLElement | null = null;
|
|
60
|
+
private mediaElements: HTMLElement[] = [];
|
|
61
|
+
private slideStates: SlideState[] = [];
|
|
62
|
+
private items: NativeImagePreviewWebResolvedItem[] = [];
|
|
63
|
+
private options: NativeImagePreviewWebOpenOptions | null = null;
|
|
64
|
+
private index = 0;
|
|
65
|
+
private openState = false;
|
|
66
|
+
private closing = false;
|
|
67
|
+
private sourceVisibility: NativeImagePreviewWebSourceVisibility = 'hidden';
|
|
68
|
+
private sourceElement: HTMLElement | null = null;
|
|
69
|
+
private sourcePreviousVisibility = '';
|
|
70
|
+
private pointers = new Map<number, Point>();
|
|
71
|
+
private mode: GestureMode = 'none';
|
|
72
|
+
private downX = 0;
|
|
73
|
+
private downY = 0;
|
|
74
|
+
private lastX = 0;
|
|
75
|
+
private lastY = 0;
|
|
76
|
+
private downAt = 0;
|
|
77
|
+
private dragX = 0;
|
|
78
|
+
private dragY = 0;
|
|
79
|
+
private pinchStartDistance = 1;
|
|
80
|
+
private pinchStartScale = 1;
|
|
81
|
+
private pinchStartTranslateX = 0;
|
|
82
|
+
private pinchStartTranslateY = 0;
|
|
83
|
+
private pinchStartCenterX = 0;
|
|
84
|
+
private pinchStartCenterY = 0;
|
|
85
|
+
private previousBodyOverflow = '';
|
|
86
|
+
private previousHtmlOverflow = '';
|
|
87
|
+
private viewportWidth = 1;
|
|
88
|
+
private viewportHeight = 1;
|
|
89
|
+
|
|
90
|
+
async open(options: NativeImagePreviewWebOpenOptions): Promise<void> {
|
|
91
|
+
if (this.openState) {
|
|
92
|
+
await this.close('replace');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const items = normalizeItems(options.items);
|
|
96
|
+
if (!items.length) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.items = items;
|
|
102
|
+
this.index = clamp(Math.trunc(options.index ?? 0), 0, items.length - 1);
|
|
103
|
+
this.sourceVisibility = options.sourceVisibility ?? 'hidden';
|
|
104
|
+
this.sourceElement = this.resolveSourceElement(this.index);
|
|
105
|
+
this.slideStates = items.map(() => this.createSlideState());
|
|
106
|
+
this.openState = true;
|
|
107
|
+
this.closing = false;
|
|
108
|
+
this.captureViewportSize();
|
|
109
|
+
|
|
110
|
+
this.lockDocumentScroll();
|
|
111
|
+
this.mount();
|
|
112
|
+
await this.layoutMediaElements();
|
|
113
|
+
this.updateCounter();
|
|
114
|
+
this.updateTrack();
|
|
115
|
+
|
|
116
|
+
await this.nextFrame();
|
|
117
|
+
await this.performOpenTransition();
|
|
118
|
+
this.showContent();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async close(reason: NativeImagePreviewWebCloseReason = 'api'): Promise<void> {
|
|
122
|
+
if (!this.openState || this.closing) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.closing = true;
|
|
126
|
+
await this.performCloseTransition(reason);
|
|
127
|
+
this.finishClose(reason);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
destroy(): void {
|
|
131
|
+
if (!this.openState) {
|
|
132
|
+
this.unmount();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.finishClose('api');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getIndex(): number {
|
|
140
|
+
return this.index;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isOpen(): boolean {
|
|
144
|
+
return this.openState;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private mount(): void {
|
|
148
|
+
const root = document.createElement('div');
|
|
149
|
+
root.className = `native-image-preview-root native-image-preview-theme-${this.options?.theme ?? 'dark'}`;
|
|
150
|
+
root.tabIndex = -1;
|
|
151
|
+
this.applyViewportStyle(root);
|
|
152
|
+
|
|
153
|
+
const backdrop = document.createElement('div');
|
|
154
|
+
backdrop.className = 'native-image-preview-backdrop';
|
|
155
|
+
backdrop.style.opacity = '0';
|
|
156
|
+
root.append(backdrop);
|
|
157
|
+
|
|
158
|
+
const viewport = document.createElement('div');
|
|
159
|
+
viewport.className = 'native-image-preview-viewport';
|
|
160
|
+
|
|
161
|
+
const track = document.createElement('div');
|
|
162
|
+
track.className = 'native-image-preview-track';
|
|
163
|
+
track.style.opacity = '0';
|
|
164
|
+
|
|
165
|
+
this.items.forEach((item, index) => {
|
|
166
|
+
const slide = document.createElement('div');
|
|
167
|
+
slide.className = 'native-image-preview-slide';
|
|
168
|
+
slide.dataset.index = String(index);
|
|
169
|
+
const media = this.createMediaElement(item);
|
|
170
|
+
this.mediaElements.push(media);
|
|
171
|
+
slide.append(media);
|
|
172
|
+
track.append(slide);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
viewport.append(track);
|
|
176
|
+
root.append(viewport);
|
|
177
|
+
|
|
178
|
+
const closeButton = document.createElement('button');
|
|
179
|
+
closeButton.className = 'native-image-preview-close';
|
|
180
|
+
closeButton.type = 'button';
|
|
181
|
+
closeButton.textContent = '×';
|
|
182
|
+
closeButton.setAttribute('aria-label', 'Close image preview');
|
|
183
|
+
closeButton.hidden = this.options?.closeButton === false;
|
|
184
|
+
closeButton.style.opacity = '0';
|
|
185
|
+
root.append(closeButton);
|
|
186
|
+
|
|
187
|
+
const counter = document.createElement('div');
|
|
188
|
+
counter.className = 'native-image-preview-counter';
|
|
189
|
+
counter.hidden = this.options?.counter === false || this.items.length <= 1;
|
|
190
|
+
counter.style.opacity = '0';
|
|
191
|
+
root.append(counter);
|
|
192
|
+
|
|
193
|
+
this.root = root;
|
|
194
|
+
this.backdrop = backdrop;
|
|
195
|
+
this.viewport = viewport;
|
|
196
|
+
this.track = track;
|
|
197
|
+
this.closeButton = closeButton;
|
|
198
|
+
this.counter = counter;
|
|
199
|
+
|
|
200
|
+
root.addEventListener('pointerdown', this.handlePointerDown);
|
|
201
|
+
root.addEventListener('pointermove', this.handlePointerMove);
|
|
202
|
+
root.addEventListener('pointerup', this.handlePointerUp);
|
|
203
|
+
root.addEventListener('pointercancel', this.handlePointerUp);
|
|
204
|
+
root.addEventListener('lostpointercapture', this.handleLostPointerCapture);
|
|
205
|
+
root.addEventListener('wheel', this.handleWheel, { passive: false });
|
|
206
|
+
root.addEventListener('click', this.handleRootClick);
|
|
207
|
+
closeButton.addEventListener('click', this.handleCloseClick);
|
|
208
|
+
window.addEventListener('keydown', this.handleKeyDown);
|
|
209
|
+
window.addEventListener('resize', this.handleResize);
|
|
210
|
+
window.addEventListener('blur', this.handleWindowBlur);
|
|
211
|
+
window.addEventListener('pagehide', this.handleWindowBlur);
|
|
212
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
213
|
+
|
|
214
|
+
document.body.append(root);
|
|
215
|
+
root.focus({ preventScroll: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private createMediaElement(item: NativeImagePreviewWebResolvedItem): HTMLElement {
|
|
219
|
+
if (item.type === 'video') {
|
|
220
|
+
const video = document.createElement('video');
|
|
221
|
+
video.className = 'native-image-preview-media native-image-preview-video';
|
|
222
|
+
video.src = item.src;
|
|
223
|
+
video.poster = item.poster ?? '';
|
|
224
|
+
if (item.width && item.height) {
|
|
225
|
+
video.width = item.width;
|
|
226
|
+
video.height = item.height;
|
|
227
|
+
}
|
|
228
|
+
video.controls = true;
|
|
229
|
+
video.playsInline = true;
|
|
230
|
+
video.preload = 'metadata';
|
|
231
|
+
return video;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const image = document.createElement('img');
|
|
235
|
+
image.className = 'native-image-preview-media native-image-preview-image';
|
|
236
|
+
image.src = item.src;
|
|
237
|
+
image.alt = item.alt;
|
|
238
|
+
if (item.width && item.height) {
|
|
239
|
+
image.width = item.width;
|
|
240
|
+
image.height = item.height;
|
|
241
|
+
}
|
|
242
|
+
image.draggable = false;
|
|
243
|
+
image.decoding = 'async';
|
|
244
|
+
return image;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async performOpenTransition(): Promise<void> {
|
|
248
|
+
const root = this.root;
|
|
249
|
+
const backdrop = this.backdrop;
|
|
250
|
+
const track = this.track;
|
|
251
|
+
const item = this.items[this.index];
|
|
252
|
+
if (!root || !backdrop || !track || !item) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
track.style.opacity = '0';
|
|
257
|
+
backdrop.style.opacity = '0';
|
|
258
|
+
|
|
259
|
+
const sourceGeometry = await this.resolveSourceGeometry(item);
|
|
260
|
+
const targetGeometry = await this.resolveTargetGeometry(item);
|
|
261
|
+
if (!sourceGeometry || !targetGeometry) {
|
|
262
|
+
backdrop.style.opacity = '1';
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await animateSharedElement({
|
|
267
|
+
item,
|
|
268
|
+
from: sourceGeometry,
|
|
269
|
+
to: targetGeometry,
|
|
270
|
+
parent: root,
|
|
271
|
+
beforeStart: () => {
|
|
272
|
+
this.hideSourceElement();
|
|
273
|
+
},
|
|
274
|
+
onProgress: (progress) => {
|
|
275
|
+
backdrop.style.opacity = String(progress);
|
|
276
|
+
},
|
|
277
|
+
beforeRemove: () => {
|
|
278
|
+
this.showContent();
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async performCloseTransition(reason: NativeImagePreviewWebCloseReason): Promise<void> {
|
|
284
|
+
const root = this.root;
|
|
285
|
+
const backdrop = this.backdrop;
|
|
286
|
+
const track = this.track;
|
|
287
|
+
const item = this.items[this.index];
|
|
288
|
+
const media = this.mediaElements[this.index];
|
|
289
|
+
if (!root || !backdrop || !track || !item || !media) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.pauseInactiveVideos(-1);
|
|
294
|
+
this.hideControls();
|
|
295
|
+
|
|
296
|
+
const from = await this.resolveCurrentMediaGeometry(item, media);
|
|
297
|
+
const to = await this.resolveReturnGeometry(item);
|
|
298
|
+
if (!from || !to) {
|
|
299
|
+
root.classList.add('native-image-preview-fading-out');
|
|
300
|
+
await this.delay(180);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const backdropStartOpacity = this.resolveBackdropCloseStartOpacity(backdrop, reason);
|
|
305
|
+
await animateSharedElement({
|
|
306
|
+
item,
|
|
307
|
+
from,
|
|
308
|
+
to,
|
|
309
|
+
parent: root,
|
|
310
|
+
duration: reason === 'drag' ? 260 : 300,
|
|
311
|
+
beforeStart: () => {
|
|
312
|
+
track.style.opacity = '0';
|
|
313
|
+
},
|
|
314
|
+
onProgress: (progress) => {
|
|
315
|
+
backdrop.style.opacity = String(backdropStartOpacity * (1 - progress));
|
|
316
|
+
},
|
|
317
|
+
beforeRemove: () => {
|
|
318
|
+
this.restoreSourceElement();
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private resolveBackdropCloseStartOpacity(
|
|
324
|
+
backdrop: HTMLElement,
|
|
325
|
+
reason: NativeImagePreviewWebCloseReason
|
|
326
|
+
): number {
|
|
327
|
+
if (reason !== 'drag') {
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
const opacity = Number.parseFloat(getComputedStyle(backdrop).opacity);
|
|
331
|
+
return Number.isFinite(opacity) ? clamp(opacity, 0, 1) : 1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private showContent(): void {
|
|
335
|
+
if (!this.track || !this.backdrop) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.backdrop.style.opacity = '1';
|
|
339
|
+
this.track.style.opacity = '1';
|
|
340
|
+
this.showControls();
|
|
341
|
+
this.applyActiveMediaTransform(false);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private showControls(): void {
|
|
345
|
+
if (this.closeButton && !this.closeButton.hidden) {
|
|
346
|
+
this.closeButton.style.opacity = '1';
|
|
347
|
+
}
|
|
348
|
+
if (this.counter && !this.counter.hidden) {
|
|
349
|
+
this.counter.style.opacity = '1';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private hideControls(): void {
|
|
354
|
+
if (this.closeButton) {
|
|
355
|
+
this.closeButton.style.opacity = '0';
|
|
356
|
+
}
|
|
357
|
+
if (this.counter) {
|
|
358
|
+
this.counter.style.opacity = '0';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private finishClose(reason: NativeImagePreviewWebCloseReason): void {
|
|
363
|
+
const item = this.items[this.index] ?? null;
|
|
364
|
+
const index = this.index;
|
|
365
|
+
this.restoreSourceElement();
|
|
366
|
+
this.unlockDocumentScroll();
|
|
367
|
+
this.unmount();
|
|
368
|
+
this.options?.onClose?.({ reason, index, item });
|
|
369
|
+
this.openState = false;
|
|
370
|
+
this.closing = false;
|
|
371
|
+
this.items = [];
|
|
372
|
+
this.options = null;
|
|
373
|
+
this.mediaElements = [];
|
|
374
|
+
this.slideStates = [];
|
|
375
|
+
this.pointers.clear();
|
|
376
|
+
this.mode = 'none';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private unmount(): void {
|
|
380
|
+
const root = this.root;
|
|
381
|
+
const closeButton = this.closeButton;
|
|
382
|
+
if (!root) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
root.removeEventListener('pointerdown', this.handlePointerDown);
|
|
386
|
+
root.removeEventListener('pointermove', this.handlePointerMove);
|
|
387
|
+
root.removeEventListener('pointerup', this.handlePointerUp);
|
|
388
|
+
root.removeEventListener('pointercancel', this.handlePointerUp);
|
|
389
|
+
root.removeEventListener('lostpointercapture', this.handleLostPointerCapture);
|
|
390
|
+
root.removeEventListener('wheel', this.handleWheel);
|
|
391
|
+
root.removeEventListener('click', this.handleRootClick);
|
|
392
|
+
closeButton?.removeEventListener('click', this.handleCloseClick);
|
|
393
|
+
window.removeEventListener('keydown', this.handleKeyDown);
|
|
394
|
+
window.removeEventListener('resize', this.handleResize);
|
|
395
|
+
window.removeEventListener('blur', this.handleWindowBlur);
|
|
396
|
+
window.removeEventListener('pagehide', this.handleWindowBlur);
|
|
397
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
398
|
+
root.remove();
|
|
399
|
+
this.root = null;
|
|
400
|
+
this.backdrop = null;
|
|
401
|
+
this.viewport = null;
|
|
402
|
+
this.track = null;
|
|
403
|
+
this.closeButton = null;
|
|
404
|
+
this.counter = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private captureViewportSize(): void {
|
|
408
|
+
const visualViewport = window.visualViewport;
|
|
409
|
+
this.viewportWidth = Math.max(1, visualViewport?.width ?? window.innerWidth);
|
|
410
|
+
this.viewportHeight = Math.max(1, visualViewport?.height ?? window.innerHeight);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private applyViewportStyle(root = this.root): void {
|
|
414
|
+
if (!root) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
root.style.setProperty('--native-image-preview-vw', `${this.viewportWidth}px`);
|
|
418
|
+
root.style.setProperty('--native-image-preview-vh', `${this.viewportHeight}px`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private viewportRect(): NativeImagePreviewWebRect {
|
|
422
|
+
return normalizeRect({
|
|
423
|
+
left: 0,
|
|
424
|
+
top: 0,
|
|
425
|
+
width: this.viewportWidth,
|
|
426
|
+
height: this.viewportHeight,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private async resolveSourceGeometry(item: NativeImagePreviewWebResolvedItem): Promise<SharedElementGeometry | null> {
|
|
431
|
+
const element = this.sourceElement;
|
|
432
|
+
const rect = this.options?.sourceRect
|
|
433
|
+
? rectFromDOMRect(this.options.sourceRect)
|
|
434
|
+
: element ? rectFromElement(element) : null;
|
|
435
|
+
if (!isUsableRect(rect)) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
await waitForElementImage(element);
|
|
439
|
+
const size = await this.resolveSharedMediaSize(item);
|
|
440
|
+
return sharedGeometryFor(rect, size, this.resolveSourceObjectFit(element));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private async resolveReturnGeometry(item: NativeImagePreviewWebResolvedItem): Promise<SharedElementGeometry | null> {
|
|
444
|
+
const element = this.resolveSourceElement(this.index);
|
|
445
|
+
if (!element || !element.isConnected) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
await waitForElementImage(element);
|
|
449
|
+
const rect = rectFromElement(element);
|
|
450
|
+
if (!isUsableRect(rect)) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const size = await this.resolveSharedMediaSize(item);
|
|
454
|
+
return sharedGeometryFor(rect, size, this.resolveSourceObjectFit(element));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async resolveTargetGeometry(item: NativeImagePreviewWebResolvedItem): Promise<SharedElementGeometry | null> {
|
|
458
|
+
const size = await this.resolveSharedMediaSize(item);
|
|
459
|
+
return targetGeometryFor(size, this.viewportRect());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async resolveCurrentMediaGeometry(
|
|
463
|
+
item: NativeImagePreviewWebResolvedItem,
|
|
464
|
+
media: HTMLElement
|
|
465
|
+
): Promise<SharedElementGeometry | null> {
|
|
466
|
+
const rect = rectFromElement(media);
|
|
467
|
+
if (!isUsableRect(rect)) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const size = await this.resolveSharedMediaSize(item);
|
|
471
|
+
return sharedGeometryFor(rect, size, 'fill');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async resolveSharedMediaSize(item: NativeImagePreviewWebResolvedItem): Promise<NativeImagePreviewWebImageSize> {
|
|
475
|
+
return resolveMediaSize(item, this.options?.sourceImageSize);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private resolveSourceObjectFit(element: HTMLElement | null): NativeImagePreviewWebObjectFit {
|
|
479
|
+
const optionFit = this.options?.sourceObjectFit ?? this.options?.objectFit;
|
|
480
|
+
if (optionFit) {
|
|
481
|
+
return optionFit;
|
|
482
|
+
}
|
|
483
|
+
if (!element) {
|
|
484
|
+
return 'cover';
|
|
485
|
+
}
|
|
486
|
+
const fit = getComputedStyle(element).objectFit;
|
|
487
|
+
if (fit === 'contain' || fit === 'fill') {
|
|
488
|
+
return fit;
|
|
489
|
+
}
|
|
490
|
+
return 'cover';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private resolveSourceElement(index: number): HTMLElement | null {
|
|
494
|
+
const item = this.items[index];
|
|
495
|
+
if (!item) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
return this.options?.getSourceElement?.(index, item)
|
|
499
|
+
?? this.options?.sourceElements?.[index]
|
|
500
|
+
?? (index === (this.options?.index ?? 0) ? this.options?.sourceElement ?? null : null);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private hideSourceElement(): void {
|
|
504
|
+
if (this.sourceVisibility !== 'hidden' || !this.sourceElement) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.sourcePreviousVisibility = this.sourceElement.style.visibility;
|
|
508
|
+
this.sourceElement.style.visibility = 'hidden';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private restoreSourceElement(): void {
|
|
512
|
+
if (!this.sourceElement) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
this.sourceElement.style.visibility = this.sourcePreviousVisibility;
|
|
516
|
+
this.sourceElement = null;
|
|
517
|
+
this.sourcePreviousVisibility = '';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private syncHiddenSourceElement(index: number): void {
|
|
521
|
+
this.restoreSourceElement();
|
|
522
|
+
this.sourceElement = this.resolveSourceElement(index);
|
|
523
|
+
this.hideSourceElement();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private handlePointerDown = (event: PointerEvent): void => {
|
|
527
|
+
if (this.closing || !this.root) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const target = event.target as HTMLElement | null;
|
|
531
|
+
if (this.shouldIgnorePointerStart(target, event)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this.root.setPointerCapture(event.pointerId);
|
|
536
|
+
this.pointers.set(event.pointerId, { id: event.pointerId, x: event.clientX, y: event.clientY });
|
|
537
|
+
this.downX = event.clientX;
|
|
538
|
+
this.downY = event.clientY;
|
|
539
|
+
this.lastX = event.clientX;
|
|
540
|
+
this.lastY = event.clientY;
|
|
541
|
+
this.downAt = performance.now();
|
|
542
|
+
this.dragX = 0;
|
|
543
|
+
this.dragY = 0;
|
|
544
|
+
this.mode = this.pointers.size >= 2 ? 'pinch' : 'none';
|
|
545
|
+
this.preparePinchIfNeeded();
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
private shouldIgnorePointerStart(target: HTMLElement | null, event: PointerEvent): boolean {
|
|
549
|
+
if (!target) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
if (target.closest('button')) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const video = target.closest('video');
|
|
557
|
+
if (!video) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
return this.isPointerInVideoControls(video, event);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private isPointerInVideoControls(video: HTMLVideoElement, event: PointerEvent): boolean {
|
|
564
|
+
if (!video.controls) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const rect = video.getBoundingClientRect();
|
|
569
|
+
if (!isUsableRect(rectFromDOMRect(rect))) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const controlsHeight = Math.min(72, Math.max(44, rect.height * 0.24));
|
|
574
|
+
return event.clientY >= rect.bottom - controlsHeight;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private handlePointerMove = (event: PointerEvent): void => {
|
|
578
|
+
if (this.closing || !this.pointers.has(event.pointerId)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
this.pointers.set(event.pointerId, { id: event.pointerId, x: event.clientX, y: event.clientY });
|
|
583
|
+
|
|
584
|
+
if (this.pointers.size >= 2) {
|
|
585
|
+
this.mode = 'pinch';
|
|
586
|
+
this.applyPinch();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const state = this.activeState();
|
|
591
|
+
const dx = event.clientX - this.downX;
|
|
592
|
+
const dy = event.clientY - this.downY;
|
|
593
|
+
const frameDx = event.clientX - this.lastX;
|
|
594
|
+
const frameDy = event.clientY - this.lastY;
|
|
595
|
+
this.lastX = event.clientX;
|
|
596
|
+
this.lastY = event.clientY;
|
|
597
|
+
|
|
598
|
+
if (this.mode === 'none') {
|
|
599
|
+
if (Math.hypot(dx, dy) < MODE_LOCK_DISTANCE) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (state.scale > 1.02) {
|
|
603
|
+
this.mode = 'pan';
|
|
604
|
+
} else if (Math.abs(dy) > Math.abs(dx) * 1.15) {
|
|
605
|
+
this.mode = 'dismiss';
|
|
606
|
+
} else {
|
|
607
|
+
this.mode = 'swipe';
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (this.mode === 'pan') {
|
|
612
|
+
state.translateX += frameDx;
|
|
613
|
+
state.translateY += frameDy;
|
|
614
|
+
this.clampActiveState();
|
|
615
|
+
this.applyActiveMediaTransform(false);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (this.mode === 'dismiss') {
|
|
620
|
+
this.applyDismissDrag(dy);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (this.mode === 'swipe') {
|
|
625
|
+
this.dragX = dx;
|
|
626
|
+
this.updateTrack();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
private handlePointerUp = (event: PointerEvent): void => {
|
|
631
|
+
if (!this.pointers.has(event.pointerId)) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const finalDx = event.clientX - this.downX;
|
|
635
|
+
const finalDy = event.clientY - this.downY;
|
|
636
|
+
this.pointers.delete(event.pointerId);
|
|
637
|
+
if (this.root?.hasPointerCapture(event.pointerId)) {
|
|
638
|
+
this.root.releasePointerCapture(event.pointerId);
|
|
639
|
+
}
|
|
640
|
+
if (this.pointers.size >= 2) {
|
|
641
|
+
this.preparePinchIfNeeded();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (this.pointers.size === 1) {
|
|
645
|
+
const point = [...this.pointers.values()][0];
|
|
646
|
+
this.downX = point.x;
|
|
647
|
+
this.downY = point.y;
|
|
648
|
+
this.lastX = point.x;
|
|
649
|
+
this.lastY = point.y;
|
|
650
|
+
this.mode = 'none';
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const elapsed = Math.max(1, performance.now() - this.downAt);
|
|
655
|
+
const velocityX = (event.clientX - this.downX) / elapsed * 1000;
|
|
656
|
+
const velocityY = (event.clientY - this.downY) / elapsed * 1000;
|
|
657
|
+
|
|
658
|
+
if (this.mode === 'dismiss') {
|
|
659
|
+
this.applyDismissDrag(finalDy);
|
|
660
|
+
const shouldClose = Math.abs(this.dragY) > DRAG_DISMISS_DISTANCE
|
|
661
|
+
|| (
|
|
662
|
+
Math.abs(this.dragY) > DRAG_DISMISS_MIN_FLING_DISTANCE
|
|
663
|
+
&& Math.abs(velocityY) > DRAG_DISMISS_VELOCITY
|
|
664
|
+
);
|
|
665
|
+
if (shouldClose) {
|
|
666
|
+
void this.close('drag');
|
|
667
|
+
} else {
|
|
668
|
+
this.resetDismissDrag();
|
|
669
|
+
}
|
|
670
|
+
} else if (this.mode === 'swipe') {
|
|
671
|
+
this.dragX = finalDx;
|
|
672
|
+
this.finishSwipe(velocityX);
|
|
673
|
+
} else if (this.mode === 'none') {
|
|
674
|
+
this.handleTap(event);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
this.mode = 'none';
|
|
678
|
+
this.dragX = 0;
|
|
679
|
+
this.dragY = 0;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
private handleWheel = (event: WheelEvent): void => {
|
|
683
|
+
if (!event.ctrlKey && Math.abs(event.deltaY) < Math.abs(event.deltaX)) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
event.preventDefault();
|
|
687
|
+
const state = this.activeState();
|
|
688
|
+
const nextScale = clamp(state.scale * (event.deltaY > 0 ? 0.92 : 1.08), 1, MAX_SCALE);
|
|
689
|
+
state.scale = nextScale;
|
|
690
|
+
if (nextScale <= 1.01) {
|
|
691
|
+
state.translateX = 0;
|
|
692
|
+
state.translateY = 0;
|
|
693
|
+
}
|
|
694
|
+
this.clampActiveState();
|
|
695
|
+
this.applyActiveMediaTransform(true);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
private handleRootClick = (event: MouseEvent): void => {
|
|
699
|
+
if (event.target === this.backdrop) {
|
|
700
|
+
void this.close('backdrop');
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
private handleCloseClick = (): void => {
|
|
705
|
+
void this.close('button');
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
private handleKeyDown = (event: KeyboardEvent): void => {
|
|
709
|
+
if (!this.openState) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (event.key === 'Escape') {
|
|
713
|
+
void this.close('escape');
|
|
714
|
+
}
|
|
715
|
+
if (event.key === 'ArrowLeft') {
|
|
716
|
+
this.goTo(this.index - 1, 'api');
|
|
717
|
+
}
|
|
718
|
+
if (event.key === 'ArrowRight') {
|
|
719
|
+
this.goTo(this.index + 1, 'api');
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
private handleResize = (): void => {
|
|
724
|
+
this.captureViewportSize();
|
|
725
|
+
this.applyViewportStyle();
|
|
726
|
+
void this.layoutMediaElements().then(() => {
|
|
727
|
+
this.clampActiveState();
|
|
728
|
+
this.updateTrack();
|
|
729
|
+
this.applyActiveMediaTransform(false);
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
private handleWindowBlur = (): void => {
|
|
734
|
+
this.cancelActiveGesture();
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
private handleVisibilityChange = (): void => {
|
|
738
|
+
if (document.visibilityState !== 'visible') {
|
|
739
|
+
this.cancelActiveGesture();
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
private handleLostPointerCapture = (event: PointerEvent): void => {
|
|
744
|
+
const pointerId = event.pointerId;
|
|
745
|
+
window.setTimeout(() => {
|
|
746
|
+
if (this.pointers.has(pointerId)) {
|
|
747
|
+
this.cancelActiveGesture();
|
|
748
|
+
}
|
|
749
|
+
}, 80);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
private handleTap(event: PointerEvent): void {
|
|
753
|
+
const state = this.activeState();
|
|
754
|
+
const now = performance.now();
|
|
755
|
+
if (now - state.lastTapAt < 280) {
|
|
756
|
+
state.scale = state.scale > 1.02 ? 1 : DOUBLE_TAP_SCALE;
|
|
757
|
+
state.translateX = 0;
|
|
758
|
+
state.translateY = 0;
|
|
759
|
+
this.applyActiveMediaTransform(true);
|
|
760
|
+
state.lastTapAt = 0;
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
state.lastTapAt = now;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private preparePinchIfNeeded(): void {
|
|
768
|
+
if (this.pointers.size < 2) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const [first, second] = [...this.pointers.values()];
|
|
772
|
+
const state = this.activeState();
|
|
773
|
+
this.pinchStartDistance = Math.max(1, distance(first, second));
|
|
774
|
+
this.pinchStartScale = state.scale;
|
|
775
|
+
this.pinchStartTranslateX = state.translateX;
|
|
776
|
+
this.pinchStartTranslateY = state.translateY;
|
|
777
|
+
this.pinchStartCenterX = (first.x + second.x) / 2;
|
|
778
|
+
this.pinchStartCenterY = (first.y + second.y) / 2;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private applyPinch(): void {
|
|
782
|
+
if (this.pointers.size < 2) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const [first, second] = [...this.pointers.values()];
|
|
786
|
+
const state = this.activeState();
|
|
787
|
+
const nextDistance = Math.max(1, distance(first, second));
|
|
788
|
+
const centerX = (first.x + second.x) / 2;
|
|
789
|
+
const centerY = (first.y + second.y) / 2;
|
|
790
|
+
state.scale = clamp(this.pinchStartScale * nextDistance / this.pinchStartDistance, 1, MAX_SCALE);
|
|
791
|
+
state.translateX = this.pinchStartTranslateX + centerX - this.pinchStartCenterX;
|
|
792
|
+
state.translateY = this.pinchStartTranslateY + centerY - this.pinchStartCenterY;
|
|
793
|
+
if (state.scale <= 1.01) {
|
|
794
|
+
state.translateX = 0;
|
|
795
|
+
state.translateY = 0;
|
|
796
|
+
}
|
|
797
|
+
this.clampActiveState();
|
|
798
|
+
this.applyActiveMediaTransform(false);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private finishSwipe(velocityX: number): void {
|
|
802
|
+
const previous = this.index;
|
|
803
|
+
const shouldGoNext = this.dragX < -SWIPE_DISTANCE
|
|
804
|
+
|| (this.dragX < -SWIPE_MIN_FLING_DISTANCE && velocityX < -SWIPE_VELOCITY);
|
|
805
|
+
const shouldGoPrevious = this.dragX > SWIPE_DISTANCE
|
|
806
|
+
|| (this.dragX > SWIPE_MIN_FLING_DISTANCE && velocityX > SWIPE_VELOCITY);
|
|
807
|
+
if (shouldGoNext) {
|
|
808
|
+
this.goTo(this.index + 1, 'swipe');
|
|
809
|
+
} else if (shouldGoPrevious) {
|
|
810
|
+
this.goTo(this.index - 1, 'swipe');
|
|
811
|
+
}
|
|
812
|
+
if (previous === this.index) {
|
|
813
|
+
this.updateTrack(true);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private goTo(nextIndex: number, reason: 'api' | 'swipe'): void {
|
|
818
|
+
const clamped = clamp(nextIndex, 0, this.items.length - 1);
|
|
819
|
+
const previousIndex = this.index;
|
|
820
|
+
this.index = clamped;
|
|
821
|
+
this.dragX = 0;
|
|
822
|
+
this.resetSlideState(clamped);
|
|
823
|
+
this.updateTrack(true);
|
|
824
|
+
this.updateCounter();
|
|
825
|
+
this.pauseInactiveVideos(clamped);
|
|
826
|
+
if (previousIndex !== clamped) {
|
|
827
|
+
this.syncHiddenSourceElement(clamped);
|
|
828
|
+
const item = this.items[clamped];
|
|
829
|
+
const meta: NativeImagePreviewWebChangeMeta = { reason, previousIndex, index: clamped, item };
|
|
830
|
+
this.options?.onChange?.(clamped, meta);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private resetDismissDrag(): void {
|
|
835
|
+
this.dragX = 0;
|
|
836
|
+
this.dragY = 0;
|
|
837
|
+
this.updateTrack(true);
|
|
838
|
+
this.applyActiveMediaTransform(true);
|
|
839
|
+
if (this.backdrop) {
|
|
840
|
+
this.backdrop.style.transition = 'opacity 220ms cubic-bezier(0.22, 1, 0.36, 1)';
|
|
841
|
+
this.backdrop.style.opacity = '1';
|
|
842
|
+
window.setTimeout(() => {
|
|
843
|
+
if (this.backdrop) {
|
|
844
|
+
this.backdrop.style.transition = '';
|
|
845
|
+
}
|
|
846
|
+
}, 240);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private applyDismissDrag(dy: number): void {
|
|
851
|
+
const media = this.mediaElements[this.index];
|
|
852
|
+
this.dragX = 0;
|
|
853
|
+
this.dragY = dy;
|
|
854
|
+
this.updateTrack(false);
|
|
855
|
+
|
|
856
|
+
const progress = Math.min(1, Math.abs(dy) / Math.max(1, this.viewportHeight));
|
|
857
|
+
const scale = Math.max(0.84, 1 - progress * 0.16);
|
|
858
|
+
if (media) {
|
|
859
|
+
media.style.transition = 'none';
|
|
860
|
+
media.style.transform = `translate3d(0px, ${dy}px, 0) scale(${scale})`;
|
|
861
|
+
}
|
|
862
|
+
if (this.backdrop) {
|
|
863
|
+
this.backdrop.style.opacity = String(1 - Math.min(0.9, progress * 1.35));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private cancelActiveGesture(): void {
|
|
868
|
+
if (!this.pointers.size && this.mode === 'none' && !this.dragX && !this.dragY) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const mode = this.mode;
|
|
873
|
+
const pointers = [...this.pointers.values()];
|
|
874
|
+
this.pointers.clear();
|
|
875
|
+
this.mode = 'none';
|
|
876
|
+
this.dragX = 0;
|
|
877
|
+
this.dragY = 0;
|
|
878
|
+
this.releasePointerCaptures(pointers);
|
|
879
|
+
|
|
880
|
+
if (!this.openState || this.closing) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (mode === 'dismiss') {
|
|
885
|
+
this.resetDismissDrag();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (mode === 'swipe') {
|
|
889
|
+
this.updateTrack(true);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private releasePointerCaptures(pointers: Point[]): void {
|
|
894
|
+
const root = this.root;
|
|
895
|
+
if (!root) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
pointers.forEach((pointer) => {
|
|
899
|
+
if (root.hasPointerCapture(pointer.id)) {
|
|
900
|
+
root.releasePointerCapture(pointer.id);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private updateTrack(animated = false): void {
|
|
906
|
+
if (!this.track) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
this.track.style.transition = animated ? 'transform 260ms cubic-bezier(0.22, 1, 0.36, 1)' : 'none';
|
|
910
|
+
const x = -this.index * this.viewportWidth + this.dragX;
|
|
911
|
+
this.track.style.transform = `translate3d(${x}px, 0, 0)`;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private applyActiveMediaTransform(animated: boolean): void {
|
|
915
|
+
const media = this.mediaElements[this.index];
|
|
916
|
+
const state = this.activeState();
|
|
917
|
+
if (!media) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
media.style.transition = animated ? 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)' : 'none';
|
|
921
|
+
media.style.transform = `translate3d(${state.translateX}px, ${state.translateY}px, 0) scale(${state.scale})`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private clampActiveState(): void {
|
|
925
|
+
const state = this.activeState();
|
|
926
|
+
const media = this.mediaElements[this.index];
|
|
927
|
+
if (!media || state.scale <= 1.01) {
|
|
928
|
+
if (state.scale <= 1.01) {
|
|
929
|
+
state.scale = 1;
|
|
930
|
+
state.translateX = 0;
|
|
931
|
+
state.translateY = 0;
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const rect = media.getBoundingClientRect();
|
|
936
|
+
const baseWidth = rect.width / state.scale;
|
|
937
|
+
const baseHeight = rect.height / state.scale;
|
|
938
|
+
const maxX = Math.max(0, (baseWidth * state.scale - this.viewportWidth) / 2);
|
|
939
|
+
const maxY = Math.max(0, (baseHeight * state.scale - this.viewportHeight) / 2);
|
|
940
|
+
state.translateX = clamp(state.translateX, -maxX, maxX);
|
|
941
|
+
state.translateY = clamp(state.translateY, -maxY, maxY);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private activeState(): SlideState {
|
|
945
|
+
return this.slideStates[this.index] ?? this.createSlideState();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private resetSlideState(index: number): void {
|
|
949
|
+
this.slideStates[index] = this.createSlideState();
|
|
950
|
+
const media = this.mediaElements[index];
|
|
951
|
+
if (media) {
|
|
952
|
+
media.style.transition = 'none';
|
|
953
|
+
media.style.transform = '';
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private createSlideState(): SlideState {
|
|
958
|
+
return { scale: 1, translateX: 0, translateY: 0, lastTapAt: 0 };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private updateCounter(): void {
|
|
962
|
+
if (!this.counter) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
this.counter.textContent = `${this.index + 1} / ${this.items.length}`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private pauseInactiveVideos(activeIndex: number): void {
|
|
969
|
+
this.mediaElements.forEach((element, index) => {
|
|
970
|
+
if (index !== activeIndex && element instanceof HTMLVideoElement) {
|
|
971
|
+
element.pause();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private async layoutMediaElements(): Promise<void> {
|
|
977
|
+
const viewport = this.viewportRect();
|
|
978
|
+
await Promise.all(this.items.map(async (item, index) => {
|
|
979
|
+
const media = this.mediaElements[index];
|
|
980
|
+
if (!media) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const size = await resolveMediaSize(
|
|
984
|
+
item,
|
|
985
|
+
index === this.index ? this.options?.sourceImageSize : null
|
|
986
|
+
);
|
|
987
|
+
const frame = targetGeometryFor(size, viewport).visibleFrame;
|
|
988
|
+
media.style.width = `${frame.width}px`;
|
|
989
|
+
media.style.height = `${frame.height}px`;
|
|
990
|
+
}));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private lockDocumentScroll(): void {
|
|
994
|
+
this.previousBodyOverflow = document.body.style.overflow;
|
|
995
|
+
this.previousHtmlOverflow = document.documentElement.style.overflow;
|
|
996
|
+
document.body.style.overflow = 'hidden';
|
|
997
|
+
document.documentElement.style.overflow = 'hidden';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private unlockDocumentScroll(): void {
|
|
1001
|
+
document.body.style.overflow = this.previousBodyOverflow;
|
|
1002
|
+
document.documentElement.style.overflow = this.previousHtmlOverflow;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private nextFrame(): Promise<void> {
|
|
1006
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private delay(ms: number): Promise<void> {
|
|
1010
|
+
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function createNativeImagePreviewWeb(): NativeImagePreviewWebController {
|
|
1015
|
+
return new NativeImagePreviewWebViewer();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function distance(first: Point, second: Point): number {
|
|
1019
|
+
return Math.hypot(first.x - second.x, first.y - second.y);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function sourceRectFromElement(element: HTMLElement): NativeImagePreviewWebRect {
|
|
1023
|
+
return rectFromDOMRect(element.getBoundingClientRect());
|
|
1024
|
+
}
|