@hoangyell/gallery 1.0.7 → 1.1.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/README.md +3 -1
- package/package.json +1 -1
- package/src/Gallery.astro +694 -51
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
- **Pyramid layout** — distribute images across rows with `rows` prop
|
|
30
30
|
- **Two fit modes** — `cover` (default, uniform photo grid) or `contain` (natural aspect, for diagrams and tall portraits)
|
|
31
|
-
- **Lightbox** — click to open,
|
|
31
|
+
- **Lightbox & pinch-to-zoom** — click to open, smooth multi-touch pinch-to-zoom & pan, double-tap zoom, mouse wheel, keyboard navigation (`+`, `-`, `0`, arrows, ESC)
|
|
32
32
|
- **Corner rounding** — only the four outer corners of each group are rounded
|
|
33
33
|
- **Hover zoom** — image scales inside its cell without changing layout
|
|
34
34
|
- **Accessible** — focus management, ARIA labels, safe-area controls, reduced-motion support
|
|
@@ -81,6 +81,8 @@ import { Gallery } from "@hoangyell/gallery";
|
|
|
81
81
|
| `orientation` | `"landscape" \| "portrait"` | `"landscape"` | Item aspect ratio hint |
|
|
82
82
|
| `scale` | `number` | `1` | Gallery width scale (0–1) |
|
|
83
83
|
| `fit` | `ObjectFit` | `"cover"` | Default CSS object-fit — see Fit modes below |
|
|
84
|
+
| `zoom` | `boolean` | `true` | Enable pinch-to-zoom, pan, double-tap, and wheel zoom |
|
|
85
|
+
| `zoomControls` | `boolean` | `false` | Show visible zoom in / zoom out / reset buttons in lightbox |
|
|
84
86
|
| `class` | `string` | — | Extra wrapper class |
|
|
85
87
|
|
|
86
88
|
## Fit modes
|
package/package.json
CHANGED
package/src/Gallery.astro
CHANGED
|
@@ -29,6 +29,10 @@ export interface Props {
|
|
|
29
29
|
scale?: number;
|
|
30
30
|
/** Global object-fit for all items unless overridden per-item. */
|
|
31
31
|
fit?: ObjectFit;
|
|
32
|
+
/** Enable zoom gestures (pinch, double-tap, pan, wheel) in lightbox. Defaults to true. */
|
|
33
|
+
zoom?: boolean;
|
|
34
|
+
/** Show visible zoom in/out/reset buttons in the lightbox toolbar. Defaults to false. */
|
|
35
|
+
zoomControls?: boolean;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const {
|
|
@@ -37,6 +41,8 @@ const {
|
|
|
37
41
|
orientation = "landscape",
|
|
38
42
|
scale = 1,
|
|
39
43
|
fit = "cover",
|
|
44
|
+
zoom = true,
|
|
45
|
+
zoomControls = false,
|
|
40
46
|
} = Astro.props;
|
|
41
47
|
|
|
42
48
|
const rawItems = Array.isArray(Astro.props.items) ? Astro.props.items : [];
|
|
@@ -101,6 +107,8 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
101
107
|
data-hy-gallery
|
|
102
108
|
data-orientation={orientation}
|
|
103
109
|
data-fit={fit}
|
|
110
|
+
data-zoom={zoom ? "true" : "false"}
|
|
111
|
+
data-zoom-controls={zoomControls ? "true" : "false"}
|
|
104
112
|
style={scale !== 1 ? `width: ${scale * 100}%; margin: 2rem auto;` : undefined}
|
|
105
113
|
>
|
|
106
114
|
{groupedItems.map((group, rowIdx) => {
|
|
@@ -340,6 +348,9 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
340
348
|
opacity: 0;
|
|
341
349
|
transition: opacity 0.3s ease;
|
|
342
350
|
cursor: zoom-out;
|
|
351
|
+
touch-action: none;
|
|
352
|
+
user-select: none;
|
|
353
|
+
-webkit-user-select: none;
|
|
343
354
|
}
|
|
344
355
|
|
|
345
356
|
:global(.hy-gallery-overlay.is-active) {
|
|
@@ -353,6 +364,8 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
353
364
|
display: flex;
|
|
354
365
|
align-items: center;
|
|
355
366
|
justify-content: center;
|
|
367
|
+
overflow: visible;
|
|
368
|
+
touch-action: none;
|
|
356
369
|
}
|
|
357
370
|
|
|
358
371
|
:global(.hy-gallery-overlay-img) {
|
|
@@ -362,15 +375,22 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
362
375
|
border-radius: 0.5rem;
|
|
363
376
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
364
377
|
border: 1px solid var(--hy-gallery-border);
|
|
365
|
-
cursor:
|
|
378
|
+
cursor: zoom-in;
|
|
366
379
|
user-select: none;
|
|
367
|
-
|
|
380
|
+
-webkit-user-select: none;
|
|
381
|
+
-webkit-user-drag: none;
|
|
382
|
+
touch-action: none;
|
|
383
|
+
transform-origin: center center;
|
|
384
|
+
will-change: transform;
|
|
368
385
|
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
|
369
|
-
will-change: transform, opacity;
|
|
370
386
|
}
|
|
371
387
|
|
|
372
|
-
:global(.hy-gallery-overlay.is-
|
|
373
|
-
|
|
388
|
+
:global(.hy-gallery-overlay-img.is-zoomed) {
|
|
389
|
+
cursor: grab;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
:global(.hy-gallery-overlay-img.is-dragging) {
|
|
393
|
+
cursor: grabbing !important;
|
|
374
394
|
}
|
|
375
395
|
|
|
376
396
|
:global(.hy-gallery-slide-anim) {
|
|
@@ -412,10 +432,18 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
412
432
|
backdrop-filter: blur(4px);
|
|
413
433
|
}
|
|
414
434
|
|
|
415
|
-
:global(.hy-gallery-
|
|
435
|
+
:global(.hy-gallery-controls) {
|
|
416
436
|
position: absolute;
|
|
417
437
|
top: max(env(safe-area-inset-top, 0px), 16px);
|
|
418
438
|
right: max(env(safe-area-inset-right, 0px), 16px);
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 8px;
|
|
442
|
+
z-index: 10;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
:global(.hy-gallery-btn),
|
|
446
|
+
:global(.hy-gallery-close) {
|
|
419
447
|
width: 2.5rem;
|
|
420
448
|
height: 2.5rem;
|
|
421
449
|
display: flex;
|
|
@@ -426,10 +454,41 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
426
454
|
background: var(--hy-gallery-control-bg);
|
|
427
455
|
color: var(--hy-gallery-text);
|
|
428
456
|
cursor: pointer;
|
|
429
|
-
transition: transform 0.2s ease;
|
|
457
|
+
transition: transform 0.2s ease, color 0.2s ease, opacity 0.2s ease;
|
|
430
458
|
z-index: 10;
|
|
431
459
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
432
460
|
backdrop-filter: blur(4px);
|
|
461
|
+
padding: 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
:global(.hy-gallery-btn:hover),
|
|
465
|
+
:global(.hy-gallery-btn:focus-visible),
|
|
466
|
+
:global(.hy-gallery-close:hover),
|
|
467
|
+
:global(.hy-gallery-close:focus-visible) {
|
|
468
|
+
transform: scale(1.08);
|
|
469
|
+
color: var(--hy-gallery-accent);
|
|
470
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
:global(.hy-gallery-btn:disabled),
|
|
474
|
+
:global(.hy-gallery-btn[aria-disabled="true"]) {
|
|
475
|
+
opacity: 0.35;
|
|
476
|
+
cursor: not-allowed;
|
|
477
|
+
transform: none !important;
|
|
478
|
+
pointer-events: none;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
:global(.hy-gallery-btn:focus),
|
|
482
|
+
:global(.hy-gallery-close:focus),
|
|
483
|
+
:global(.hy-gallery-nav:focus) {
|
|
484
|
+
outline: none;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
:global(.hy-gallery-btn:focus-visible),
|
|
488
|
+
:global(.hy-gallery-close:focus-visible),
|
|
489
|
+
:global(.hy-gallery-nav:focus-visible) {
|
|
490
|
+
outline: 2px solid var(--hy-gallery-accent);
|
|
491
|
+
outline-offset: 2px;
|
|
433
492
|
}
|
|
434
493
|
|
|
435
494
|
:global(.hy-gallery-nav) {
|
|
@@ -460,24 +519,34 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
460
519
|
right: max(env(safe-area-inset-right, 0px), 16px);
|
|
461
520
|
}
|
|
462
521
|
|
|
463
|
-
:global(.hy-gallery-close:hover),
|
|
464
522
|
:global(.hy-gallery-nav:hover),
|
|
465
|
-
:global(.hy-gallery-close:focus-visible),
|
|
466
523
|
:global(.hy-gallery-nav:focus-visible) {
|
|
467
|
-
transform: scale(1.1);
|
|
524
|
+
transform: translateY(-50%) scale(1.1);
|
|
468
525
|
color: var(--hy-gallery-accent);
|
|
469
526
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
470
527
|
}
|
|
471
528
|
|
|
472
|
-
:global(.hy-gallery-nav:hover),
|
|
473
|
-
:global(.hy-gallery-nav:focus-visible) {
|
|
474
|
-
transform: translateY(-50%) scale(1.1);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
529
|
@media (max-width: 639px) {
|
|
530
|
+
:global(.hy-gallery-controls) {
|
|
531
|
+
top: max(env(safe-area-inset-top, 0px), 8px);
|
|
532
|
+
right: max(env(safe-area-inset-right, 0px), 8px);
|
|
533
|
+
gap: 6px;
|
|
534
|
+
}
|
|
535
|
+
:global(.hy-gallery-counter) {
|
|
536
|
+
top: max(env(safe-area-inset-top, 0px), 8px);
|
|
537
|
+
left: max(env(safe-area-inset-left, 0px), 8px);
|
|
538
|
+
width: 2.25rem;
|
|
539
|
+
height: 2.25rem;
|
|
540
|
+
font-size: 0.625rem;
|
|
541
|
+
}
|
|
542
|
+
:global(.hy-gallery-btn),
|
|
543
|
+
:global(.hy-gallery-close) {
|
|
544
|
+
width: 2.25rem;
|
|
545
|
+
height: 2.25rem;
|
|
546
|
+
}
|
|
478
547
|
:global(.hy-gallery-nav) {
|
|
479
|
-
width: 2.
|
|
480
|
-
height: 2.
|
|
548
|
+
width: 2.25rem;
|
|
549
|
+
height: 2.25rem;
|
|
481
550
|
}
|
|
482
551
|
:global(.hy-gallery-nav-prev) {
|
|
483
552
|
left: max(env(safe-area-inset-left, 0px), 8px);
|
|
@@ -497,18 +566,55 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
497
566
|
counterEl: HTMLElement | null;
|
|
498
567
|
prevBtn: HTMLButtonElement | null;
|
|
499
568
|
nextBtn: HTMLButtonElement | null;
|
|
569
|
+
zoomControls: HTMLElement | null;
|
|
570
|
+
zoomInBtn: HTMLButtonElement | null;
|
|
571
|
+
zoomOutBtn: HTMLButtonElement | null;
|
|
572
|
+
zoomResetBtn: HTMLButtonElement | null;
|
|
573
|
+
closeBtn: HTMLButtonElement | null;
|
|
500
574
|
currentIndex: number;
|
|
575
|
+
zoomEnabled: boolean;
|
|
576
|
+
zoomControlsEnabled: boolean;
|
|
577
|
+
|
|
578
|
+
// Zoom & pan state
|
|
579
|
+
scale: number;
|
|
580
|
+
translateX: number;
|
|
581
|
+
translateY: number;
|
|
582
|
+
|
|
583
|
+
// Touch tracking
|
|
501
584
|
touchStartX: number;
|
|
502
585
|
touchStartY: number;
|
|
586
|
+
touchLastX: number;
|
|
587
|
+
touchLastY: number;
|
|
588
|
+
isPinching: boolean;
|
|
589
|
+
isDragging: boolean;
|
|
590
|
+
pinchStartDist: number;
|
|
591
|
+
pinchStartScale: number;
|
|
592
|
+
pinchStartCenter: { x: number; y: number };
|
|
593
|
+
pinchStartTranslate: { x: number; y: number };
|
|
594
|
+
lastTapTime: number;
|
|
595
|
+
|
|
596
|
+
// Mouse tracking
|
|
597
|
+
isMouseDown: boolean;
|
|
598
|
+
mouseStartX: number;
|
|
599
|
+
mouseStartY: number;
|
|
600
|
+
mouseLastX: number;
|
|
601
|
+
mouseLastY: number;
|
|
602
|
+
hasMouseDragged: boolean;
|
|
603
|
+
isAnimating: boolean;
|
|
604
|
+
|
|
605
|
+
// Body scroll lock state
|
|
503
606
|
lockedScrollY: number;
|
|
504
607
|
previousBodyOverflow: string;
|
|
505
608
|
previousBodyPosition: string;
|
|
506
609
|
previousBodyTop: string;
|
|
507
610
|
previousBodyWidth: string;
|
|
508
611
|
previousHtmlOverflow: string;
|
|
612
|
+
previousHtmlScrollBehavior: string;
|
|
509
613
|
|
|
510
614
|
constructor(el: HTMLElement) {
|
|
511
615
|
this.gallery = el;
|
|
616
|
+
this.zoomEnabled = el.dataset.zoom !== "false";
|
|
617
|
+
this.zoomControlsEnabled = el.dataset.zoomControls === "true";
|
|
512
618
|
this.items = Array.from(
|
|
513
619
|
el.querySelectorAll<HTMLButtonElement>(".hy-gallery-item"),
|
|
514
620
|
);
|
|
@@ -517,15 +623,44 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
517
623
|
this.counterEl = null;
|
|
518
624
|
this.prevBtn = null;
|
|
519
625
|
this.nextBtn = null;
|
|
626
|
+
this.zoomControls = null;
|
|
627
|
+
this.zoomInBtn = null;
|
|
628
|
+
this.zoomOutBtn = null;
|
|
629
|
+
this.zoomResetBtn = null;
|
|
630
|
+
this.closeBtn = null;
|
|
520
631
|
this.currentIndex = -1;
|
|
632
|
+
|
|
633
|
+
this.scale = 1;
|
|
634
|
+
this.translateX = 0;
|
|
635
|
+
this.translateY = 0;
|
|
636
|
+
|
|
521
637
|
this.touchStartX = 0;
|
|
522
638
|
this.touchStartY = 0;
|
|
639
|
+
this.touchLastX = 0;
|
|
640
|
+
this.touchLastY = 0;
|
|
641
|
+
this.isPinching = false;
|
|
642
|
+
this.isDragging = false;
|
|
643
|
+
this.pinchStartDist = 0;
|
|
644
|
+
this.pinchStartScale = 1;
|
|
645
|
+
this.pinchStartCenter = { x: 0, y: 0 };
|
|
646
|
+
this.pinchStartTranslate = { x: 0, y: 0 };
|
|
647
|
+
this.lastTapTime = 0;
|
|
648
|
+
|
|
649
|
+
this.isMouseDown = false;
|
|
650
|
+
this.mouseStartX = 0;
|
|
651
|
+
this.mouseStartY = 0;
|
|
652
|
+
this.mouseLastX = 0;
|
|
653
|
+
this.mouseLastY = 0;
|
|
654
|
+
this.hasMouseDragged = false;
|
|
655
|
+
this.isAnimating = false;
|
|
656
|
+
|
|
523
657
|
this.lockedScrollY = -1;
|
|
524
658
|
this.previousBodyOverflow = "";
|
|
525
659
|
this.previousBodyPosition = "";
|
|
526
660
|
this.previousBodyTop = "";
|
|
527
661
|
this.previousBodyWidth = "";
|
|
528
662
|
this.previousHtmlOverflow = "";
|
|
663
|
+
this.previousHtmlScrollBehavior = "";
|
|
529
664
|
|
|
530
665
|
this.bind();
|
|
531
666
|
document.addEventListener("astro:before-swap", this.forceClose);
|
|
@@ -542,15 +677,173 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
542
677
|
});
|
|
543
678
|
};
|
|
544
679
|
|
|
680
|
+
getContentCenter = () => {
|
|
681
|
+
return {
|
|
682
|
+
x: window.innerWidth / 2,
|
|
683
|
+
y: window.innerHeight / 2,
|
|
684
|
+
};
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
applyTransform = (withTransition = false) => {
|
|
688
|
+
if (!this.imgEl) return;
|
|
689
|
+
if (withTransition) {
|
|
690
|
+
this.imgEl.style.transition =
|
|
691
|
+
"transform 0.28s cubic-bezier(0.16, 1, 0.3, 1)";
|
|
692
|
+
} else {
|
|
693
|
+
this.imgEl.style.transition = "none";
|
|
694
|
+
}
|
|
695
|
+
this.imgEl.style.transform = `translate3d(${this.translateX}px, ${this.translateY}px, 0px) scale(${this.scale})`;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
animateTo = (targetScale: number, targetTx: number, targetTy: number) => {
|
|
699
|
+
this.scale = targetScale;
|
|
700
|
+
this.translateX = targetTx;
|
|
701
|
+
this.translateY = targetTy;
|
|
702
|
+
this.isAnimating = true;
|
|
703
|
+
this.applyTransform(true);
|
|
704
|
+
this.updateZoomUI();
|
|
705
|
+
window.setTimeout(() => {
|
|
706
|
+
this.isAnimating = false;
|
|
707
|
+
if (this.imgEl && !this.isPinching && !this.isMouseDown) {
|
|
708
|
+
this.imgEl.style.transition = "";
|
|
709
|
+
}
|
|
710
|
+
}, 300);
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
resetZoom = (animated = false) => {
|
|
714
|
+
if (animated) {
|
|
715
|
+
this.animateTo(1, 0, 0);
|
|
716
|
+
} else {
|
|
717
|
+
this.scale = 1;
|
|
718
|
+
this.translateX = 0;
|
|
719
|
+
this.translateY = 0;
|
|
720
|
+
this.applyTransform(false);
|
|
721
|
+
this.updateZoomUI();
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
clampToBounds = (animate = false) => {
|
|
726
|
+
if (!this.imgEl) return;
|
|
727
|
+
if (this.scale <= 1.01) {
|
|
728
|
+
if (animate) {
|
|
729
|
+
this.animateTo(1, 0, 0);
|
|
730
|
+
} else {
|
|
731
|
+
this.resetZoom(false);
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const baseW = this.imgEl.offsetWidth || this.imgEl.naturalWidth || 800;
|
|
737
|
+
const baseH = this.imgEl.offsetHeight || this.imgEl.naturalHeight || 600;
|
|
738
|
+
const scaledW = baseW * this.scale;
|
|
739
|
+
const scaledH = baseH * this.scale;
|
|
740
|
+
|
|
741
|
+
const vpW = window.innerWidth;
|
|
742
|
+
const vpH = window.innerHeight;
|
|
743
|
+
|
|
744
|
+
const maxTx = Math.max(0, (scaledW - vpW) / 2 + 24);
|
|
745
|
+
const maxTy = Math.max(0, (scaledH - vpH) / 2 + 24);
|
|
746
|
+
|
|
747
|
+
const clampedTx = Math.min(maxTx, Math.max(-maxTx, this.translateX));
|
|
748
|
+
const clampedTy = Math.min(maxTy, Math.max(-maxTy, this.translateY));
|
|
749
|
+
|
|
750
|
+
if (animate) {
|
|
751
|
+
this.animateTo(this.scale, clampedTx, clampedTy);
|
|
752
|
+
} else {
|
|
753
|
+
this.translateX = clampedTx;
|
|
754
|
+
this.translateY = clampedTy;
|
|
755
|
+
this.applyTransform(false);
|
|
756
|
+
this.updateZoomUI();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
zoomBy = (factor: number) => {
|
|
761
|
+
if (!this.zoomEnabled) return;
|
|
762
|
+
const newScale = Math.min(5.0, Math.max(1.0, this.scale * factor));
|
|
763
|
+
if (newScale <= 1.01) {
|
|
764
|
+
this.resetZoom(true);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const ratio = newScale / this.scale;
|
|
768
|
+
this.scale = newScale;
|
|
769
|
+
this.translateX = this.translateX * ratio;
|
|
770
|
+
this.translateY = this.translateY * ratio;
|
|
771
|
+
this.clampToBounds(true);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
panBy = (dx: number, dy: number) => {
|
|
775
|
+
this.translateX += dx;
|
|
776
|
+
this.translateY += dy;
|
|
777
|
+
this.clampToBounds(true);
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
handleDoubleTap = (clientX: number, clientY: number) => {
|
|
781
|
+
if (!this.zoomEnabled) return;
|
|
782
|
+
if (this.scale > 1.15) {
|
|
783
|
+
this.resetZoom(true);
|
|
784
|
+
} else {
|
|
785
|
+
const targetScale = 2.5;
|
|
786
|
+
const center = this.getContentCenter();
|
|
787
|
+
const targetTx = clientX - center.x - targetScale * (clientX - center.x);
|
|
788
|
+
const targetTy = clientY - center.y - targetScale * (clientY - center.y);
|
|
789
|
+
this.scale = targetScale;
|
|
790
|
+
this.translateX = targetTx;
|
|
791
|
+
this.translateY = targetTy;
|
|
792
|
+
this.clampToBounds(true);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
updateZoomUI = () => {
|
|
797
|
+
if (!this.zoomEnabled) return;
|
|
798
|
+
if (this.zoomOutBtn) {
|
|
799
|
+
const disabled = this.scale <= 1.01;
|
|
800
|
+
this.zoomOutBtn.disabled = disabled;
|
|
801
|
+
this.zoomOutBtn.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
802
|
+
}
|
|
803
|
+
if (this.zoomInBtn) {
|
|
804
|
+
const disabled = this.scale >= 4.95;
|
|
805
|
+
this.zoomInBtn.disabled = disabled;
|
|
806
|
+
this.zoomInBtn.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
807
|
+
}
|
|
808
|
+
if (this.zoomResetBtn) {
|
|
809
|
+
const isZoomed = this.scale > 1.01;
|
|
810
|
+
this.zoomResetBtn.disabled = !isZoomed;
|
|
811
|
+
this.zoomResetBtn.setAttribute("aria-disabled", !isZoomed ? "true" : "false");
|
|
812
|
+
const title = isZoomed
|
|
813
|
+
? `Reset zoom (${Math.round(this.scale * 100)}%)`
|
|
814
|
+
: "Reset zoom";
|
|
815
|
+
this.zoomResetBtn.title = title;
|
|
816
|
+
this.zoomResetBtn.setAttribute("aria-label", title);
|
|
817
|
+
}
|
|
818
|
+
if (this.imgEl) {
|
|
819
|
+
if (this.scale > 1.01) {
|
|
820
|
+
this.imgEl.classList.add("is-zoomed");
|
|
821
|
+
this.imgEl.style.cursor = this.isMouseDown ? "grabbing" : "grab";
|
|
822
|
+
} else {
|
|
823
|
+
this.imgEl.classList.remove("is-zoomed", "is-dragging");
|
|
824
|
+
this.imgEl.style.cursor = "zoom-in";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
545
829
|
forceClose = () => {
|
|
546
830
|
if (this.overlay) {
|
|
831
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
|
832
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
|
833
|
+
window.removeEventListener("resize", this.handleResize);
|
|
834
|
+
document.removeEventListener("keydown", this.handleKey);
|
|
835
|
+
document.removeEventListener("gesturestart", this.preventZoom);
|
|
836
|
+
|
|
547
837
|
this.overlay.remove();
|
|
548
838
|
this.overlay = null;
|
|
549
839
|
this.imgEl = null;
|
|
550
840
|
this.counterEl = null;
|
|
841
|
+
this.zoomControls = null;
|
|
842
|
+
this.zoomInBtn = null;
|
|
843
|
+
this.zoomOutBtn = null;
|
|
844
|
+
this.zoomResetBtn = null;
|
|
845
|
+
this.closeBtn = null;
|
|
551
846
|
|
|
552
|
-
document.removeEventListener("keydown", this.handleKey);
|
|
553
|
-
document.removeEventListener("gesturestart", this.preventZoom);
|
|
554
847
|
const main = document.querySelector("main");
|
|
555
848
|
if (main) main.removeAttribute("inert");
|
|
556
849
|
}
|
|
@@ -568,6 +861,7 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
568
861
|
|
|
569
862
|
this.imgEl.src = src;
|
|
570
863
|
this.imgEl.alt = alt;
|
|
864
|
+
this.resetZoom(false);
|
|
571
865
|
this.updateCounter();
|
|
572
866
|
this.updateNavButtons();
|
|
573
867
|
this.preload(index + 1);
|
|
@@ -597,16 +891,70 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
597
891
|
this.overlay.setAttribute("aria-modal", "true");
|
|
598
892
|
this.overlay.setAttribute("aria-label", "Image viewer");
|
|
599
893
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
894
|
+
// Counter (top left)
|
|
895
|
+
this.counterEl = document.createElement("div");
|
|
896
|
+
this.counterEl.className = "hy-gallery-counter";
|
|
897
|
+
this.counterEl.setAttribute("aria-hidden", "true");
|
|
898
|
+
|
|
899
|
+
// Controls (top right)
|
|
900
|
+
this.zoomControls = document.createElement("div");
|
|
901
|
+
this.zoomControls.className = "hy-gallery-controls";
|
|
902
|
+
|
|
903
|
+
if (this.zoomEnabled && this.zoomControlsEnabled) {
|
|
904
|
+
this.zoomOutBtn = document.createElement("button");
|
|
905
|
+
this.zoomOutBtn.className = "hy-gallery-btn hy-gallery-zoom-out";
|
|
906
|
+
this.zoomOutBtn.setAttribute("type", "button");
|
|
907
|
+
this.zoomOutBtn.setAttribute("aria-label", "Zoom out");
|
|
908
|
+
this.zoomOutBtn.title = "Zoom out (-)";
|
|
909
|
+
this.zoomOutBtn.disabled = true;
|
|
910
|
+
this.zoomOutBtn.setAttribute("aria-disabled", "true");
|
|
911
|
+
this.zoomOutBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`;
|
|
912
|
+
this.zoomOutBtn.addEventListener("click", (e) => {
|
|
913
|
+
e.stopPropagation();
|
|
914
|
+
this.zoomBy(1 / 1.5);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
this.zoomInBtn = document.createElement("button");
|
|
918
|
+
this.zoomInBtn.className = "hy-gallery-btn hy-gallery-zoom-in";
|
|
919
|
+
this.zoomInBtn.setAttribute("type", "button");
|
|
920
|
+
this.zoomInBtn.setAttribute("aria-label", "Zoom in");
|
|
921
|
+
this.zoomInBtn.title = "Zoom in (+)";
|
|
922
|
+
this.zoomInBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`;
|
|
923
|
+
this.zoomInBtn.addEventListener("click", (e) => {
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
this.zoomBy(1.5);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
this.zoomResetBtn = document.createElement("button");
|
|
929
|
+
this.zoomResetBtn.className = "hy-gallery-btn hy-gallery-zoom-reset";
|
|
930
|
+
this.zoomResetBtn.setAttribute("type", "button");
|
|
931
|
+
this.zoomResetBtn.setAttribute("aria-label", "Reset zoom");
|
|
932
|
+
this.zoomResetBtn.title = "Reset zoom (0)";
|
|
933
|
+
this.zoomResetBtn.disabled = true;
|
|
934
|
+
this.zoomResetBtn.setAttribute("aria-disabled", "true");
|
|
935
|
+
this.zoomResetBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>`;
|
|
936
|
+
this.zoomResetBtn.addEventListener("click", (e) => {
|
|
937
|
+
e.stopPropagation();
|
|
938
|
+
this.resetZoom(true);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
this.zoomControls.append(this.zoomOutBtn, this.zoomInBtn, this.zoomResetBtn);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
this.closeBtn = document.createElement("button");
|
|
945
|
+
this.closeBtn.className = "hy-gallery-close";
|
|
946
|
+
this.closeBtn.setAttribute("type", "button");
|
|
947
|
+
this.closeBtn.setAttribute("aria-label", "Close");
|
|
948
|
+
this.closeBtn.title = "Close (ESC)";
|
|
949
|
+
this.closeBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
950
|
+
this.closeBtn.addEventListener("click", this.close);
|
|
951
|
+
this.zoomControls.appendChild(this.closeBtn);
|
|
605
952
|
|
|
606
953
|
this.prevBtn = document.createElement("button");
|
|
607
954
|
this.prevBtn.className = "hy-gallery-nav hy-gallery-nav-prev";
|
|
955
|
+
this.prevBtn.setAttribute("type", "button");
|
|
608
956
|
this.prevBtn.setAttribute("aria-label", "Previous image");
|
|
609
|
-
this.prevBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`;
|
|
957
|
+
this.prevBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>`;
|
|
610
958
|
this.prevBtn.addEventListener("click", (e) => {
|
|
611
959
|
e.stopPropagation();
|
|
612
960
|
this.navigate(-1);
|
|
@@ -614,8 +962,9 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
614
962
|
|
|
615
963
|
this.nextBtn = document.createElement("button");
|
|
616
964
|
this.nextBtn.className = "hy-gallery-nav hy-gallery-nav-next";
|
|
965
|
+
this.nextBtn.setAttribute("type", "button");
|
|
617
966
|
this.nextBtn.setAttribute("aria-label", "Next image");
|
|
618
|
-
this.nextBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
|
|
967
|
+
this.nextBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>`;
|
|
619
968
|
this.nextBtn.addEventListener("click", (e) => {
|
|
620
969
|
e.stopPropagation();
|
|
621
970
|
this.navigate(1);
|
|
@@ -628,12 +977,14 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
628
977
|
this.imgEl.className = "hy-gallery-overlay-img";
|
|
629
978
|
this.imgEl.setAttribute("decoding", "sync");
|
|
630
979
|
|
|
631
|
-
this.counterEl = document.createElement("div");
|
|
632
|
-
this.counterEl.className = "hy-gallery-counter";
|
|
633
|
-
this.counterEl.setAttribute("aria-hidden", "true");
|
|
634
|
-
|
|
635
980
|
content.appendChild(this.imgEl);
|
|
636
|
-
this.overlay.append(
|
|
981
|
+
this.overlay.append(
|
|
982
|
+
this.counterEl,
|
|
983
|
+
this.zoomControls,
|
|
984
|
+
this.prevBtn,
|
|
985
|
+
this.nextBtn,
|
|
986
|
+
content,
|
|
987
|
+
);
|
|
637
988
|
|
|
638
989
|
if (this.items.length <= 1) {
|
|
639
990
|
this.prevBtn.style.display = "none";
|
|
@@ -642,12 +993,24 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
642
993
|
}
|
|
643
994
|
|
|
644
995
|
this.overlay.addEventListener("click", this.handleOverlayClick);
|
|
996
|
+
this.overlay.addEventListener("wheel", this.handleWheel, { passive: false });
|
|
645
997
|
this.overlay.addEventListener("touchstart", this.handleTouchStart, {
|
|
646
|
-
passive:
|
|
998
|
+
passive: false,
|
|
999
|
+
});
|
|
1000
|
+
this.overlay.addEventListener("touchmove", this.handleTouchMove, {
|
|
1001
|
+
passive: false,
|
|
647
1002
|
});
|
|
648
1003
|
this.overlay.addEventListener("touchend", this.handleTouchEnd, {
|
|
649
|
-
passive:
|
|
1004
|
+
passive: false,
|
|
1005
|
+
});
|
|
1006
|
+
this.overlay.addEventListener("touchcancel", this.handleTouchEnd, {
|
|
1007
|
+
passive: false,
|
|
650
1008
|
});
|
|
1009
|
+
this.overlay.addEventListener("mousedown", this.handleMouseDown);
|
|
1010
|
+
this.overlay.addEventListener("dblclick", this.handleDblClick);
|
|
1011
|
+
window.addEventListener("mousemove", this.handleMouseMove);
|
|
1012
|
+
window.addEventListener("mouseup", this.handleMouseUp);
|
|
1013
|
+
window.addEventListener("resize", this.handleResize);
|
|
651
1014
|
document.addEventListener("keydown", this.handleKey);
|
|
652
1015
|
};
|
|
653
1016
|
|
|
@@ -655,20 +1018,31 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
655
1018
|
if (!this.overlay) return;
|
|
656
1019
|
|
|
657
1020
|
this.overlay.classList.remove("is-active");
|
|
1021
|
+
this.resetZoom(true);
|
|
658
1022
|
|
|
659
1023
|
setTimeout(() => {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1024
|
+
if (this.overlay) {
|
|
1025
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
|
1026
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
|
1027
|
+
window.removeEventListener("resize", this.handleResize);
|
|
1028
|
+
document.removeEventListener("keydown", this.handleKey);
|
|
1029
|
+
document.removeEventListener("gesturestart", this.preventZoom);
|
|
1030
|
+
|
|
1031
|
+
this.overlay.parentNode?.removeChild(this.overlay);
|
|
1032
|
+
this.overlay = null;
|
|
1033
|
+
this.imgEl = null;
|
|
1034
|
+
this.counterEl = null;
|
|
1035
|
+
this.zoomControls = null;
|
|
1036
|
+
this.zoomInBtn = null;
|
|
1037
|
+
this.zoomOutBtn = null;
|
|
1038
|
+
this.zoomResetBtn = null;
|
|
1039
|
+
this.closeBtn = null;
|
|
1040
|
+
}
|
|
664
1041
|
|
|
665
1042
|
this.unlockBackgroundScroll();
|
|
666
1043
|
const main = document.querySelector("main");
|
|
667
1044
|
if (main) main.removeAttribute("inert");
|
|
668
1045
|
|
|
669
|
-
document.removeEventListener("keydown", this.handleKey);
|
|
670
|
-
document.removeEventListener("gesturestart", this.preventZoom);
|
|
671
|
-
|
|
672
1046
|
if (this.currentIndex >= 0 && this.currentIndex < this.items.length) {
|
|
673
1047
|
this.items[this.currentIndex].focus();
|
|
674
1048
|
}
|
|
@@ -684,6 +1058,7 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
684
1058
|
this.previousBodyTop = document.body.style.top;
|
|
685
1059
|
this.previousBodyWidth = document.body.style.width;
|
|
686
1060
|
this.previousHtmlOverflow = document.documentElement.style.overflow;
|
|
1061
|
+
this.previousHtmlScrollBehavior = document.documentElement.style.scrollBehavior;
|
|
687
1062
|
|
|
688
1063
|
document.documentElement.style.overflow = "hidden";
|
|
689
1064
|
document.body.style.overflow = "hidden";
|
|
@@ -700,7 +1075,12 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
700
1075
|
document.body.style.position = this.previousBodyPosition;
|
|
701
1076
|
document.body.style.top = this.previousBodyTop;
|
|
702
1077
|
document.body.style.width = this.previousBodyWidth;
|
|
703
|
-
|
|
1078
|
+
document.documentElement.style.scrollBehavior = "auto";
|
|
1079
|
+
window.scrollTo({ top: this.lockedScrollY, left: 0, behavior: "auto" });
|
|
1080
|
+
requestAnimationFrame(() => {
|
|
1081
|
+
document.documentElement.style.scrollBehavior =
|
|
1082
|
+
this.previousHtmlScrollBehavior;
|
|
1083
|
+
});
|
|
704
1084
|
this.lockedScrollY = -1;
|
|
705
1085
|
};
|
|
706
1086
|
|
|
@@ -712,6 +1092,8 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
712
1092
|
const content = this.overlay?.querySelector(".hy-gallery-overlay-content");
|
|
713
1093
|
if (!content || !this.imgEl) return;
|
|
714
1094
|
|
|
1095
|
+
this.resetZoom(false);
|
|
1096
|
+
|
|
715
1097
|
const oldImg = this.imgEl;
|
|
716
1098
|
this.imgEl = document.createElement("img");
|
|
717
1099
|
this.imgEl.className = "hy-gallery-overlay-img hy-gallery-slide-anim";
|
|
@@ -725,8 +1107,17 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
725
1107
|
|
|
726
1108
|
content.replaceChild(this.imgEl, oldImg);
|
|
727
1109
|
|
|
1110
|
+
this.imgEl.addEventListener(
|
|
1111
|
+
"animationend",
|
|
1112
|
+
() => {
|
|
1113
|
+
this.imgEl?.classList.remove("hy-gallery-slide-anim");
|
|
1114
|
+
},
|
|
1115
|
+
{ once: true },
|
|
1116
|
+
);
|
|
1117
|
+
|
|
728
1118
|
this.updateCounter();
|
|
729
1119
|
this.updateNavButtons();
|
|
1120
|
+
this.updateZoomUI();
|
|
730
1121
|
this.preload(next + dir);
|
|
731
1122
|
};
|
|
732
1123
|
|
|
@@ -751,6 +1142,10 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
751
1142
|
};
|
|
752
1143
|
|
|
753
1144
|
handleOverlayClick = (e: MouseEvent) => {
|
|
1145
|
+
if (this.hasMouseDragged) {
|
|
1146
|
+
this.hasMouseDragged = false;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
754
1149
|
const target = e.target as HTMLElement;
|
|
755
1150
|
if (
|
|
756
1151
|
target.classList.contains("hy-gallery-overlay") ||
|
|
@@ -761,22 +1156,270 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
761
1156
|
};
|
|
762
1157
|
|
|
763
1158
|
handleKey = (e: KeyboardEvent) => {
|
|
764
|
-
if (e.key === "Escape")
|
|
765
|
-
|
|
766
|
-
|
|
1159
|
+
if (e.key === "Escape") {
|
|
1160
|
+
if (this.scale > 1.05) {
|
|
1161
|
+
this.resetZoom(true);
|
|
1162
|
+
} else {
|
|
1163
|
+
this.close();
|
|
1164
|
+
}
|
|
1165
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
1166
|
+
this.zoomBy(1.3);
|
|
1167
|
+
} else if (e.key === "-" || e.key === "_") {
|
|
1168
|
+
this.zoomBy(1 / 1.3);
|
|
1169
|
+
} else if (e.key === "0") {
|
|
1170
|
+
this.resetZoom(true);
|
|
1171
|
+
} else if (e.key === "ArrowLeft") {
|
|
1172
|
+
if (this.scale > 1.05) {
|
|
1173
|
+
this.panBy(60, 0);
|
|
1174
|
+
} else {
|
|
1175
|
+
this.navigate(-1);
|
|
1176
|
+
}
|
|
1177
|
+
} else if (e.key === "ArrowRight") {
|
|
1178
|
+
if (this.scale > 1.05) {
|
|
1179
|
+
this.panBy(-60, 0);
|
|
1180
|
+
} else {
|
|
1181
|
+
this.navigate(1);
|
|
1182
|
+
}
|
|
1183
|
+
} else if (e.key === "ArrowUp" && this.scale > 1.05) {
|
|
1184
|
+
this.panBy(0, 60);
|
|
1185
|
+
} else if (e.key === "ArrowDown" && this.scale > 1.05) {
|
|
1186
|
+
this.panBy(0, -60);
|
|
1187
|
+
}
|
|
767
1188
|
};
|
|
768
1189
|
|
|
769
1190
|
handleTouchStart = (e: TouchEvent) => {
|
|
770
|
-
|
|
771
|
-
|
|
1191
|
+
const target = e.target as HTMLElement;
|
|
1192
|
+
if (target.closest(".hy-gallery-btn, .hy-gallery-close, .hy-gallery-nav")) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (e.touches.length === 1) {
|
|
1197
|
+
this.touchStartX = e.touches[0].clientX;
|
|
1198
|
+
this.touchStartY = e.touches[0].clientY;
|
|
1199
|
+
this.touchLastX = e.touches[0].clientX;
|
|
1200
|
+
this.touchLastY = e.touches[0].clientY;
|
|
1201
|
+
this.isDragging = false;
|
|
1202
|
+
this.isPinching = false;
|
|
1203
|
+
} else if (e.touches.length === 2 && this.zoomEnabled) {
|
|
1204
|
+
this.isPinching = true;
|
|
1205
|
+
this.isDragging = false;
|
|
1206
|
+
const t1 = e.touches[0];
|
|
1207
|
+
const t2 = e.touches[1];
|
|
1208
|
+
this.pinchStartDist = Math.hypot(
|
|
1209
|
+
t2.clientX - t1.clientX,
|
|
1210
|
+
t2.clientY - t1.clientY,
|
|
1211
|
+
);
|
|
1212
|
+
this.pinchStartScale = this.scale;
|
|
1213
|
+
this.pinchStartCenter = {
|
|
1214
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
1215
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
1216
|
+
};
|
|
1217
|
+
this.pinchStartTranslate = { x: this.translateX, y: this.translateY };
|
|
1218
|
+
this.applyTransform(false);
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
handleTouchMove = (e: TouchEvent) => {
|
|
1223
|
+
if (this.isPinching && e.touches.length >= 2) {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
const t1 = e.touches[0];
|
|
1226
|
+
const t2 = e.touches[1];
|
|
1227
|
+
const currDist = Math.hypot(
|
|
1228
|
+
t2.clientX - t1.clientX,
|
|
1229
|
+
t2.clientY - t1.clientY,
|
|
1230
|
+
);
|
|
1231
|
+
const ratio = currDist / (this.pinchStartDist || 1);
|
|
1232
|
+
const rawScale = this.pinchStartScale * ratio;
|
|
1233
|
+
const clampedScale = Math.min(5.5, Math.max(0.75, rawScale));
|
|
1234
|
+
|
|
1235
|
+
const currCenter = {
|
|
1236
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
1237
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
1238
|
+
};
|
|
1239
|
+
const center = this.getContentCenter();
|
|
1240
|
+
|
|
1241
|
+
const T_x =
|
|
1242
|
+
currCenter.x -
|
|
1243
|
+
center.x -
|
|
1244
|
+
(clampedScale / this.pinchStartScale) *
|
|
1245
|
+
(this.pinchStartCenter.x - center.x - this.pinchStartTranslate.x);
|
|
1246
|
+
const T_y =
|
|
1247
|
+
currCenter.y -
|
|
1248
|
+
center.y -
|
|
1249
|
+
(clampedScale / this.pinchStartScale) *
|
|
1250
|
+
(this.pinchStartCenter.y - center.y - this.pinchStartTranslate.y);
|
|
1251
|
+
|
|
1252
|
+
this.scale = clampedScale;
|
|
1253
|
+
this.translateX = T_x;
|
|
1254
|
+
this.translateY = T_y;
|
|
1255
|
+
this.applyTransform(false);
|
|
1256
|
+
this.updateZoomUI();
|
|
1257
|
+
} else if (!this.isPinching && e.touches.length === 1) {
|
|
1258
|
+
const touch = e.touches[0];
|
|
1259
|
+
if (this.scale > 1.01) {
|
|
1260
|
+
e.preventDefault();
|
|
1261
|
+
const dx = touch.clientX - this.touchLastX;
|
|
1262
|
+
const dy = touch.clientY - this.touchLastY;
|
|
1263
|
+
this.touchLastX = touch.clientX;
|
|
1264
|
+
this.touchLastY = touch.clientY;
|
|
1265
|
+
this.translateX += dx;
|
|
1266
|
+
this.translateY += dy;
|
|
1267
|
+
this.isDragging = true;
|
|
1268
|
+
this.applyTransform(false);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
772
1271
|
};
|
|
773
1272
|
|
|
774
1273
|
handleTouchEnd = (e: TouchEvent) => {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1274
|
+
if (this.isPinching) {
|
|
1275
|
+
if (e.touches.length < 2) {
|
|
1276
|
+
this.isPinching = false;
|
|
1277
|
+
if (e.touches.length === 1) {
|
|
1278
|
+
this.touchLastX = e.touches[0].clientX;
|
|
1279
|
+
this.touchLastY = e.touches[0].clientY;
|
|
1280
|
+
this.touchStartX = e.touches[0].clientX;
|
|
1281
|
+
this.touchStartY = e.touches[0].clientY;
|
|
1282
|
+
}
|
|
1283
|
+
if (this.scale < 1.0) {
|
|
1284
|
+
this.resetZoom(true);
|
|
1285
|
+
} else if (this.scale > 5.0) {
|
|
1286
|
+
this.scale = 5.0;
|
|
1287
|
+
this.clampToBounds(true);
|
|
1288
|
+
} else {
|
|
1289
|
+
this.clampToBounds(true);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (e.changedTouches.length === 1) {
|
|
1296
|
+
const touch = e.changedTouches[0];
|
|
1297
|
+
const tapDist = Math.hypot(
|
|
1298
|
+
touch.clientX - this.touchStartX,
|
|
1299
|
+
touch.clientY - this.touchStartY,
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
if (tapDist < 15 && !this.isDragging) {
|
|
1303
|
+
const now = Date.now();
|
|
1304
|
+
if (now - this.lastTapTime < 300) {
|
|
1305
|
+
this.handleDoubleTap(touch.clientX, touch.clientY);
|
|
1306
|
+
this.lastTapTime = 0;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
this.lastTapTime = now;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (this.scale > 1.01) {
|
|
1313
|
+
this.clampToBounds(true);
|
|
1314
|
+
} else {
|
|
1315
|
+
const diffX = touch.clientX - this.touchStartX;
|
|
1316
|
+
const diffY = touch.clientY - this.touchStartY;
|
|
1317
|
+
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
1318
|
+
if (diffX > 0) this.navigate(-1);
|
|
1319
|
+
else this.navigate(1);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
handleWheel = (e: WheelEvent) => {
|
|
1326
|
+
if (!this.overlay || !this.imgEl || !this.zoomEnabled) return;
|
|
1327
|
+
e.preventDefault();
|
|
1328
|
+
const isPinch = e.ctrlKey;
|
|
1329
|
+
const factor = isPinch
|
|
1330
|
+
? Math.exp(-e.deltaY * 0.015)
|
|
1331
|
+
: e.deltaY < 0
|
|
1332
|
+
? 1.2
|
|
1333
|
+
: 0.833;
|
|
1334
|
+
|
|
1335
|
+
const targetScale = Math.min(5.0, Math.max(1.0, this.scale * factor));
|
|
1336
|
+
if (Math.abs(targetScale - this.scale) < 0.001) return;
|
|
1337
|
+
|
|
1338
|
+
if (targetScale <= 1.01) {
|
|
1339
|
+
this.resetZoom(true);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const center = this.getContentCenter();
|
|
1344
|
+
const T_x =
|
|
1345
|
+
e.clientX -
|
|
1346
|
+
center.x -
|
|
1347
|
+
(targetScale / this.scale) * (e.clientX - center.x - this.translateX);
|
|
1348
|
+
const T_y =
|
|
1349
|
+
e.clientY -
|
|
1350
|
+
center.y -
|
|
1351
|
+
(targetScale / this.scale) * (e.clientY - center.y - this.translateY);
|
|
1352
|
+
|
|
1353
|
+
this.scale = targetScale;
|
|
1354
|
+
this.translateX = T_x;
|
|
1355
|
+
this.translateY = T_y;
|
|
1356
|
+
this.clampToBounds(false);
|
|
1357
|
+
this.updateZoomUI();
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
handleMouseDown = (e: MouseEvent) => {
|
|
1361
|
+
const target = e.target as HTMLElement;
|
|
1362
|
+
if (target.closest(".hy-gallery-btn, .hy-gallery-close, .hy-gallery-nav")) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (e.button !== 0) return;
|
|
1366
|
+
if (this.scale <= 1.01) return;
|
|
1367
|
+
|
|
1368
|
+
this.isMouseDown = true;
|
|
1369
|
+
this.mouseStartX = e.clientX;
|
|
1370
|
+
this.mouseStartY = e.clientY;
|
|
1371
|
+
this.mouseLastX = e.clientX;
|
|
1372
|
+
this.mouseLastY = e.clientY;
|
|
1373
|
+
this.hasMouseDragged = false;
|
|
1374
|
+
if (this.imgEl) {
|
|
1375
|
+
this.imgEl.classList.add("is-dragging");
|
|
1376
|
+
this.imgEl.style.cursor = "grabbing";
|
|
1377
|
+
this.imgEl.style.transition = "none";
|
|
1378
|
+
}
|
|
1379
|
+
e.preventDefault();
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
handleMouseMove = (e: MouseEvent) => {
|
|
1383
|
+
if (!this.isMouseDown) return;
|
|
1384
|
+
const dx = e.clientX - this.mouseLastX;
|
|
1385
|
+
const dy = e.clientY - this.mouseLastY;
|
|
1386
|
+
if (
|
|
1387
|
+
Math.hypot(e.clientX - this.mouseStartX, e.clientY - this.mouseStartY) >
|
|
1388
|
+
5
|
|
1389
|
+
) {
|
|
1390
|
+
this.hasMouseDragged = true;
|
|
1391
|
+
}
|
|
1392
|
+
this.mouseLastX = e.clientX;
|
|
1393
|
+
this.mouseLastY = e.clientY;
|
|
1394
|
+
this.translateX += dx;
|
|
1395
|
+
this.translateY += dy;
|
|
1396
|
+
this.applyTransform(false);
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
handleMouseUp = () => {
|
|
1400
|
+
if (!this.isMouseDown) return;
|
|
1401
|
+
this.isMouseDown = false;
|
|
1402
|
+
if (this.imgEl) {
|
|
1403
|
+
this.imgEl.classList.remove("is-dragging");
|
|
1404
|
+
this.imgEl.style.cursor = this.scale > 1.01 ? "grab" : "zoom-in";
|
|
1405
|
+
}
|
|
1406
|
+
this.clampToBounds(true);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
handleDblClick = (e: MouseEvent) => {
|
|
1410
|
+
const target = e.target as HTMLElement;
|
|
1411
|
+
if (target.closest(".hy-gallery-btn, .hy-gallery-close, .hy-gallery-nav")) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (!this.zoomEnabled) return;
|
|
1415
|
+
e.preventDefault();
|
|
1416
|
+
e.stopPropagation();
|
|
1417
|
+
this.handleDoubleTap(e.clientX, e.clientY);
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
handleResize = () => {
|
|
1421
|
+
if (this.scale > 1.01) {
|
|
1422
|
+
this.clampToBounds(false);
|
|
780
1423
|
}
|
|
781
1424
|
};
|
|
782
1425
|
|