@marianmeres/stuic 3.33.0 → 3.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AssetsPreview/AssetsPreview.svelte +74 -561
- package/dist/components/AssetsPreview/AssetsPreview.svelte.d.ts +23 -11
- package/dist/components/AssetsPreview/AssetsPreviewInline.svelte +152 -0
- package/dist/components/AssetsPreview/AssetsPreviewInline.svelte.d.ts +45 -0
- package/dist/components/AssetsPreview/_internal/AssetsPreviewContent.svelte +663 -0
- package/dist/components/AssetsPreview/_internal/AssetsPreviewContent.svelte.d.ts +41 -0
- package/dist/components/AssetsPreview/_internal/assets-preview-types.d.ts +27 -0
- package/dist/components/AssetsPreview/_internal/assets-preview-types.js +1 -0
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.d.ts +5 -0
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.js +81 -0
- package/dist/components/AssetsPreview/index.css +28 -2
- package/dist/components/AssetsPreview/index.d.ts +2 -1
- package/dist/components/AssetsPreview/index.js +2 -1
- package/dist/components/Book/Book.svelte +23 -2
- package/dist/components/Carousel/Carousel.svelte +10 -1
- package/dist/components/Carousel/Carousel.svelte.d.ts +2 -0
- package/dist/components/Carousel/index.css +6 -0
- package/dist/components/ModalDialog/ModalDialog.svelte +2 -2
- package/dist/components/ModalDialog/index.css +8 -0
- package/package.json +1 -1
|
@@ -1,63 +1,27 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
import {
|
|
3
|
-
iconFile,
|
|
4
|
-
iconFileBinary,
|
|
5
|
-
iconFileCode,
|
|
6
|
-
iconFileImage,
|
|
7
|
-
iconFileMusic,
|
|
8
|
-
iconFilePdf,
|
|
9
|
-
iconFileRichtext,
|
|
10
|
-
iconFileSlides,
|
|
11
|
-
iconFileSpreadsheet,
|
|
12
|
-
iconFileText,
|
|
13
|
-
iconFileWord,
|
|
14
|
-
iconFileZip,
|
|
15
|
-
iconArrowLeft as iconPrevious,
|
|
16
|
-
iconArrowRight as iconNext,
|
|
17
|
-
iconDownload,
|
|
18
|
-
iconPlus as iconAdd,
|
|
19
|
-
iconTrash as iconDelete,
|
|
20
|
-
iconZoomIn,
|
|
21
|
-
iconZoomOut,
|
|
22
|
-
} from "../../icons/index.js";
|
|
23
|
-
import { getFileTypeLabel } from "../../utils/get-file-type-label.js";
|
|
24
2
|
import { ModalDialog } from "../ModalDialog/index.js";
|
|
25
3
|
import { createClog } from "@marianmeres/clog";
|
|
26
|
-
import { isImage } from "../../utils/is-image.js";
|
|
27
|
-
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
28
|
-
import { replaceMap } from "../../utils/replace-map.js";
|
|
29
4
|
import type { TranslateFn } from "../../types.js";
|
|
30
5
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { X } from "../X/index.js";
|
|
34
|
-
import { preloadImgs } from "../../utils/preload-img.js";
|
|
35
|
-
import { fade } from "svelte/transition";
|
|
6
|
+
import { preloadImgs, type PreloadImgOptions } from "../../utils/preload-img.js";
|
|
7
|
+
import { resolveUrl, resolveSrcset } from "../../utils/resolve-url.js";
|
|
36
8
|
|
|
37
|
-
export type
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
original: string | URL;
|
|
44
|
-
};
|
|
9
|
+
export type {
|
|
10
|
+
AssetPreviewUrlObj,
|
|
11
|
+
AssetPreview,
|
|
12
|
+
AssetArea,
|
|
13
|
+
} from "./_internal/assets-preview-types.js";
|
|
14
|
+
export { getAssetIcon } from "./_internal/assets-preview-utils.js";
|
|
45
15
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface AssetPreviewNormalized extends AssetPreview {
|
|
53
|
-
name: string;
|
|
54
|
-
type: string;
|
|
55
|
-
ext: string;
|
|
56
|
-
isImage: boolean;
|
|
57
|
-
}
|
|
16
|
+
// re-import for local use
|
|
17
|
+
import type { AssetArea, AssetPreview } from "./_internal/assets-preview-types.js";
|
|
18
|
+
import type { AssetPreviewNormalized } from "./_internal/assets-preview-types.js";
|
|
19
|
+
import { normalizeInput, t_default } from "./_internal/assets-preview-utils.js";
|
|
58
20
|
|
|
59
21
|
export interface Props {
|
|
60
22
|
assets: string[] | AssetPreview[];
|
|
23
|
+
/** Fallback base URL for resolving relative asset URLs */
|
|
24
|
+
baseUrl?: string;
|
|
61
25
|
classControls?: string;
|
|
62
26
|
//
|
|
63
27
|
modalClassDialog?: string;
|
|
@@ -77,358 +41,80 @@
|
|
|
77
41
|
noName?: boolean;
|
|
78
42
|
/** When true (default), panning is clamped to keep image within bounds */
|
|
79
43
|
clampPan?: boolean;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
text: iconFileText,
|
|
95
|
-
unknown: iconFile,
|
|
96
|
-
};
|
|
97
|
-
return map[getFileTypeLabel(ext ?? "unknown")] ?? iconFile;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// i18n ready
|
|
101
|
-
function t_default(
|
|
102
|
-
k: string,
|
|
103
|
-
values: false | null | undefined | Record<string, string | number> = null,
|
|
104
|
-
fallback: string | boolean = "",
|
|
105
|
-
i18nSpanWrap: boolean = true
|
|
106
|
-
) {
|
|
107
|
-
const m: Record<string, string> = {
|
|
108
|
-
unable_to_preview: "Unable to preview",
|
|
109
|
-
download: "Download",
|
|
110
|
-
close: "Close",
|
|
111
|
-
zoom_in: "Zoom in",
|
|
112
|
-
zoom_out: "Zoom out",
|
|
113
|
-
delete: "Delete",
|
|
114
|
-
};
|
|
115
|
-
let out = m[k] ?? fallback ?? k;
|
|
116
|
-
|
|
117
|
-
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// naive best-effort
|
|
121
|
-
function ext(name: string): string {
|
|
122
|
-
const _ext = name.split(".").at(-1) ?? "";
|
|
123
|
-
return _ext ? `.${_ext}` : "";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeInput(input: string | AssetPreview): AssetPreviewNormalized | null {
|
|
127
|
-
const asset: AssetPreviewNormalized = {
|
|
128
|
-
name: "",
|
|
129
|
-
type: "",
|
|
130
|
-
url: { full: "", thumb: "", original: "" },
|
|
131
|
-
isImage: false,
|
|
132
|
-
ext: "",
|
|
133
|
-
};
|
|
134
|
-
if (typeof input === "string") {
|
|
135
|
-
asset.url.full = input;
|
|
136
|
-
} else {
|
|
137
|
-
// Handle AssetPreview object
|
|
138
|
-
asset.url = { ...input.url };
|
|
139
|
-
asset.name = input.name ?? "";
|
|
140
|
-
asset.type = input.type ?? "";
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!asset.url.full) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
asset.url.full = new URL(
|
|
148
|
-
asset.url.full,
|
|
149
|
-
globalThis.location?.href ?? "http://placeholder"
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// Use "full" also as "thumb" and "original", if not provided
|
|
153
|
-
asset.url.thumb ||= asset.url.full;
|
|
154
|
-
asset.url.original ||= asset.url.full;
|
|
155
|
-
|
|
156
|
-
// best-effort..
|
|
157
|
-
if (!asset.name) {
|
|
158
|
-
asset.name = asset.url.full.pathname.split("/").at(-1) ?? "";
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// best-effort..
|
|
162
|
-
if (!asset.type) {
|
|
163
|
-
asset.type = getFileTypeLabel(ext(asset.name) ?? "unknown");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
asset.ext = ext(asset.name);
|
|
167
|
-
asset.isImage = isImage(asset.ext);
|
|
168
|
-
|
|
169
|
-
return asset;
|
|
44
|
+
/** Do not offer download if truthy (default false) */
|
|
45
|
+
noDownload?: boolean;
|
|
46
|
+
/** Hide prev/next arrow buttons */
|
|
47
|
+
noPrevNext?: boolean;
|
|
48
|
+
/** Disable all zooming (buttons + gestures) */
|
|
49
|
+
noZoom?: boolean;
|
|
50
|
+
/** Hide zoom buttons only (gestures still work) */
|
|
51
|
+
noZoomButtons?: boolean;
|
|
52
|
+
/** Never show dots (even if less than 10) */
|
|
53
|
+
noDots?: boolean;
|
|
54
|
+
/** Never show "x / y" meta */
|
|
55
|
+
noCurrentOfTotal?: boolean;
|
|
56
|
+
/** Callback when a clickable area on an image is clicked */
|
|
57
|
+
onAreaClick?: (data: { area: AssetArea; asset: AssetPreviewNormalized }) => void;
|
|
170
58
|
}
|
|
171
59
|
</script>
|
|
172
60
|
|
|
173
61
|
<script lang="ts">
|
|
174
|
-
import
|
|
62
|
+
import AssetsPreviewContent from "./_internal/AssetsPreviewContent.svelte";
|
|
175
63
|
const clog = createClog("AssetsPreview", { color: "auto" });
|
|
176
64
|
|
|
177
65
|
let {
|
|
178
66
|
modalClassDialog = "",
|
|
179
67
|
modalClass = "",
|
|
180
68
|
assets: _assets,
|
|
69
|
+
baseUrl,
|
|
181
70
|
t = t_default,
|
|
182
71
|
classControls = "",
|
|
183
72
|
onDelete,
|
|
73
|
+
onAreaClick,
|
|
184
74
|
noName,
|
|
185
75
|
clampPan = false,
|
|
76
|
+
noDownload = false,
|
|
77
|
+
noPrevNext = false,
|
|
78
|
+
noZoom = false,
|
|
79
|
+
noZoomButtons = false,
|
|
80
|
+
noDots = false,
|
|
81
|
+
noCurrentOfTotal = false,
|
|
186
82
|
}: Props = $props();
|
|
187
83
|
|
|
188
84
|
let assets: AssetPreviewNormalized[] = $derived(
|
|
189
85
|
(_assets ?? []).map(normalizeInput).filter(Boolean) as AssetPreviewNormalized[]
|
|
190
86
|
);
|
|
191
87
|
let previewIdx = $state<number>(0);
|
|
88
|
+
let _openIdx: number | undefined = $state();
|
|
192
89
|
let modal: ModalDialog | undefined = $state();
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
// Zoom state
|
|
196
|
-
const ZOOM_LEVELS = [1, 1.5, 2, 3, 4] as const;
|
|
197
|
-
const MIN_ZOOM = ZOOM_LEVELS[0];
|
|
198
|
-
const MAX_ZOOM = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
|
|
199
|
-
let zoomLevelIdx = $state(0);
|
|
200
|
-
|
|
201
|
-
// Pinch zoom state
|
|
202
|
-
let isPinching = $state(false);
|
|
203
|
-
let initialPinchDistance = 0;
|
|
204
|
-
let initialPinchZoom = 1;
|
|
205
|
-
let continuousZoom = $state(1);
|
|
206
|
-
|
|
207
|
-
// Use continuous zoom during pinch, discrete levels otherwise
|
|
208
|
-
let zoomLevel = $derived(isPinching ? continuousZoom : ZOOM_LEVELS[zoomLevelIdx]);
|
|
209
|
-
|
|
210
|
-
// Pan state
|
|
211
|
-
let isPanning = $state(false);
|
|
212
|
-
let panX = $state(0);
|
|
213
|
-
let panY = $state(0);
|
|
214
|
-
let startPanX = 0;
|
|
215
|
-
let startPanY = 0;
|
|
216
|
-
let startMouseX = 0;
|
|
217
|
-
let startMouseY = 0;
|
|
218
|
-
|
|
219
|
-
// Image and container dimensions for pan clamping
|
|
220
|
-
let imgEl: HTMLImageElement | null = null;
|
|
221
|
-
let containerEl: HTMLDivElement | null = $state(null);
|
|
222
|
-
|
|
223
|
-
const BUTTON_CLS = "stuic-assets-preview-control pointer-events-auto p-0!";
|
|
224
|
-
|
|
225
|
-
const BUTTON_PROPS = {
|
|
226
|
-
aspect1: true,
|
|
227
|
-
variant: "soft",
|
|
228
|
-
roundedFull: true,
|
|
229
|
-
};
|
|
90
|
+
let content: AssetsPreviewContent | undefined = $state();
|
|
230
91
|
|
|
231
92
|
$effect(() => {
|
|
232
93
|
const visible = modal?.visibility().visible;
|
|
233
94
|
if (visible) {
|
|
234
|
-
//
|
|
235
|
-
previewIdx = 0;
|
|
95
|
+
// Use the index from open(index) if provided, otherwise reset to 0
|
|
96
|
+
previewIdx = _openIdx ?? 0;
|
|
97
|
+
_openIdx = undefined;
|
|
236
98
|
|
|
237
99
|
// perhaps we should have some upper limit here...
|
|
238
|
-
const toPreload = (assets ?? [])
|
|
239
|
-
.
|
|
240
|
-
.
|
|
100
|
+
const toPreload: PreloadImgOptions[] = (assets ?? [])
|
|
101
|
+
.filter((asset) => asset.isImage)
|
|
102
|
+
.map((asset) => ({
|
|
103
|
+
src: resolveUrl(String(asset.url.full), baseUrl),
|
|
104
|
+
srcset: resolveSrcset(asset.srcset ?? "", baseUrl) || undefined,
|
|
105
|
+
sizes: asset.sizes,
|
|
106
|
+
}));
|
|
241
107
|
|
|
242
108
|
clog.debug("going to (maybe) preload", toPreload);
|
|
243
|
-
|
|
109
|
+
if (toPreload.length) preloadImgs(toPreload);
|
|
244
110
|
} else {
|
|
245
111
|
// Reset zoom when modal closes
|
|
246
|
-
resetZoom();
|
|
112
|
+
content?.resetZoom();
|
|
247
113
|
}
|
|
248
114
|
});
|
|
249
115
|
|
|
250
|
-
// Wheel zoom handler
|
|
251
|
-
function handleWheel(e: WheelEvent) {
|
|
252
|
-
e.preventDefault();
|
|
253
|
-
if (e.deltaY > 0) {
|
|
254
|
-
zoomOut();
|
|
255
|
-
} else {
|
|
256
|
-
zoomIn();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Svelte action for pan event listeners - guaranteed to run when element is created
|
|
261
|
-
function pannable(node: HTMLImageElement) {
|
|
262
|
-
imgEl = node;
|
|
263
|
-
node.addEventListener("mousedown", panStart);
|
|
264
|
-
node.addEventListener("touchstart", panStart, { passive: false });
|
|
265
|
-
node.addEventListener("wheel", handleWheel, { passive: false });
|
|
266
|
-
|
|
267
|
-
document.addEventListener("mousemove", panMove);
|
|
268
|
-
document.addEventListener("mouseup", panEnd);
|
|
269
|
-
document.addEventListener("touchmove", panMove, { passive: false });
|
|
270
|
-
document.addEventListener("touchend", panEnd);
|
|
271
|
-
document.addEventListener("touchcancel", panEnd);
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
destroy() {
|
|
275
|
-
imgEl = null;
|
|
276
|
-
node.removeEventListener("mousedown", panStart);
|
|
277
|
-
node.removeEventListener("touchstart", panStart);
|
|
278
|
-
node.removeEventListener("wheel", handleWheel);
|
|
279
|
-
document.removeEventListener("mousemove", panMove);
|
|
280
|
-
document.removeEventListener("mouseup", panEnd);
|
|
281
|
-
document.removeEventListener("touchmove", panMove);
|
|
282
|
-
document.removeEventListener("touchend", panEnd);
|
|
283
|
-
document.removeEventListener("touchcancel", panEnd);
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Clamp pan values to keep image within bounds
|
|
289
|
-
function getClampedPan(newPanX: number, newPanY: number): { x: number; y: number } {
|
|
290
|
-
if (!imgEl || !containerEl) return { x: newPanX, y: newPanY };
|
|
291
|
-
|
|
292
|
-
const imgRect = imgEl.getBoundingClientRect();
|
|
293
|
-
const containerRect = containerEl.getBoundingClientRect();
|
|
294
|
-
|
|
295
|
-
// Calculate the scaled image dimensions
|
|
296
|
-
const scaledWidth = imgRect.width; // already includes transform scale
|
|
297
|
-
const scaledHeight = imgRect.height;
|
|
298
|
-
|
|
299
|
-
// Calculate max pan distance (how much the scaled image exceeds the container)
|
|
300
|
-
// Divide by zoomLevel because translate values are applied before scale in our transform
|
|
301
|
-
const maxPanX = Math.max(0, (scaledWidth - containerRect.width) / 2 / zoomLevel);
|
|
302
|
-
const maxPanY = Math.max(0, (scaledHeight - containerRect.height) / 2 / zoomLevel);
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
x: Math.max(-maxPanX, Math.min(maxPanX, newPanX)),
|
|
306
|
-
y: Math.max(-maxPanY, Math.min(maxPanY, newPanY)),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Zoom functions
|
|
311
|
-
function zoomIn() {
|
|
312
|
-
if (zoomLevelIdx < ZOOM_LEVELS.length - 1) {
|
|
313
|
-
zoomLevelIdx++;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function zoomOut() {
|
|
318
|
-
if (zoomLevelIdx > 0) {
|
|
319
|
-
zoomLevelIdx--;
|
|
320
|
-
// Reset pan when zooming out to 1x
|
|
321
|
-
if (zoomLevelIdx === 0) {
|
|
322
|
-
panX = 0;
|
|
323
|
-
panY = 0;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function resetZoom() {
|
|
329
|
-
zoomLevelIdx = 0;
|
|
330
|
-
continuousZoom = 1;
|
|
331
|
-
panX = 0;
|
|
332
|
-
panY = 0;
|
|
333
|
-
isPinching = false;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Pinch zoom helpers
|
|
337
|
-
function getDistance(touch1: Touch, touch2: Touch): number {
|
|
338
|
-
const dx = touch1.clientX - touch2.clientX;
|
|
339
|
-
const dy = touch1.clientY - touch2.clientY;
|
|
340
|
-
return Math.hypot(dx, dy);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function findNearestZoomLevelIdx(zoom: number): number {
|
|
344
|
-
let nearestIdx = 0;
|
|
345
|
-
let minDiff = Math.abs(ZOOM_LEVELS[0] - zoom);
|
|
346
|
-
for (let i = 1; i < ZOOM_LEVELS.length; i++) {
|
|
347
|
-
const diff = Math.abs(ZOOM_LEVELS[i] - zoom);
|
|
348
|
-
if (diff < minDiff) {
|
|
349
|
-
minDiff = diff;
|
|
350
|
-
nearestIdx = i;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return nearestIdx;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Pan/drag handlers
|
|
357
|
-
function panStart(e: MouseEvent | TouchEvent) {
|
|
358
|
-
// Detect two-finger pinch gesture
|
|
359
|
-
if ("touches" in e && e.touches.length === 2) {
|
|
360
|
-
e.preventDefault();
|
|
361
|
-
isPinching = true;
|
|
362
|
-
isPanning = false;
|
|
363
|
-
initialPinchDistance = getDistance(e.touches[0], e.touches[1]);
|
|
364
|
-
initialPinchZoom = continuousZoom;
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Single-finger pan (only when zoomed in)
|
|
369
|
-
if (zoomLevel <= 1) return;
|
|
370
|
-
e.preventDefault();
|
|
371
|
-
isPanning = true;
|
|
372
|
-
|
|
373
|
-
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
374
|
-
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
375
|
-
|
|
376
|
-
startMouseX = clientX;
|
|
377
|
-
startMouseY = clientY;
|
|
378
|
-
startPanX = panX;
|
|
379
|
-
startPanY = panY;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function panMove(e: MouseEvent | TouchEvent) {
|
|
383
|
-
// Handle pinch zoom
|
|
384
|
-
if ("touches" in e && e.touches.length === 2 && isPinching) {
|
|
385
|
-
e.preventDefault();
|
|
386
|
-
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
|
387
|
-
const scale = currentDistance / initialPinchDistance;
|
|
388
|
-
continuousZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, initialPinchZoom * scale));
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Handle single-finger pan
|
|
393
|
-
if (!isPanning) return;
|
|
394
|
-
e.preventDefault();
|
|
395
|
-
|
|
396
|
-
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
397
|
-
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
398
|
-
|
|
399
|
-
const newPanX = startPanX + (clientX - startMouseX);
|
|
400
|
-
const newPanY = startPanY + (clientY - startMouseY);
|
|
401
|
-
|
|
402
|
-
if (clampPan) {
|
|
403
|
-
const clamped = getClampedPan(newPanX, newPanY);
|
|
404
|
-
panX = clamped.x;
|
|
405
|
-
panY = clamped.y;
|
|
406
|
-
} else {
|
|
407
|
-
panX = newPanX;
|
|
408
|
-
panY = newPanY;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function panEnd() {
|
|
413
|
-
// Handle pinch end - snap to nearest discrete level
|
|
414
|
-
if (isPinching) {
|
|
415
|
-
isPinching = false;
|
|
416
|
-
zoomLevelIdx = findNearestZoomLevelIdx(continuousZoom);
|
|
417
|
-
continuousZoom = ZOOM_LEVELS[zoomLevelIdx];
|
|
418
|
-
// Reset pan when zoomed out to 1x
|
|
419
|
-
if (zoomLevelIdx === 0) {
|
|
420
|
-
panX = 0;
|
|
421
|
-
panY = 0;
|
|
422
|
-
}
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
isPanning = false;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
116
|
export function open(index?: number) {
|
|
429
|
-
|
|
430
|
-
previewIdx = index;
|
|
431
|
-
}
|
|
117
|
+
_openIdx = typeof index === "number" ? index : undefined;
|
|
432
118
|
modal?.open();
|
|
433
119
|
}
|
|
434
120
|
|
|
@@ -449,33 +135,16 @@
|
|
|
449
135
|
export function setOpener(opener?: null | HTMLElement) {
|
|
450
136
|
modal?.setOpener(opener);
|
|
451
137
|
}
|
|
452
|
-
|
|
453
|
-
function preview_previous() {
|
|
454
|
-
previewIdx = (previewIdx - 1 + assets.length) % assets.length;
|
|
455
|
-
resetZoom();
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function preview_next() {
|
|
459
|
-
previewIdx = (previewIdx + 1) % assets.length;
|
|
460
|
-
resetZoom();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function preview(idx: number) {
|
|
464
|
-
previewIdx = idx % assets.length;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const ICON_SIZE = 24;
|
|
468
|
-
// $inspect(assets).with(clog);
|
|
469
138
|
</script>
|
|
470
139
|
|
|
471
140
|
<!-- this must be on window as we're catching any typing anywhere -->
|
|
472
141
|
<svelte:window
|
|
473
142
|
onkeydown={(e) => {
|
|
474
|
-
if (modal?.visibility().visible) {
|
|
143
|
+
if (modal?.visibility().visible && !noPrevNext) {
|
|
475
144
|
if (["ArrowRight"].includes(e.key)) {
|
|
476
|
-
|
|
145
|
+
content?.next();
|
|
477
146
|
} else if (["ArrowLeft"].includes(e.key)) {
|
|
478
|
-
|
|
147
|
+
content?.previous();
|
|
479
148
|
}
|
|
480
149
|
}
|
|
481
150
|
}}
|
|
@@ -492,180 +161,24 @@
|
|
|
492
161
|
modalClass
|
|
493
162
|
)}
|
|
494
163
|
>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
/>
|
|
515
|
-
</div>
|
|
516
|
-
{:else}
|
|
517
|
-
<div>
|
|
518
|
-
<div>
|
|
519
|
-
{@html getAssetIcon(previewAsset.ext)({
|
|
520
|
-
size: 32,
|
|
521
|
-
class: "mx-auto",
|
|
522
|
-
})}
|
|
523
|
-
</div>
|
|
524
|
-
<div class="text-(--stuic-color-muted-foreground) mt-4">
|
|
525
|
-
{t("unable_to_preview")}
|
|
526
|
-
</div>
|
|
527
|
-
</div>
|
|
528
|
-
{/if}
|
|
529
|
-
|
|
530
|
-
{#if assets?.length > 1}
|
|
531
|
-
<div
|
|
532
|
-
class="absolute inset-0 flex items-center justify-between pointer-events-none"
|
|
533
|
-
>
|
|
534
|
-
<!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
|
|
535
|
-
<Button
|
|
536
|
-
class={twMerge(BUTTON_CLS, "ml-4", classControls)}
|
|
537
|
-
onclick={preview_previous}
|
|
538
|
-
type="button"
|
|
539
|
-
{...BUTTON_PROPS}
|
|
540
|
-
>
|
|
541
|
-
<!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
|
|
542
|
-
{@html iconPrevious({ size: ICON_SIZE })}
|
|
543
|
-
<!-- </span> -->
|
|
544
|
-
</Button>
|
|
545
|
-
|
|
546
|
-
<!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
|
|
547
|
-
<Button
|
|
548
|
-
class={twMerge(BUTTON_CLS, "mr-4", classControls)}
|
|
549
|
-
onclick={preview_next}
|
|
550
|
-
type="button"
|
|
551
|
-
{...BUTTON_PROPS}
|
|
552
|
-
>
|
|
553
|
-
<!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
|
|
554
|
-
{@html iconNext({ size: ICON_SIZE })}
|
|
555
|
-
<!-- </span> -->
|
|
556
|
-
</Button>
|
|
557
|
-
</div>
|
|
558
|
-
{/if}
|
|
559
|
-
|
|
560
|
-
<div class="absolute top-4 left-4 right-4 flex items-center justify-between gap-3">
|
|
561
|
-
{#if !noName && previewAsset?.name}
|
|
562
|
-
<span class="stuic-assets-preview-label truncate px-1">
|
|
563
|
-
{previewAsset?.name}
|
|
564
|
-
</span>
|
|
565
|
-
{:else}
|
|
566
|
-
<span></span>
|
|
567
|
-
{/if}
|
|
568
|
-
<div class="flex items-center space-x-3 shrink-0">
|
|
569
|
-
{#if previewAsset.isImage}
|
|
570
|
-
<Button
|
|
571
|
-
class={twMerge(BUTTON_CLS, classControls)}
|
|
572
|
-
type="button"
|
|
573
|
-
onclick={zoomOut}
|
|
574
|
-
disabled={zoomLevelIdx === 0}
|
|
575
|
-
aria-label={t("zoom_out")}
|
|
576
|
-
tooltip={t("zoom_out")}
|
|
577
|
-
{...BUTTON_PROPS}
|
|
578
|
-
>
|
|
579
|
-
{@html iconZoomOut({ size: ICON_SIZE })}
|
|
580
|
-
</Button>
|
|
581
|
-
|
|
582
|
-
<Button
|
|
583
|
-
class={twMerge(BUTTON_CLS, classControls)}
|
|
584
|
-
type="button"
|
|
585
|
-
onclick={zoomIn}
|
|
586
|
-
disabled={zoomLevelIdx === ZOOM_LEVELS.length - 1}
|
|
587
|
-
aria-label={t("zoom_in")}
|
|
588
|
-
tooltip={t("zoom_in")}
|
|
589
|
-
{...BUTTON_PROPS}
|
|
590
|
-
>
|
|
591
|
-
{@html iconZoomIn({ size: ICON_SIZE })}
|
|
592
|
-
</Button>
|
|
593
|
-
{/if}
|
|
594
|
-
|
|
595
|
-
{#if typeof onDelete === "function"}
|
|
596
|
-
<Button
|
|
597
|
-
class={twMerge(BUTTON_CLS, classControls)}
|
|
598
|
-
type="button"
|
|
599
|
-
onclick={() => onDelete(previewAsset, previewIdx, { close })}
|
|
600
|
-
aria-label={t("delete")}
|
|
601
|
-
tooltip={t("delete")}
|
|
602
|
-
{...BUTTON_PROPS}
|
|
603
|
-
>
|
|
604
|
-
{@html iconDelete({ size: ICON_SIZE })}
|
|
605
|
-
</Button>
|
|
606
|
-
{/if}
|
|
607
|
-
|
|
608
|
-
<Button
|
|
609
|
-
class={twMerge(BUTTON_CLS, classControls)}
|
|
610
|
-
type="button"
|
|
611
|
-
onclick={(e) => {
|
|
612
|
-
e.preventDefault();
|
|
613
|
-
forceDownload(String(previewAsset.url.original), previewAsset?.name || "");
|
|
614
|
-
}}
|
|
615
|
-
aria-label={t("download")}
|
|
616
|
-
tooltip={t("download")}
|
|
617
|
-
{...BUTTON_PROPS}
|
|
618
|
-
>
|
|
619
|
-
{@html iconDownload({ size: ICON_SIZE })}
|
|
620
|
-
</Button>
|
|
621
|
-
|
|
622
|
-
<Button
|
|
623
|
-
class={twMerge(BUTTON_CLS, classControls)}
|
|
624
|
-
onclick={modal?.close}
|
|
625
|
-
aria-label={t("close")}
|
|
626
|
-
type="button"
|
|
627
|
-
tooltip={t("close")}
|
|
628
|
-
{...BUTTON_PROPS}
|
|
629
|
-
x
|
|
630
|
-
/>
|
|
631
|
-
</div>
|
|
632
|
-
</div>
|
|
633
|
-
|
|
634
|
-
{#if assets.length > 1}
|
|
635
|
-
{#if !noName && dotTooltip}
|
|
636
|
-
<div
|
|
637
|
-
class="absolute bottom-10 left-0 right-0 text-center"
|
|
638
|
-
transition:fade={{ duration: 100 }}
|
|
639
|
-
>
|
|
640
|
-
<span class="stuic-assets-preview-label p-1">
|
|
641
|
-
{dotTooltip}
|
|
642
|
-
</span>
|
|
643
|
-
</div>
|
|
644
|
-
{/if}
|
|
645
|
-
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3">
|
|
646
|
-
{#each assets as _, i}
|
|
647
|
-
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
648
|
-
<button
|
|
649
|
-
type="button"
|
|
650
|
-
class={twMerge(
|
|
651
|
-
"stuic-assets-preview-dot",
|
|
652
|
-
i === previewIdx ? "active" : ""
|
|
653
|
-
)}
|
|
654
|
-
onclick={() => {
|
|
655
|
-
previewIdx = i;
|
|
656
|
-
resetZoom();
|
|
657
|
-
}}
|
|
658
|
-
aria-label={assets[i]?.name}
|
|
659
|
-
onmouseover={() => {
|
|
660
|
-
dotTooltip = assets[i]?.name;
|
|
661
|
-
}}
|
|
662
|
-
onmouseout={() => {
|
|
663
|
-
dotTooltip = undefined;
|
|
664
|
-
}}
|
|
665
|
-
></button>
|
|
666
|
-
{/each}
|
|
667
|
-
</div>
|
|
668
|
-
{/if}
|
|
669
|
-
{/if}
|
|
164
|
+
<AssetsPreviewContent
|
|
165
|
+
bind:this={content}
|
|
166
|
+
bind:previewIdx
|
|
167
|
+
{assets}
|
|
168
|
+
{baseUrl}
|
|
169
|
+
{classControls}
|
|
170
|
+
{t}
|
|
171
|
+
{onDelete}
|
|
172
|
+
{noName}
|
|
173
|
+
{clampPan}
|
|
174
|
+
{noDownload}
|
|
175
|
+
{noPrevNext}
|
|
176
|
+
{noZoom}
|
|
177
|
+
{noZoomButtons}
|
|
178
|
+
{noDots}
|
|
179
|
+
{noCurrentOfTotal}
|
|
180
|
+
{onAreaClick}
|
|
181
|
+
onClose={() => modal?.close()}
|
|
182
|
+
/>
|
|
670
183
|
</ModalDialog>
|
|
671
184
|
{/if}
|