@hoangyell/gallery 1.0.8 → 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 +685 -50
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,9 +566,43 @@ 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;
|
|
@@ -510,6 +613,8 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
510
613
|
|
|
511
614
|
constructor(el: HTMLElement) {
|
|
512
615
|
this.gallery = el;
|
|
616
|
+
this.zoomEnabled = el.dataset.zoom !== "false";
|
|
617
|
+
this.zoomControlsEnabled = el.dataset.zoomControls === "true";
|
|
513
618
|
this.items = Array.from(
|
|
514
619
|
el.querySelectorAll<HTMLButtonElement>(".hy-gallery-item"),
|
|
515
620
|
);
|
|
@@ -518,9 +623,37 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
518
623
|
this.counterEl = null;
|
|
519
624
|
this.prevBtn = null;
|
|
520
625
|
this.nextBtn = null;
|
|
626
|
+
this.zoomControls = null;
|
|
627
|
+
this.zoomInBtn = null;
|
|
628
|
+
this.zoomOutBtn = null;
|
|
629
|
+
this.zoomResetBtn = null;
|
|
630
|
+
this.closeBtn = null;
|
|
521
631
|
this.currentIndex = -1;
|
|
632
|
+
|
|
633
|
+
this.scale = 1;
|
|
634
|
+
this.translateX = 0;
|
|
635
|
+
this.translateY = 0;
|
|
636
|
+
|
|
522
637
|
this.touchStartX = 0;
|
|
523
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
|
+
|
|
524
657
|
this.lockedScrollY = -1;
|
|
525
658
|
this.previousBodyOverflow = "";
|
|
526
659
|
this.previousBodyPosition = "";
|
|
@@ -544,15 +677,173 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
544
677
|
});
|
|
545
678
|
};
|
|
546
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
|
+
|
|
547
829
|
forceClose = () => {
|
|
548
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
|
+
|
|
549
837
|
this.overlay.remove();
|
|
550
838
|
this.overlay = null;
|
|
551
839
|
this.imgEl = null;
|
|
552
840
|
this.counterEl = null;
|
|
841
|
+
this.zoomControls = null;
|
|
842
|
+
this.zoomInBtn = null;
|
|
843
|
+
this.zoomOutBtn = null;
|
|
844
|
+
this.zoomResetBtn = null;
|
|
845
|
+
this.closeBtn = null;
|
|
553
846
|
|
|
554
|
-
document.removeEventListener("keydown", this.handleKey);
|
|
555
|
-
document.removeEventListener("gesturestart", this.preventZoom);
|
|
556
847
|
const main = document.querySelector("main");
|
|
557
848
|
if (main) main.removeAttribute("inert");
|
|
558
849
|
}
|
|
@@ -570,6 +861,7 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
570
861
|
|
|
571
862
|
this.imgEl.src = src;
|
|
572
863
|
this.imgEl.alt = alt;
|
|
864
|
+
this.resetZoom(false);
|
|
573
865
|
this.updateCounter();
|
|
574
866
|
this.updateNavButtons();
|
|
575
867
|
this.preload(index + 1);
|
|
@@ -599,16 +891,70 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
599
891
|
this.overlay.setAttribute("aria-modal", "true");
|
|
600
892
|
this.overlay.setAttribute("aria-label", "Image viewer");
|
|
601
893
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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);
|
|
607
952
|
|
|
608
953
|
this.prevBtn = document.createElement("button");
|
|
609
954
|
this.prevBtn.className = "hy-gallery-nav hy-gallery-nav-prev";
|
|
955
|
+
this.prevBtn.setAttribute("type", "button");
|
|
610
956
|
this.prevBtn.setAttribute("aria-label", "Previous image");
|
|
611
|
-
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>`;
|
|
612
958
|
this.prevBtn.addEventListener("click", (e) => {
|
|
613
959
|
e.stopPropagation();
|
|
614
960
|
this.navigate(-1);
|
|
@@ -616,8 +962,9 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
616
962
|
|
|
617
963
|
this.nextBtn = document.createElement("button");
|
|
618
964
|
this.nextBtn.className = "hy-gallery-nav hy-gallery-nav-next";
|
|
965
|
+
this.nextBtn.setAttribute("type", "button");
|
|
619
966
|
this.nextBtn.setAttribute("aria-label", "Next image");
|
|
620
|
-
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>`;
|
|
621
968
|
this.nextBtn.addEventListener("click", (e) => {
|
|
622
969
|
e.stopPropagation();
|
|
623
970
|
this.navigate(1);
|
|
@@ -630,12 +977,14 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
630
977
|
this.imgEl.className = "hy-gallery-overlay-img";
|
|
631
978
|
this.imgEl.setAttribute("decoding", "sync");
|
|
632
979
|
|
|
633
|
-
this.counterEl = document.createElement("div");
|
|
634
|
-
this.counterEl.className = "hy-gallery-counter";
|
|
635
|
-
this.counterEl.setAttribute("aria-hidden", "true");
|
|
636
|
-
|
|
637
980
|
content.appendChild(this.imgEl);
|
|
638
|
-
this.overlay.append(
|
|
981
|
+
this.overlay.append(
|
|
982
|
+
this.counterEl,
|
|
983
|
+
this.zoomControls,
|
|
984
|
+
this.prevBtn,
|
|
985
|
+
this.nextBtn,
|
|
986
|
+
content,
|
|
987
|
+
);
|
|
639
988
|
|
|
640
989
|
if (this.items.length <= 1) {
|
|
641
990
|
this.prevBtn.style.display = "none";
|
|
@@ -644,12 +993,24 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
644
993
|
}
|
|
645
994
|
|
|
646
995
|
this.overlay.addEventListener("click", this.handleOverlayClick);
|
|
996
|
+
this.overlay.addEventListener("wheel", this.handleWheel, { passive: false });
|
|
647
997
|
this.overlay.addEventListener("touchstart", this.handleTouchStart, {
|
|
648
|
-
passive:
|
|
998
|
+
passive: false,
|
|
999
|
+
});
|
|
1000
|
+
this.overlay.addEventListener("touchmove", this.handleTouchMove, {
|
|
1001
|
+
passive: false,
|
|
649
1002
|
});
|
|
650
1003
|
this.overlay.addEventListener("touchend", this.handleTouchEnd, {
|
|
651
|
-
passive:
|
|
1004
|
+
passive: false,
|
|
1005
|
+
});
|
|
1006
|
+
this.overlay.addEventListener("touchcancel", this.handleTouchEnd, {
|
|
1007
|
+
passive: false,
|
|
652
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);
|
|
653
1014
|
document.addEventListener("keydown", this.handleKey);
|
|
654
1015
|
};
|
|
655
1016
|
|
|
@@ -657,20 +1018,31 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
657
1018
|
if (!this.overlay) return;
|
|
658
1019
|
|
|
659
1020
|
this.overlay.classList.remove("is-active");
|
|
1021
|
+
this.resetZoom(true);
|
|
660
1022
|
|
|
661
1023
|
setTimeout(() => {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
+
}
|
|
666
1041
|
|
|
667
1042
|
this.unlockBackgroundScroll();
|
|
668
1043
|
const main = document.querySelector("main");
|
|
669
1044
|
if (main) main.removeAttribute("inert");
|
|
670
1045
|
|
|
671
|
-
document.removeEventListener("keydown", this.handleKey);
|
|
672
|
-
document.removeEventListener("gesturestart", this.preventZoom);
|
|
673
|
-
|
|
674
1046
|
if (this.currentIndex >= 0 && this.currentIndex < this.items.length) {
|
|
675
1047
|
this.items[this.currentIndex].focus();
|
|
676
1048
|
}
|
|
@@ -720,6 +1092,8 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
720
1092
|
const content = this.overlay?.querySelector(".hy-gallery-overlay-content");
|
|
721
1093
|
if (!content || !this.imgEl) return;
|
|
722
1094
|
|
|
1095
|
+
this.resetZoom(false);
|
|
1096
|
+
|
|
723
1097
|
const oldImg = this.imgEl;
|
|
724
1098
|
this.imgEl = document.createElement("img");
|
|
725
1099
|
this.imgEl.className = "hy-gallery-overlay-img hy-gallery-slide-anim";
|
|
@@ -733,8 +1107,17 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
733
1107
|
|
|
734
1108
|
content.replaceChild(this.imgEl, oldImg);
|
|
735
1109
|
|
|
1110
|
+
this.imgEl.addEventListener(
|
|
1111
|
+
"animationend",
|
|
1112
|
+
() => {
|
|
1113
|
+
this.imgEl?.classList.remove("hy-gallery-slide-anim");
|
|
1114
|
+
},
|
|
1115
|
+
{ once: true },
|
|
1116
|
+
);
|
|
1117
|
+
|
|
736
1118
|
this.updateCounter();
|
|
737
1119
|
this.updateNavButtons();
|
|
1120
|
+
this.updateZoomUI();
|
|
738
1121
|
this.preload(next + dir);
|
|
739
1122
|
};
|
|
740
1123
|
|
|
@@ -759,6 +1142,10 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
759
1142
|
};
|
|
760
1143
|
|
|
761
1144
|
handleOverlayClick = (e: MouseEvent) => {
|
|
1145
|
+
if (this.hasMouseDragged) {
|
|
1146
|
+
this.hasMouseDragged = false;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
762
1149
|
const target = e.target as HTMLElement;
|
|
763
1150
|
if (
|
|
764
1151
|
target.classList.contains("hy-gallery-overlay") ||
|
|
@@ -769,22 +1156,270 @@ if (resolvedRows <= 1 || galleryItems.length <= 1) {
|
|
|
769
1156
|
};
|
|
770
1157
|
|
|
771
1158
|
handleKey = (e: KeyboardEvent) => {
|
|
772
|
-
if (e.key === "Escape")
|
|
773
|
-
|
|
774
|
-
|
|
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
|
+
}
|
|
775
1188
|
};
|
|
776
1189
|
|
|
777
1190
|
handleTouchStart = (e: TouchEvent) => {
|
|
778
|
-
|
|
779
|
-
|
|
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
|
+
}
|
|
780
1271
|
};
|
|
781
1272
|
|
|
782
1273
|
handleTouchEnd = (e: TouchEvent) => {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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);
|
|
788
1423
|
}
|
|
789
1424
|
};
|
|
790
1425
|
|