@fibery/ui-kit 1.39.1 → 1.40.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -2
  3. package/src/button/select-button.tsx +11 -3
  4. package/src/collapsible/index.tsx +2 -2
  5. package/src/command-menu/index.tsx +4 -2
  6. package/src/copy-to-clipboard.ts +10 -0
  7. package/src/file-item-2.tsx +218 -128
  8. package/src/file-item.tsx +2 -0
  9. package/src/guide-link.tsx +1 -1
  10. package/src/html-styles.ts +2 -12
  11. package/src/icons/ast/FileImage.ts +1 -1
  12. package/src/icons/ast/Mermaid.ts +8 -0
  13. package/src/icons/ast/TextNoWrap.ts +8 -0
  14. package/src/icons/ast/TextWrap.ts +8 -0
  15. package/src/icons/ast/TypeId.ts +8 -0
  16. package/src/icons/ast/index.tsx +4 -0
  17. package/src/icons/react/Mermaid.tsx +13 -0
  18. package/src/icons/react/TextNoWrap.tsx +13 -0
  19. package/src/icons/react/TextWrap.tsx +13 -0
  20. package/src/icons/react/TypeId.tsx +13 -0
  21. package/src/icons/react/index.tsx +4 -0
  22. package/src/icons/svg/file-image.svg +2 -2
  23. package/src/icons/svg/mermaid.svg +3 -0
  24. package/src/icons/svg/text-no-wrap.svg +3 -0
  25. package/src/icons/svg/text-wrap.svg +3 -0
  26. package/src/icons/svg/type/id.svg +3 -0
  27. package/src/images-gallery/images-gallery-fall-through-provider.tsx +2 -1
  28. package/src/images-gallery/images-gallery.tsx +524 -185
  29. package/src/{images-gallery-2 → images-gallery}/slide-buttons.tsx +25 -13
  30. package/src/images-gallery/zoom.tsx +5 -94
  31. package/src/popover/index.tsx +5 -3
  32. package/src/select/components/menu-list-virtualized.tsx +123 -84
  33. package/src/select/components/menu.tsx +5 -3
  34. package/src/table.tsx +13 -10
  35. package/src/toggle-button/toggle-button.tsx +5 -0
  36. package/src/images-gallery-2/images-gallery-2.tsx +0 -522
  37. package/src/images-gallery-2/zoom.tsx +0 -35
@@ -1,24 +1,26 @@
1
- import {ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, useState} from "react";
2
- import _ from "lodash";
3
- import {createContext} from "@fibery/react/src/create-context";
4
- import {useIsPhone} from "../use-is-phone";
5
- import {safeAreaInsetBottomVar} from "../mobile-styles";
6
- import {buttonGroup, ButtonGroup} from "../button/button-group";
7
- import {IconButton} from "../button/icon-button";
8
- import ArrowRight from "../icons/react/ArrowRight";
9
- import ArrowLeft from "../icons/react/ArrowLeft";
1
+ /* eslint-disable max-lines */
2
+ import PhotoSwipeLightbox, {type PhotoSwipeEventsMap, type PhotoSwipeOptions} from "photoswipe/lightbox";
3
+ import "photoswipe/style.css";
4
+ import {PropsWithChildren, useCallback, useEffect, useMemo, useReducer, useRef, useState} from "react";
10
5
  import {css} from "@linaria/core";
11
- import cx from "classnames";
6
+ import {createRoot} from "react-dom/client";
7
+ import {space, themeVars} from "../design-system";
8
+ import {safeAreaInsetLeftVar, safeAreaInsetRightVar, safeAreaInsetTopVar} from "../mobile-styles";
9
+ import {useToast} from "../toast/toast-provider";
10
+ import {ToastQueue} from "../toast/toast-queue";
11
+ import {useThemeMode} from "../theme-provider";
12
+ import {ThemeMode} from "../theme-settings";
13
+ import {SlideButtons} from "./slide-buttons";
14
+ import {useIsPhone} from "../use-is-phone";
12
15
 
13
- type Registry = Map<string, {zoomed: boolean; setZoomed: (zoomed: boolean) => void; src: string}>;
16
+ import _ from "lodash";
17
+ import {createContext} from "@fibery/react/src/create-context";
14
18
 
15
19
  const [ImagesGalleryRegistryProvider, useImagesGalleryRegistry] = createContext(
16
20
  "ImagesGalleryRegistryContext",
17
21
  new Map()
18
22
  );
19
23
 
20
- export {ImagesGalleryRegistryProvider, useImagesGalleryRegistry};
21
-
22
24
  type Methods = {
23
25
  setVisible: (visible: boolean) => void;
24
26
  setActivated: (activated: boolean) => void;
@@ -26,6 +28,9 @@ type Methods = {
26
28
  nextImage: () => void;
27
29
  prevImage: () => void;
28
30
  };
31
+
32
+ export {ImagesGalleryRegistryProvider, useImagesGalleryRegistry};
33
+
29
34
  const [ImagesGalleryMethodsProvider, useImagesGalleryMethods] = createContext<Methods>("ImagesGalleryMethodsProvider", {
30
35
  setVisible: _.noop,
31
36
  setActivated: _.noop,
@@ -46,10 +51,154 @@ export {
46
51
  ImagesGalleryStateContext,
47
52
  };
48
53
 
54
+ type Registry = Map<
55
+ string,
56
+ {
57
+ src: string;
58
+ label?: string;
59
+ type: "image" | "video" | "iframe";
60
+ contentType?: string;
61
+ actions?: React.ReactNode;
62
+ zoomed: boolean;
63
+ setZoomed: (zoomed: boolean) => void;
64
+ }
65
+ >;
66
+
67
+ export const galleryMainClass = css`
68
+ z-index: 1000;
69
+ & .pswp__button--arrow--prev {
70
+ left: calc(${space.s20}px + var(${safeAreaInsetLeftVar}));
71
+ height: ${space.s28}px;
72
+ width: ${space.s28}px;
73
+ margin-top: -${space.s14}px;
74
+ }
75
+ & .pswp__button--arrow--next {
76
+ right: calc(${space.s20}px + var(${safeAreaInsetRightVar}));
77
+ height: ${space.s28}px;
78
+ width: ${space.s28}px;
79
+ margin-top: -${space.s14}px;
80
+ }
81
+
82
+ & .pswp__button--zoom {
83
+ top: ${space.s20}px;
84
+ height: ${space.s28}px;
85
+ width: ${space.s28}px;
86
+ }
87
+
88
+ & .pswp__counter {
89
+ margin-top: ${space.s20}px;
90
+ margin-inline-start: ${space.s24}px;
91
+ }
92
+
93
+ & .pswp__video-container {
94
+ padding: ${space.s60}px;
95
+ }
96
+
97
+ & .pswp__video-container video {
98
+ width: 100%;
99
+ height: 100%;
100
+ object-fit: contain;
101
+ display: block;
102
+ background-color: black;
103
+ }
104
+
105
+ & .pswp__iframe-container {
106
+ padding: ${space.s60}px;
107
+ display: flex;
108
+ justify-content: center;
109
+ align-items: center;
110
+ }
111
+
112
+ & .pswp__iframe-container iframe {
113
+ width: 100%;
114
+ height: 100%;
115
+ object-fit: contain;
116
+ display: block;
117
+ color: ${themeVars.textColor};
118
+ background-color: ${themeVars.colorBgPopup};
119
+ border: none;
120
+ }
121
+
122
+ & .pswp__top-bar {
123
+ top: var(${safeAreaInsetTopVar});
124
+ left: var(${safeAreaInsetLeftVar});
125
+ }
126
+ `;
127
+
128
+ export function ImagesGalleryProvider({children}: PropsWithChildren) {
129
+ const [imagesRegistry] = useState<Registry>(() => new Map());
130
+ const [galleryState, dispatchState] = useReducer(imagesGalleryReducer, [], initialState);
131
+
132
+ const lightbox = useRef<PhotoSwipeLightbox | null>(null);
133
+ const markAllNotZoomed = useCallback(() => {
134
+ for (const node of imagesRegistry.values()) {
135
+ if (node.zoomed) {
136
+ node.setZoomed(false);
137
+ }
138
+ }
139
+ }, [imagesRegistry]);
140
+
141
+ useEffect(() => {
142
+ return () => {
143
+ lightbox.current?.destroy();
144
+ lightbox.current = null;
145
+ markAllNotZoomed();
146
+ };
147
+ }, [markAllNotZoomed]);
148
+
149
+ const methods: Methods = useMemo(() => {
150
+ return {
151
+ setVisible: () => {},
152
+ setActivated: (activated: boolean) => {
153
+ dispatchState({type: "set-activated", payload: activated});
154
+ },
155
+ update: () => {
156
+ dispatchState({type: "inc"});
157
+ },
158
+ nextImage: () => {
159
+ lightbox.current?.pswp?.next();
160
+ },
161
+ prevImage: () => {
162
+ lightbox.current?.pswp?.prev();
163
+ },
164
+ };
165
+ }, []);
166
+
167
+ const toast = useToast();
168
+ const themeMode = useThemeMode();
169
+ const isPhone = useIsPhone();
170
+
171
+ useEffect(() => {
172
+ if (galleryState.activated) {
173
+ const value = initGallery(imagesRegistry, {toastQueue: toast, isPhone, themeMode});
174
+ value.on("close", () => {
175
+ dispatchState({type: "set-activated", payload: false});
176
+ markAllNotZoomed();
177
+ value.destroy();
178
+ });
179
+
180
+ lightbox.current = value;
181
+ }
182
+
183
+ return () => {
184
+ lightbox.current?.destroy();
185
+ lightbox.current = null;
186
+ };
187
+ }, [galleryState.activated, imagesRegistry, markAllNotZoomed, toast, themeMode, isPhone]);
188
+
189
+ return (
190
+ <ImagesGalleryRegistryProvider value={imagesRegistry}>
191
+ <ImagesGalleryMethodsProvider value={methods}>
192
+ <ImagesGalleryStateProvider value={galleryState}>{children}</ImagesGalleryStateProvider>
193
+ </ImagesGalleryMethodsProvider>
194
+ </ImagesGalleryRegistryProvider>
195
+ );
196
+ }
197
+
49
198
  type Actions = {type: "set-visible"; payload: boolean} | {type: "set-activated"; payload: boolean} | {type: "inc"};
50
199
 
51
200
  function initialState(): State {
52
- return {version: "1", visible: false, activated: false, _inc: 0};
201
+ return {version: "2", visible: false, activated: false, _inc: 0};
53
202
  }
54
203
 
55
204
  function imagesGalleryReducer(state: State, action: Actions) {
@@ -65,193 +214,383 @@ function imagesGalleryReducer(state: State, action: Actions) {
65
214
  return state;
66
215
  }
67
216
 
68
- const controlsGroupStyle = css`
69
- &.${buttonGroup} {
70
- display: none;
71
- }
72
- `;
217
+ type GalleryEntry = {
218
+ type: string;
219
+ src: string;
220
+ contentType: string | null;
221
+ zoomed: boolean;
222
+ setZoomed: (value: boolean) => void;
223
+ state: "none" | "loading" | "error" | "loaded";
224
+ width?: number;
225
+ height?: number;
226
+ };
73
227
 
74
- export function Controls() {
75
- const imagesRegistry = useImagesGalleryRegistry();
76
- const {visible} = useImagesGalleryState();
77
- const isPhone = useIsPhone();
78
- const {prevImage, nextImage, setVisible} = useImagesGalleryMethods();
79
- // Determine if there is a zoomed image and its position in the registry
80
- let activeNext = true;
81
- let activePrev = true;
82
- let i = 0;
83
- for (const image of imagesRegistry.values()) {
84
- if (image.zoomed) {
85
- if (i === 0) {
86
- activePrev = false;
87
- }
88
- if (i === imagesRegistry.size - 1) {
89
- activeNext = false;
90
- }
91
- break;
228
+ function attachCustomControls(
229
+ lightbox: PhotoSwipeLightbox,
230
+ items: {
231
+ src: string;
232
+ label?: string;
233
+ type: "image" | "video" | "iframe";
234
+ contentType?: string;
235
+ actions?: React.ReactNode;
236
+ zoomed: boolean;
237
+ setZoomed: (zoomed: boolean) => void;
238
+ }[],
239
+ toastQueue: ToastQueue,
240
+ themeMode: ThemeMode
241
+ ) {
242
+ const HIDE_UI_TIMEOUT_MS = 3000;
243
+ let forceShowUI = false;
244
+ let hideTimeout: ReturnType<typeof setTimeout>;
245
+ let isHidden = false;
246
+
247
+ function hideUI() {
248
+ if (forceShowUI) {
249
+ return;
92
250
  }
93
- i++;
251
+ lightbox.pswp?.element?.classList.remove("pswp--ui-visible");
252
+ lightbox.pswp?.element?.classList.add("pswp--hide-cursor");
253
+ isHidden = true;
94
254
  }
95
- const visibleState = visible || isPhone;
96
- if (imagesRegistry.size > 1) {
97
- const size = isPhone ? "xLarge" : "small";
98
- return (
99
- <div
100
- className={css`
101
- position: fixed;
102
- bottom: max(var(${safeAreaInsetBottomVar}), 12px);
103
- left: 0;
104
- right: 0;
105
- display: grid;
106
- align-items: center;
107
- justify-content: center;
108
- &:hover .${controlsGroupStyle}.${buttonGroup} {
109
- display: flex;
110
- }
111
- `}
112
- >
113
- <ButtonGroup
114
- className={cx(
115
- controlsGroupStyle,
116
- visibleState &&
117
- css`
118
- &.${controlsGroupStyle}.${buttonGroup} {
119
- display: flex;
120
- }
121
- `
122
- )}
123
- >
124
- <IconButton
125
- variant={"outline"}
126
- size={size}
127
- onClick={() => {
128
- prevImage();
129
- setVisible(true);
130
- }}
131
- disabled={!activePrev}
132
- >
133
- <ArrowLeft />
134
- </IconButton>
135
- <IconButton
136
- variant={"outline"}
137
- size={size}
138
- onClick={() => {
139
- nextImage();
140
- setVisible(true);
141
- }}
142
- disabled={!activeNext}
143
- >
144
- <ArrowRight />
145
- </IconButton>
146
- </ButtonGroup>
147
- </div>
148
- );
255
+
256
+ function showUI() {
257
+ if (isHidden) {
258
+ lightbox.pswp?.element?.classList.add("pswp--ui-visible");
259
+ lightbox.pswp?.element?.classList.remove("pswp--hide-cursor");
260
+ isHidden = false;
261
+ }
262
+ clearTimeout(hideTimeout);
263
+ hideTimeout = setTimeout(hideUI, HIDE_UI_TIMEOUT_MS);
149
264
  }
150
- return null;
265
+
266
+ lightbox.on("afterInit", () => {
267
+ const pswp = lightbox.pswp;
268
+
269
+ if (!pswp) {
270
+ return;
271
+ }
272
+
273
+ pswp.element?.addEventListener("mousemove", showUI);
274
+ pswp.element?.addEventListener("touchstart", showUI);
275
+
276
+ hideTimeout = setTimeout(hideUI, HIDE_UI_TIMEOUT_MS);
277
+ });
278
+
279
+ const uiHandlers = {
280
+ showUI,
281
+ setForceShowUI: (value: boolean) => {
282
+ forceShowUI = value;
283
+ // resets timer
284
+ if (!forceShowUI) {
285
+ showUI();
286
+ }
287
+ },
288
+ };
289
+
290
+ lightbox.on("uiRegister", () => {
291
+ lightbox.pswp?.ui?.registerElement({
292
+ name: "custom-nav",
293
+ order: 8,
294
+ isButton: false,
295
+ appendTo: "wrapper",
296
+ onInit: (el, pswp) => {
297
+ createRoot(el).render(
298
+ <SlideButtons
299
+ items={items}
300
+ pswp={pswp}
301
+ toastQueue={toastQueue}
302
+ themeMode={themeMode}
303
+ uiHandlers={uiHandlers}
304
+ />
305
+ );
306
+ },
307
+ });
308
+ });
151
309
  }
152
310
 
153
- export function ImagesGalleryProvider({children}: {children: ReactNode}) {
154
- const [imagesRegistry] = useState<Registry>(() => new Map());
155
- const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
156
- const [galleryState, dispatchState] = useReducer(imagesGalleryReducer, [], initialState);
157
- const setVisible = useCallback((visible: boolean) => dispatchState({type: "set-visible", payload: visible}), []);
158
- const setActivated = useCallback(
159
- (activated: boolean) => dispatchState({type: "set-activated", payload: activated}),
160
- []
311
+ export function createPhotoSwipeLightbox(
312
+ {
313
+ dataSource,
314
+ ...rest
315
+ }: Partial<Omit<PhotoSwipeOptions, "dataSource">> & {
316
+ dataSource: {src: string; label?: string; width: number; height: number; alt?: string}[];
317
+ },
318
+ {themeMode, toastQueue}: {themeMode: ThemeMode; toastQueue: ToastQueue}
319
+ ) {
320
+ const photoSwipeLightbox = new PhotoSwipeLightbox({
321
+ dataSource: dataSource,
322
+ mainClass: galleryMainClass,
323
+ pswpModule: () => import("photoswipe"),
324
+ preloaderDelay: 1,
325
+ arrowPrev: false,
326
+ arrowNext: false,
327
+ close: false,
328
+ zoom: false,
329
+ loop: false,
330
+ ...rest,
331
+ });
332
+ attachCustomControls(
333
+ photoSwipeLightbox,
334
+ dataSource.map(() => {
335
+ return {src: "", type: "image", zoomed: false, setZoomed: () => {}};
336
+ }),
337
+ toastQueue,
338
+ themeMode
161
339
  );
162
- const update = useCallback(() => dispatchState({type: "inc"}), []);
163
- const {visible} = galleryState;
164
- const timeout = 3000;
165
- useEffect(() => {
166
- if (visible) {
167
- clearTimeout(timeoutRef.current);
168
- timeoutRef.current = setTimeout(() => {
169
- setVisible(false);
170
- }, timeout);
340
+ return photoSwipeLightbox;
341
+ }
342
+
343
+ function initGallery(
344
+ imagesRegistry: Registry,
345
+ {toastQueue, themeMode, isPhone}: {toastQueue: ToastQueue; themeMode: ThemeMode; isPhone: boolean}
346
+ ) {
347
+ const items = [...imagesRegistry.values()];
348
+ const dataSource: GalleryEntry[] = items.map((item) => ({
349
+ ...item,
350
+ state: "none",
351
+ contentType: item.contentType ?? null,
352
+ }));
353
+
354
+ const zoomedIndex = dataSource.findIndex((item) => item.zoomed);
355
+
356
+ const lightbox = new PhotoSwipeLightbox({
357
+ dataSource,
358
+ mainClass: galleryMainClass,
359
+ pswpModule: () => import("photoswipe"),
360
+ preloaderDelay: 1,
361
+ arrowPrev: false,
362
+ arrowNext: false,
363
+ close: false,
364
+ zoom: false,
365
+ loop: false,
366
+ });
367
+
368
+ lightbox.addFilter("numItems", (_numItems) => {
369
+ return dataSource.length;
370
+ });
371
+
372
+ lightbox.addFilter("itemData", (_itemData, index) => {
373
+ const item = dataSource[index];
374
+
375
+ if (item.type === "video" || item.type === "iframe") {
376
+ return item;
171
377
  }
172
- return () => {
173
- clearTimeout(timeoutRef.current);
174
- };
175
- }, [setVisible, visible]);
176
- const nextImage = useCallback(() => {
177
- let i = 0;
178
- for (const image of imagesRegistry.values()) {
179
- if (image.zoomed) {
180
- if (i === imagesRegistry.size - 1) {
378
+
379
+ if (item.state === "loaded" || item.state === "error") {
380
+ return item;
381
+ }
382
+
383
+ return {};
384
+ });
385
+
386
+ lightbox.addFilter("isContentLoading", (_isContentLoading, content) => {
387
+ const item = dataSource[content.index];
388
+ if (item.type === "video" || item.type === "iframe") {
389
+ return false;
390
+ }
391
+ const complete = item.state === "loaded" || item.state === "error";
392
+ return !complete;
393
+ });
394
+
395
+ lightbox.addFilter("useContentPlaceholder", () => false);
396
+
397
+ lightbox.init();
398
+
399
+ lightbox.on("contentActivate", (e) => {
400
+ dataSource[e.content.index].setZoomed(true);
401
+ lightbox.pswp?.element?.focus();
402
+ });
403
+
404
+ lightbox.on("contentDeactivate", (e) => {
405
+ dataSource[e.content.index].setZoomed(false);
406
+ e.content.element?.querySelector("video")?.pause();
407
+ });
408
+
409
+ function refreshSlide(index: number) {
410
+ lightbox.pswp?.refreshSlideContent(index);
411
+ if (lightbox.pswp?.currIndex === index) {
412
+ lightbox.pswp?.updateSize(true);
413
+ }
414
+ }
415
+
416
+ const contentAppendTimeouts = [] as ReturnType<typeof setTimeout>[];
417
+ lightbox.on("contentAppend", (e) => {
418
+ // sometimes lightbox's slide is loaded with stale (empty) data
419
+ if (e.content.data !== dataSource[e.content.index]) {
420
+ contentAppendTimeouts.push(
421
+ setTimeout(() => {
422
+ refreshSlide(e.content.index);
423
+ }, 0)
424
+ );
425
+ }
426
+ });
427
+
428
+ lightbox.on("contentRemove", (e) => {
429
+ const item = dataSource[e.content.index];
430
+
431
+ if (item.type === "video" || item.type === "iframe") {
432
+ item.state = "none";
433
+ }
434
+ });
435
+
436
+ lightbox.on("contentLoad", (e) => {
437
+ const item = dataSource[e.content.index];
438
+
439
+ if (item.state === "loading") {
440
+ return;
441
+ }
442
+
443
+ if (item.state === "none") {
444
+ e.preventDefault();
445
+ const eventArgs = {
446
+ e,
447
+ item,
448
+ refreshSlide,
449
+ getPswp: () => lightbox.pswp,
450
+ isPhone,
451
+ };
452
+
453
+ switch (item.type) {
454
+ case "image": {
455
+ onImageContentLoad(eventArgs);
181
456
  break;
182
457
  }
183
- image.setZoomed(false);
184
- const nextImage = Array.from(imagesRegistry.values())[i + 1];
185
- nextImage.setZoomed(true);
186
- break;
187
- }
188
- i++;
189
- }
190
- clearTimeout(timeoutRef.current);
191
- timeoutRef.current = setTimeout(() => {
192
- setVisible(false);
193
- }, timeout);
194
- }, [imagesRegistry, setVisible]);
195
-
196
- const prevImage = useCallback(() => {
197
- let i = 0;
198
- for (const image of imagesRegistry.values()) {
199
- if (image.zoomed) {
200
- if (i === 0) {
458
+ case "video": {
459
+ onVideoContentLoad(eventArgs);
460
+ break;
461
+ }
462
+ case "iframe": {
463
+ onIframeContentLoad(eventArgs);
201
464
  break;
202
465
  }
203
- image.setZoomed(false);
204
- const prevImage = Array.from(imagesRegistry.values())[i - 1];
205
- prevImage.setZoomed(true);
206
- break;
466
+ default: {
467
+ throw new Error(`unexpected slide type: ${item.type}`);
468
+ }
207
469
  }
208
- i++;
470
+ } else if (item.state === "error") {
471
+ e.preventDefault();
472
+ e.content.onError();
209
473
  }
210
- clearTimeout(timeoutRef.current);
211
- timeoutRef.current = setTimeout(() => {
212
- setVisible(false);
213
- }, timeout);
214
- }, [imagesRegistry, setVisible]);
215
-
216
- const methods = useMemo(
217
- () => ({setVisible, setActivated, update, nextImage, prevImage}),
218
- [setVisible, setActivated, update, nextImage, prevImage]
219
- );
474
+ });
220
475
 
221
- useEffect(() => {
222
- const listener = (evt: KeyboardEvent) => {
223
- const hasActive = Array.from(imagesRegistry.values()).some((image) => image.zoomed);
224
- if (!hasActive) {
225
- return;
226
- }
227
- if (evt.key === "Escape") {
228
- for (const image of imagesRegistry.values()) {
229
- if (image.zoomed) {
230
- image.setZoomed(false);
231
- break;
232
- }
233
- }
234
- }
235
- if (evt.key === "ArrowRight") {
236
- nextImage();
237
- }
238
- if (evt.key === "ArrowLeft") {
239
- prevImage();
240
- }
476
+ attachCustomControls(lightbox, items, toastQueue, themeMode);
477
+
478
+ lightbox.on("destroy", () => {
479
+ contentAppendTimeouts.forEach((timeoutId) => {
480
+ clearTimeout(timeoutId);
481
+ });
482
+ });
483
+
484
+ lightbox.loadAndOpen(zoomedIndex === -1 ? 0 : zoomedIndex);
485
+
486
+ return lightbox;
487
+ }
488
+
489
+ type ContentLoadArg = {
490
+ e: PhotoSwipeEventsMap["contentLoad"];
491
+ item: GalleryEntry;
492
+ refreshSlide: (index: number) => void;
493
+ getPswp: () => PhotoSwipeLightbox["pswp"];
494
+ isPhone: boolean;
495
+ };
496
+
497
+ function onImageContentLoad({e, item, refreshSlide}: ContentLoadArg) {
498
+ item.state = "loading";
499
+ // we ok if content type request fails
500
+ const contentTypePromise = item.contentType
501
+ ? Promise.resolve(item.contentType)
502
+ : fetch(item.src, {method: "HEAD"})
503
+ .then((response) => response.headers.get("content-type"))
504
+ .catch();
505
+
506
+ const contentPromise = new Promise<{width: number; height: number}>((resolve, reject) => {
507
+ const onLoad = () => {
508
+ resolve({
509
+ width: img.naturalWidth,
510
+ height: img.naturalHeight,
511
+ });
241
512
  };
242
- document.addEventListener("keydown", listener, {capture: true});
243
- return () => {
244
- document.removeEventListener("keydown", listener, {capture: true});
513
+
514
+ const img = new Image();
515
+ img.src = item.src;
516
+ if (img.complete) {
517
+ onLoad();
518
+ }
519
+ img.onload = () => onLoad();
520
+ img.onerror = () => {
521
+ reject("error");
245
522
  };
246
- }, [imagesRegistry, nextImage, prevImage]);
523
+ });
247
524
 
248
- return (
249
- <>
250
- <ImagesGalleryRegistryProvider value={imagesRegistry}>
251
- <ImagesGalleryMethodsProvider value={methods}>
252
- <ImagesGalleryStateProvider value={galleryState}>{children}</ImagesGalleryStateProvider>
253
- </ImagesGalleryMethodsProvider>
254
- </ImagesGalleryRegistryProvider>
255
- </>
256
- );
525
+ Promise.allSettled([contentTypePromise, contentPromise]).then(([contentTypeResult, contentResult]) => {
526
+ if (contentResult.status === "rejected") {
527
+ item.state = "error";
528
+ refreshSlide(e.content.index);
529
+ e.content.onError();
530
+ return;
531
+ }
532
+
533
+ item.state = "loaded";
534
+ item.width = contentResult.value.width;
535
+ item.height = contentResult.value.height;
536
+
537
+ // normalize
538
+ if (contentTypeResult.status === "fulfilled" && contentTypeResult.value?.startsWith("image/svg")) {
539
+ const scale = Math.min(3000 / item.width!, 3000 / item.height!);
540
+ item.width = Math.ceil(scale * item.width!);
541
+ item.height = Math.ceil(scale * item.height!);
542
+ }
543
+
544
+ refreshSlide(e.content.index);
545
+ e.content.onLoaded();
546
+ });
547
+ }
548
+
549
+ function onVideoContentLoad({e, item, refreshSlide, getPswp, isPhone}: ContentLoadArg) {
550
+ item.state = "loaded";
551
+
552
+ e.content.element = document.createElement("div");
553
+ e.content.element.className = "pswp__video-container";
554
+ e.content.element.onclick = () => {
555
+ getPswp()?.close();
556
+ };
557
+
558
+ const video = document.createElement("video");
559
+ video.controls = true;
560
+ video.src = item.src;
561
+ video.onerror = () => {
562
+ item.state = "error";
563
+ refreshSlide(e.content.index);
564
+ e.content.onError();
565
+ };
566
+ video.onclick = (e) => {
567
+ e.preventDefault();
568
+ e.stopPropagation();
569
+ if (isPhone) {
570
+ return;
571
+ }
572
+ if (video.paused) {
573
+ video.play().catch();
574
+ } else {
575
+ video.pause();
576
+ }
577
+ };
578
+
579
+ e.content.element.appendChild(video);
580
+ }
581
+
582
+ function onIframeContentLoad({e, item, getPswp}: ContentLoadArg) {
583
+ item.state = "loaded";
584
+
585
+ e.content.element = document.createElement("div");
586
+ e.content.element.className = "pswp__iframe-container";
587
+
588
+ e.content.element.onclick = () => {
589
+ getPswp()?.close();
590
+ };
591
+
592
+ const iframe = document.createElement("iframe");
593
+ iframe.src = item.src;
594
+ iframe.setAttribute("allowfullscreen", "");
595
+ e.content.element.appendChild(iframe);
257
596
  }