@michaelyagi/shoji 0.1.0-alpha.10 → 0.1.0-alpha.11

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.
@@ -50,6 +50,9 @@ export declare class Gallery {
50
50
  private readonly onOuterClick;
51
51
  /** DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle clock. `autoHideDelay: 0` = "never show controls" — a no-op here. */
52
52
  private readonly onActivity;
53
+ /** x */
54
+ private controlsHiddenAtGestureStart;
55
+ private readonly captureGestureStartState;
53
56
  private readonly onKeydown;
54
57
  constructor(target: HTMLElement | string, options?: GalleryOptions);
55
58
  /** Everything the constructor does after `this.element` is resolved — shared with `reinit()` (§2.7). */
@@ -102,7 +105,8 @@ export declare class Gallery {
102
105
  */
103
106
  getOriginElement(index: number): HTMLElement | null;
104
107
  private scheduleAutoHide;
105
- private hideControls;
108
+ /** Forces the same fade §2.8's idle timer would eventually trigger — public so a plugin can hide controls on its own trigger. Same `isControlActive()` guard as the timer. */
109
+ hideControls(): void;
106
110
  private showControls;
107
111
  /**
108
112
  * DESIGN.md §2.1 — `caption` is `string | HTMLElement | DangerousHtmlCaption`:
@@ -175,6 +179,16 @@ export declare class Gallery {
175
179
  close(): void;
176
180
  /** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
177
181
  private resolveAspectRatio;
182
+ /**
183
+ * `item.width`/`height` only — deliberately never a thumbnail's own
184
+ * `naturalWidth`/`naturalHeight` the way `resolveAspectRatio` above will:
185
+ * that's a fine stand-in for *shape*, but using it as the real photo's
186
+ * true pixel size would under-cap a genuinely large photo down to
187
+ * thumbnail resolution. `undefined` here just means "genuinely unknown,"
188
+ * not "assume small" — `zoomTransition.ts`'s `containedBox` already
189
+ * treats it that way (no cap applied at all).
190
+ */
191
+ private resolveNaturalSize;
178
192
  /**
179
193
  * Idempotent on purpose: a pending zoom-out's `transitionend`/fallback
180
194
  * timeout can still fire after `destroy()` has already force-finished the
@@ -66,7 +66,33 @@ export declare class SlideManager {
66
66
  private moveIn;
67
67
  /** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */
68
68
  private ensureImageDecoding;
69
- /** DESIGN.md §2.3 — swaps the spinner for the placeholder once *it* decodes, not immediately: `item.thumb` is often just `item.src` again, so an undecoded placeholder can leave as long a blank gap as the spinner it replaces. */
69
+ /**
70
+ * DESIGN.md §2.3 — swaps the spinner for the placeholder once *it*
71
+ * decodes, not immediately: `item.thumb` is often just `item.src` again,
72
+ * so an undecoded placeholder can leave as long a blank gap as the
73
+ * spinner it replaces.
74
+ *
75
+ * A real bug: `.shoji-slide-open-placeholder`'s CSS unconditionally
76
+ * force-fills the frame (deliberate default: a real photo is usually
77
+ * bigger than the dialog). Wrong for a genuinely small photo with known
78
+ * `item.width`/`item.height` — same "grows too big, snaps down once real"
79
+ * symptom the zoom-in transition itself had (§2.3b), but that fix only
80
+ * governs the animated *transform*, not this placeholder's own CSS box
81
+ * once it settles. When `naturalSize` is known, sizes the placeholder
82
+ * explicitly instead, reusing `containedBox` (`zoomTransition.ts`) —
83
+ * unknown-size items keep the original force-fill guess.
84
+ *
85
+ * A second real bug in that same fix: measured `slot.media`'s own rect as
86
+ * "the container" — but `slot.media` is the exact element the zoom-in
87
+ * transition (§2.3b) animates a `scale()` transform on. `getBoundingClientRect()`
88
+ * reflects whatever transform is *currently* applied, and the placeholder's
89
+ * own decode typically resolves before that ~300ms animation settles — so
90
+ * this measured a still-small, mid-animation rect (often the origin
91
+ * thumbnail's own tiny size) instead of the real dialog size. `slot.root`
92
+ * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for
93
+ * pool-offset positioning, never scaled — a stable read regardless of
94
+ * what its child is mid-animation through.
95
+ */
70
96
  private revealOpenPlaceholder;
71
97
  /** Native `<video controls>` — real playback, not just a poster. Not autoplayed/muted: a deliberate click-to-play, not a background loop. */
72
98
  private renderVideo;
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { w as waitForTransitionEnd, z as zoomIn, a as zoomOut } from "../zoomTransition-bbKHpVpA.js";
4
+ import { w as waitForTransitionEnd, c as containedBox, z as zoomIn, a as zoomOut } from "../zoomTransition-2_HG7tyJ.js";
5
5
  let lockCount = 0;
6
6
  let savedOverflow = "";
7
7
  let savedHtmlOverflow = "";
@@ -785,7 +785,8 @@ class SlideManager {
785
785
  releaseVideo(slot.media);
786
786
  slot.media.replaceChildren(createSpinner());
787
787
  if (index === centerIndex && openPlaceholderSrc) {
788
- this.revealOpenPlaceholder(openPlaceholderSrc, slot, index);
788
+ const naturalSize = item.width && item.height ? { width: item.width, height: item.height } : void 0;
789
+ this.revealOpenPlaceholder(openPlaceholderSrc, slot, index, naturalSize);
789
790
  }
790
791
  if (((_a = item.video) == null ? void 0 : _a.provider) === "html5") {
791
792
  this.renderVideo(item, slot, index, onLoad);
@@ -854,14 +855,50 @@ class SlideManager {
854
855
  img.addEventListener("error", reveal, { once: true });
855
856
  }
856
857
  }
857
- /** DESIGN.md §2.3 — swaps the spinner for the placeholder once *it* decodes, not immediately: `item.thumb` is often just `item.src` again, so an undecoded placeholder can leave as long a blank gap as the spinner it replaces. */
858
- revealOpenPlaceholder(src, slot, index) {
858
+ /**
859
+ * DESIGN.md §2.3 — swaps the spinner for the placeholder once *it*
860
+ * decodes, not immediately: `item.thumb` is often just `item.src` again,
861
+ * so an undecoded placeholder can leave as long a blank gap as the
862
+ * spinner it replaces.
863
+ *
864
+ * A real bug: `.shoji-slide-open-placeholder`'s CSS unconditionally
865
+ * force-fills the frame (deliberate default: a real photo is usually
866
+ * bigger than the dialog). Wrong for a genuinely small photo with known
867
+ * `item.width`/`item.height` — same "grows too big, snaps down once real"
868
+ * symptom the zoom-in transition itself had (§2.3b), but that fix only
869
+ * governs the animated *transform*, not this placeholder's own CSS box
870
+ * once it settles. When `naturalSize` is known, sizes the placeholder
871
+ * explicitly instead, reusing `containedBox` (`zoomTransition.ts`) —
872
+ * unknown-size items keep the original force-fill guess.
873
+ *
874
+ * A second real bug in that same fix: measured `slot.media`'s own rect as
875
+ * "the container" — but `slot.media` is the exact element the zoom-in
876
+ * transition (§2.3b) animates a `scale()` transform on. `getBoundingClientRect()`
877
+ * reflects whatever transform is *currently* applied, and the placeholder's
878
+ * own decode typically resolves before that ~300ms animation settles — so
879
+ * this measured a still-small, mid-animation rect (often the origin
880
+ * thumbnail's own tiny size) instead of the real dialog size. `slot.root`
881
+ * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for
882
+ * pool-offset positioning, never scaled — a stable read regardless of
883
+ * what its child is mid-animation through.
884
+ */
885
+ revealOpenPlaceholder(src, slot, index, naturalSize) {
859
886
  const img = createOpenPlaceholder(src);
860
887
  const reveal = () => {
861
888
  var _a;
862
889
  if (slot.assignedIndex !== index || slot.ready) return;
863
890
  (_a = slot.media.querySelector(".shoji-slide-spinner")) == null ? void 0 : _a.remove();
864
891
  slot.media.appendChild(img);
892
+ if (naturalSize) {
893
+ const containerRect = slot.root.getBoundingClientRect();
894
+ const box = containedBox(
895
+ { left: 0, top: 0, width: containerRect.width, height: containerRect.height },
896
+ naturalSize.width / naturalSize.height,
897
+ naturalSize
898
+ );
899
+ img.style.width = `${box.width}px`;
900
+ img.style.height = `${box.height}px`;
901
+ }
865
902
  };
866
903
  if (typeof img.decode === "function") {
867
904
  img.decode().then(reveal, reveal);
@@ -1375,6 +1412,11 @@ class Gallery {
1375
1412
  this.showControls();
1376
1413
  this.scheduleAutoHide();
1377
1414
  });
1415
+ /** x */
1416
+ __publicField(this, "controlsHiddenAtGestureStart", false);
1417
+ __publicField(this, "captureGestureStartState", () => {
1418
+ this.controlsHiddenAtGestureStart = this.autoHidden;
1419
+ });
1378
1420
  __publicField(this, "onKeydown", (event) => {
1379
1421
  if (document.activeElement instanceof HTMLVideoElement && event.key !== "Escape") return;
1380
1422
  this.onActivity();
@@ -1506,6 +1548,7 @@ class Gallery {
1506
1548
  });
1507
1549
  dom.outer.addEventListener("click", this.onOuterClick);
1508
1550
  dom.outer.addEventListener("pointermove", this.onActivity, { passive: true });
1551
+ dom.outer.addEventListener("pointerdown", this.captureGestureStartState, { passive: true });
1509
1552
  dom.outer.addEventListener("pointerdown", this.onActivity, { passive: true });
1510
1553
  dom.outer.addEventListener("touchstart", this.onActivity, { passive: true });
1511
1554
  dom.outer.addEventListener("wheel", this.onActivity, { passive: true });
@@ -1545,7 +1588,11 @@ class Gallery {
1545
1588
  }
1546
1589
  },
1547
1590
  {
1548
- onTap: (x, y) => this.bus.emit("tap", { x, y }),
1591
+ onTap: (x, y) => this.bus.emit("tap", {
1592
+ x,
1593
+ y,
1594
+ controlsWereHidden: this.controlsHiddenAtGestureStart
1595
+ }),
1549
1596
  onDoubleTap: (x, y) => this.bus.emit("doubleTap", { x, y }),
1550
1597
  onPinchStart: (centerX, centerY) => this.bus.emit("pinchStart", { centerX, centerY }),
1551
1598
  onPinchMove: (scale, centerX, centerY) => this.bus.emit("pinchMove", { scale, centerX, centerY }),
@@ -1720,6 +1767,7 @@ class Gallery {
1720
1767
  }
1721
1768
  this.autoHideTimer = setTimeout(() => this.hideControls(), this.autoHideDelay);
1722
1769
  }
1770
+ /** Forces the same fade §2.8's idle timer would eventually trigger — public so a plugin can hide controls on its own trigger. Same `isControlActive()` guard as the timer. */
1723
1771
  hideControls() {
1724
1772
  if (!this.dom || this.autoHidden || this.isControlActive()) return;
1725
1773
  this.autoHidden = true;
@@ -1832,15 +1880,23 @@ class Gallery {
1832
1880
  this.captionVisibleOnVideo = !!this.options.showVideoCaption;
1833
1881
  lockBodyScroll();
1834
1882
  const origin = this.getOriginElement(index);
1835
- this.renderCurrentSlide(this.resolveOpenPlaceholderSrc(this.itemList[index], origin));
1883
+ const naturalSize = this.resolveNaturalSize(index);
1884
+ this.renderCurrentSlide(
1885
+ naturalSize ? this.resolveOpenPlaceholderSrc(this.itemList[index], origin) : void 0
1886
+ );
1836
1887
  this.dom.outer.classList.add("shoji-open");
1837
1888
  document.addEventListener("keydown", this.onKeydown);
1838
1889
  this.focusTrap.activate(this.dom.dialog);
1839
1890
  this.scheduleAutoHide();
1840
1891
  this.applyMobileControlsSetting();
1841
1892
  const media = (_a = this.slides) == null ? void 0 : _a.getActiveMedia();
1842
- if (media && origin) {
1843
- zoomIn({ origin, target: media, aspectRatio: this.resolveAspectRatio(index, origin) });
1893
+ if (media && origin && naturalSize) {
1894
+ zoomIn({
1895
+ origin,
1896
+ target: media,
1897
+ aspectRatio: this.resolveAspectRatio(index, origin),
1898
+ naturalSize
1899
+ });
1844
1900
  }
1845
1901
  this.bus.emit("open", { index });
1846
1902
  this.bus.emit("afterOpen", { index });
@@ -1940,16 +1996,17 @@ class Gallery {
1940
1996
  }
1941
1997
  }
1942
1998
  close() {
1943
- var _a;
1999
+ var _a, _b;
1944
2000
  if (this.destroyed || !this.opened || this.isClosing) return;
1945
2001
  this.bus.emit("beforeClose", {});
1946
2002
  this.isClosing = true;
1947
2003
  const media = (_a = this.slides) == null ? void 0 : _a.getActiveMedia();
1948
2004
  pauseMedia(media ?? null);
1949
2005
  const origin = this.getOriginElement(this.activeIndex);
1950
- if (media && origin) {
2006
+ const naturalSize = this.resolveNaturalSize(this.activeIndex);
2007
+ if (media && origin && (((_b = this.slides) == null ? void 0 : _b.isActiveReady()) || naturalSize)) {
1951
2008
  const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);
1952
- zoomOut({ origin, target: media, aspectRatio }, () => this.finishClose());
2009
+ zoomOut({ origin, target: media, aspectRatio, naturalSize }, () => this.finishClose());
1953
2010
  } else {
1954
2011
  this.finishClose();
1955
2012
  }
@@ -1964,6 +2021,19 @@ class Gallery {
1964
2021
  }
1965
2022
  return void 0;
1966
2023
  }
2024
+ /**
2025
+ * `item.width`/`height` only — deliberately never a thumbnail's own
2026
+ * `naturalWidth`/`naturalHeight` the way `resolveAspectRatio` above will:
2027
+ * that's a fine stand-in for *shape*, but using it as the real photo's
2028
+ * true pixel size would under-cap a genuinely large photo down to
2029
+ * thumbnail resolution. `undefined` here just means "genuinely unknown,"
2030
+ * not "assume small" — `zoomTransition.ts`'s `containedBox` already
2031
+ * treats it that way (no cap applied at all).
2032
+ */
2033
+ resolveNaturalSize(index) {
2034
+ const item = this.itemList[index];
2035
+ return (item == null ? void 0 : item.width) && item.height ? { width: item.width, height: item.height } : void 0;
2036
+ }
1967
2037
  /**
1968
2038
  * Idempotent on purpose: a pending zoom-out's `transitionend`/fallback
1969
2039
  * timeout can still fire after `destroy()` has already force-finished the