smartphoto 2.1.12 → 2.1.13

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.
@@ -3,6 +3,9 @@ var isSmartPhone = () => {
3
3
  const agent = navigator.userAgent;
4
4
  return agent.indexOf("iPhone") > 0 || agent.indexOf("iPad") > 0 || agent.indexOf("ipod") > 0 || agent.indexOf("Android") > 0;
5
5
  };
6
+ var isWebKit = () => {
7
+ return typeof window.webkitConvertPointFromNodeToPage === "function";
8
+ };
6
9
  function deepExtend(out, ...args) {
7
10
  out = out || {};
8
11
  for (let i = 0; i < args.length; i++) {
@@ -140,6 +143,7 @@ var defaults = {
140
143
  verticalGravity: false,
141
144
  useOrientationApi: false,
142
145
  useHistoryApi: true,
146
+ useViewTransitionApi: true,
143
147
  swipeTopToClose: false,
144
148
  swipeBottomToClose: true,
145
149
  swipeOffset: 100,
@@ -424,6 +428,7 @@ function createGestures({ state, callbacks }, { signal }) {
424
428
  let oldPos = null;
425
429
  let moveDir = null;
426
430
  let swipeStartTime = 0;
431
+ let tapTarget = null;
427
432
  let photoSwipable = false;
428
433
  let firstPhotoPos = null;
429
434
  let oldPhotoPos = null;
@@ -554,6 +559,7 @@ function createGestures({ state, callbacks }, { signal }) {
554
559
  firstPos = pos;
555
560
  oldPos = pos;
556
561
  swipeStartTime = Date.now();
562
+ tapTarget = e.target;
557
563
  }
558
564
  function startPhotoDrag(e) {
559
565
  photoSwipable = true;
@@ -693,11 +699,11 @@ function createGestures({ state, callbacks }, { signal }) {
693
699
  const swipeHeight = last.y - first.y;
694
700
  const noMove = swipeWidth === 0 && swipeHeight === 0;
695
701
  if (!isSmartPhone2() && noMove) {
696
- callbacks.onTap();
702
+ callbacks.onTap(tapTarget);
697
703
  return;
698
704
  }
699
705
  if (Math.abs(offset) <= 500 && noMove) {
700
- callbacks.onTap();
706
+ callbacks.onTap(tapTarget);
701
707
  return;
702
708
  }
703
709
  tapSecond = now;
@@ -1017,6 +1023,9 @@ function createView({ id, options }, handlers, { signal }) {
1017
1023
  if (slideRefs.imgWrap && slideRefs.img) {
1018
1024
  slideRefs.imgWrap.style.transform = `translate(${item.x}px,${item.y}px) scale(${item.scale})`;
1019
1025
  slideRefs.img.style.width = `${item.width}px`;
1026
+ if (!isCurrent) {
1027
+ slideRefs.img.style.transform = "translate(0px,0px) scale(1)";
1028
+ }
1020
1029
  slideRefs.img.classList.toggle("active", viewer.appear);
1021
1030
  slideRefs.img.classList.toggle(
1022
1031
  classNames.smartPhotoImgOnMove,
@@ -1100,6 +1109,23 @@ function createView({ id, options }, handlers, { signal }) {
1100
1109
  refs.list.style.transform = `translate(${viewer.translateX}px,${viewer.translateY}px)`;
1101
1110
  refs.list.classList.toggle(classNames.smartPhotoListOnMove, viewer.onMove);
1102
1111
  }
1112
+ function applyViewTransitionLayout(state) {
1113
+ for (const [item, slideRefs] of refs.slides) {
1114
+ if (item.index !== state.viewer.currentIndex) {
1115
+ continue;
1116
+ }
1117
+ const { img, imgWrap } = slideRefs;
1118
+ if (!img || !imgWrap || !item.width || !item.scale || item.scale >= 1) {
1119
+ return;
1120
+ }
1121
+ const before = img.getBoundingClientRect();
1122
+ img.style.width = `${item.width * item.scale}px`;
1123
+ imgWrap.style.transform = `translate(${item.x}px,${item.y}px) scale(1)`;
1124
+ const after = img.getBoundingClientRect();
1125
+ imgWrap.style.transform = `translate(${item.x + before.left - after.left}px,${item.y + before.top - after.top}px) scale(1)`;
1126
+ return;
1127
+ }
1128
+ }
1103
1129
  function showAppearEffect(effect) {
1104
1130
  const clone = document.createElement("img");
1105
1131
  clone.className = classNames.smartPhotoImgClone;
@@ -1123,6 +1149,7 @@ function createView({ id, options }, handlers, { signal }) {
1123
1149
  render,
1124
1150
  syncSlides,
1125
1151
  updatePhotoTransform,
1152
+ applyViewTransitionLayout,
1126
1153
  updateListTransform,
1127
1154
  showAppearEffect,
1128
1155
  removeAppearEffect,
@@ -1159,6 +1186,13 @@ var SmartPhoto = class {
1159
1186
  // doHideEffect() の後始末(画像に設定した translateY の除去と promise 解決)を
1160
1187
  // transitionend を待たずに即時実行するための関数。後始末が済むと null に戻る
1161
1188
  this.finishHideEffect = null;
1189
+ // 開く View Transition が進行中かどうか。進行中にリサイズ由来の再計算(commit)が
1190
+ // 走ると、モーフの目標位置は古いビューポート基準のままなのに実要素だけ新しい
1191
+ // 位置へ動いて「一旦ずれた位置に出てから正位置へ戻る」ように見え、さらに
1192
+ // モーフ用レイアウト(applyViewTransitionLayout)も巻き戻されてしまう。
1193
+ // そのため進行中はリサイズ再計算を保留し、finished 後にまとめて実行する
1194
+ this.isViewTransitionActive = false;
1195
+ this.pendingViewTransitionResync = false;
1162
1196
  this.timeouts = [];
1163
1197
  this.loadAllFired = /* @__PURE__ */ new Set();
1164
1198
  this.syncedGroupId = null;
@@ -1217,16 +1251,24 @@ var SmartPhoto = class {
1217
1251
  if (!this.isSmartPhoneFlag || !this.state.viewer.isOpen || !currentItems(this.state)) {
1218
1252
  return;
1219
1253
  }
1254
+ if (this.isViewTransitionActive) {
1255
+ this.pendingViewTransitionResync = true;
1256
+ return;
1257
+ }
1220
1258
  this.resetTranslateCurrent();
1221
1259
  this.setPosByCurrentIndex();
1222
1260
  this.setSizeByScreen();
1223
1261
  this.commit();
1224
1262
  };
1225
1263
  this.handleResize = () => {
1226
- if (!currentItems(this.state)) {
1264
+ if (!this.state.viewer.isOpen || !currentItems(this.state)) {
1227
1265
  return;
1228
1266
  }
1229
1267
  this.updateViewportHeight();
1268
+ if (this.isViewTransitionActive) {
1269
+ this.pendingViewTransitionResync = true;
1270
+ return;
1271
+ }
1230
1272
  this.resetTranslateCurrent();
1231
1273
  this.setPosByCurrentIndex();
1232
1274
  this.setSizeByScreen();
@@ -1246,7 +1288,11 @@ var SmartPhoto = class {
1246
1288
  }
1247
1289
  };
1248
1290
  this.handleOrientationChange = () => {
1249
- if (!currentItems(this.state)) {
1291
+ if (!this.state.viewer.isOpen || !currentItems(this.state)) {
1292
+ return;
1293
+ }
1294
+ if (this.isViewTransitionActive) {
1295
+ this.pendingViewTransitionResync = true;
1250
1296
  return;
1251
1297
  }
1252
1298
  this.updateViewportHeight();
@@ -1259,6 +1305,9 @@ var SmartPhoto = class {
1259
1305
  const timeout = 500;
1260
1306
  const poll = (time) => {
1261
1307
  this.scheduleTimeout(() => {
1308
+ if (!this.state.viewer.isOpen) {
1309
+ return;
1310
+ }
1262
1311
  if (prevWidth !== getWindowWidth2()) {
1263
1312
  this.updateViewportHeight();
1264
1313
  this.resetTranslateCurrent();
@@ -1410,10 +1459,26 @@ var SmartPhoto = class {
1410
1459
  this.state.viewer.photoPosY = 0;
1411
1460
  this.view.updatePhotoTransform(this.state);
1412
1461
  this.scheduleTimeout(() => {
1462
+ if (!this.state.viewer.isOpen) {
1463
+ return;
1464
+ }
1413
1465
  this.state.viewer.scale = true;
1414
1466
  this.view.updatePhotoTransform(this.state);
1415
1467
  this.fireEvent("zoomin");
1416
- }, 300);
1468
+ }, this.state.options.animationSpeed);
1469
+ }
1470
+ // zoomPhoto() と同じ「ズームの余地があるか」の判定(scaleBorder <= 1 なら
1471
+ // ズームしても見た目が変わらない)。zoomPhoto() 自体は公開 API の契約
1472
+ // (ズームできなければ何もしない)を保つ必要があるため、タップの
1473
+ // zoom-or-close 分岐(§7)用に判定だけを切り出している
1474
+ canZoomCurrentPhoto() {
1475
+ const item = currentItem(this.state);
1476
+ return !!item && scaleBorder(
1477
+ item,
1478
+ getWindowWidth2(),
1479
+ getWindowHeight2(),
1480
+ this.isSmartPhoneFlag
1481
+ ) > 1;
1417
1482
  }
1418
1483
  zoomOutPhoto() {
1419
1484
  this.state.viewer.scaleSize = 1;
@@ -1619,6 +1684,7 @@ var SmartPhoto = class {
1619
1684
  this.state.options.headerHeight,
1620
1685
  this.state.options.footerHeight
1621
1686
  );
1687
+ this.syncFillScale();
1622
1688
  }
1623
1689
  // resize/orientationchange ハンドラは呼び出し前に currentItems の存在を確認済み
1624
1690
  resetTranslateCurrent() {
@@ -1632,9 +1698,18 @@ var SmartPhoto = class {
1632
1698
  }
1633
1699
  return null;
1634
1700
  }
1701
+ currentLiElement() {
1702
+ for (const [item, slideRefs] of this.view.refs.slides) {
1703
+ if (item.index === this.state.viewer.currentIndex) {
1704
+ return slideRefs.li;
1705
+ }
1706
+ }
1707
+ return null;
1708
+ }
1635
1709
  // ---- 内部: dialog 制御(§8) ----
1636
1710
  syncDialog() {
1637
1711
  const { dialog, caption } = this.view.refs;
1712
+ dialog.style.transition = this.state.options.showAnimation === false ? "none" : "";
1638
1713
  if (this.state.viewer.isOpen && !dialog.open) {
1639
1714
  dialog.style.removeProperty("display");
1640
1715
  dialog.showModal();
@@ -1646,6 +1721,7 @@ var SmartPhoto = class {
1646
1721
  }
1647
1722
  commit() {
1648
1723
  this.view.render(this.state);
1724
+ this.view.updatePhotoTransform(this.state);
1649
1725
  this.syncDialog();
1650
1726
  }
1651
1727
  // ---- 内部: 開く演出 ----
@@ -1663,20 +1739,34 @@ var SmartPhoto = class {
1663
1739
  this.setPosByCurrentIndex();
1664
1740
  this.setSizeByScreen();
1665
1741
  setArrow(this.state);
1666
- if (this.state.options.resizeStyle === "fill" && this.isSmartPhoneFlag) {
1667
- const item = currentItem(this.state);
1668
- this.state.viewer.scale = true;
1669
- this.state.viewer.hideUi = true;
1670
- this.state.viewer.scaleSize = scaleBorder(
1671
- item,
1672
- getWindowWidth2(),
1673
- getWindowHeight2(),
1674
- this.isSmartPhoneFlag
1675
- );
1742
+ }
1743
+ // resizeStyle: 'fill' はスマートフォンでのみ、画面を隙間なく覆うよう
1744
+ // scaleBorder() の倍率を viewer.scaleSize に適用する。fit用の item.scale に
1745
+ // 依存するため、setSizeByScreen() の末尾から必ず呼ばれる(個別に呼ぶ必要はない)
1746
+ syncFillScale() {
1747
+ if (this.state.options.resizeStyle !== "fill" || !this.isSmartPhoneFlag) {
1748
+ return;
1676
1749
  }
1750
+ const item = currentItem(this.state);
1751
+ if (!item) {
1752
+ return;
1753
+ }
1754
+ this.state.viewer.scale = true;
1755
+ this.state.viewer.hideUi = true;
1756
+ this.state.viewer.scaleSize = scaleBorder(
1757
+ item,
1758
+ getWindowWidth2(),
1759
+ getWindowHeight2(),
1760
+ this.isSmartPhoneFlag
1761
+ );
1677
1762
  }
1678
1763
  supportsViewTransition() {
1679
- return typeof document.startViewTransition === "function";
1764
+ return this.state.options.useViewTransitionApi && typeof document.startViewTransition === "function" && // WebKit には「view-transition-name 付き要素が root スナップショットから
1765
+ // 除外されず、モーフ中に最終位置へ二重描画される」実装バグがあり、開く演出が
1766
+ // 「一旦ずれた位置に出てから正位置へ戻る」ように乱れる。API 対応の有無では
1767
+ // 検出できないため、WebKit ではクローン演出(addAppearEffect)へフォールバック
1768
+ // する(§util.isWebKit)
1769
+ !isWebKit();
1680
1770
  }
1681
1771
  openPhotoWithViewTransition(trigger) {
1682
1772
  document.documentElement.style.setProperty(
@@ -1688,7 +1778,8 @@ var SmartPhoto = class {
1688
1778
  if (thumbImg) {
1689
1779
  thumbImg.style.viewTransitionName = transitionName;
1690
1780
  }
1691
- const clearNames = () => {
1781
+ const cleanup = () => {
1782
+ this.isViewTransitionActive = false;
1692
1783
  if (thumbImg) {
1693
1784
  thumbImg.style.viewTransitionName = "";
1694
1785
  }
@@ -1696,6 +1787,21 @@ var SmartPhoto = class {
1696
1787
  if (img) {
1697
1788
  img.style.viewTransitionName = "";
1698
1789
  }
1790
+ if (!this.state.viewer.isOpen) {
1791
+ this.pendingViewTransitionResync = false;
1792
+ return;
1793
+ }
1794
+ if (this.pendingViewTransitionResync && currentItems(this.state)) {
1795
+ this.pendingViewTransitionResync = false;
1796
+ this.updateViewportHeight();
1797
+ this.resetTranslateCurrent();
1798
+ this.setPosByCurrentIndex();
1799
+ this.setSizeByScreen();
1800
+ this.commit();
1801
+ return;
1802
+ }
1803
+ this.pendingViewTransitionResync = false;
1804
+ this.view.render(this.state);
1699
1805
  };
1700
1806
  const transition = document.startViewTransition?.(() => {
1701
1807
  this.initPhoto();
@@ -1705,11 +1811,16 @@ var SmartPhoto = class {
1705
1811
  thumbImg.style.viewTransitionName = "";
1706
1812
  }
1707
1813
  this.currentImgElement().style.viewTransitionName = transitionName;
1814
+ this.view.applyViewTransitionLayout(this.state);
1708
1815
  });
1709
- transition?.ready.catch(() => {
1710
- clearNames();
1816
+ if (!transition) {
1817
+ return;
1818
+ }
1819
+ this.isViewTransitionActive = true;
1820
+ transition.ready.catch(() => {
1821
+ cleanup();
1711
1822
  });
1712
- transition?.finished.then(clearNames, clearNames);
1823
+ transition.finished.then(cleanup, cleanup);
1713
1824
  }
1714
1825
  // doOpen() の呼び出し元でこの分岐に来る時点で showAnimation !== false が保証されている
1715
1826
  addAppearEffect(trigger, item) {
@@ -1818,10 +1929,14 @@ var SmartPhoto = class {
1818
1929
  if (getWindowWidth2() === winWidthBefore && getWindowHeight2() === winHeightBefore) {
1819
1930
  return;
1820
1931
  }
1932
+ if (this.isViewTransitionActive) {
1933
+ this.pendingViewTransitionResync = true;
1934
+ return;
1935
+ }
1821
1936
  this.resetTranslateCurrent();
1822
1937
  this.setPosByCurrentIndex();
1823
1938
  this.setSizeByScreen();
1824
- this.view.render(this.state);
1939
+ this.commit();
1825
1940
  });
1826
1941
  }
1827
1942
  openPhoto(item, trigger) {
@@ -1843,21 +1958,37 @@ var SmartPhoto = class {
1843
1958
  }
1844
1959
  doHideEffect(dir) {
1845
1960
  return new Promise((resolve) => {
1961
+ if (this.state.options.showAnimation === false) {
1962
+ resolve();
1963
+ return;
1964
+ }
1846
1965
  const dialog = this.view.refs.dialog;
1847
- const img = this.currentImgElement();
1966
+ const item = currentItem(this.state);
1967
+ const li = item ? this.currentLiElement() : null;
1848
1968
  const height = getWindowHeight2();
1849
- const applied = dir === "top" ? `translateY(-${height}px)` : `translateY(${height}px)`;
1969
+ const img = this.currentImgElement();
1970
+ let frozenTransform = null;
1850
1971
  if (img) {
1851
- img.style.transform = applied;
1972
+ const rendered = getComputedStyle(img).transform;
1973
+ if (rendered && rendered !== "none") {
1974
+ img.style.transform = rendered;
1975
+ frozenTransform = img.style.transform;
1976
+ }
1852
1977
  }
1853
- const stored = img ? img.style.transform : "";
1854
- const finish = () => {
1978
+ if (li && item) {
1979
+ const offsetY = dir === "top" ? -height : height;
1980
+ li.style.transform = `translate(${item.translateX}px,${item.translateY + offsetY}px)`;
1981
+ }
1982
+ const finish = (e) => {
1855
1983
  if (this.finishHideEffect !== finish) {
1856
1984
  return;
1857
1985
  }
1986
+ if (e && e.target !== dialog) {
1987
+ return;
1988
+ }
1858
1989
  this.finishHideEffect = null;
1859
1990
  dialog.removeEventListener("transitionend", finish, true);
1860
- if (img && img.style.transform === stored) {
1991
+ if (img && frozenTransform && img.style.transform === frozenTransform) {
1861
1992
  img.style.transform = "";
1862
1993
  }
1863
1994
  resolve();
@@ -1932,7 +2063,9 @@ var SmartPhoto = class {
1932
2063
  const item = currentItem(this.state);
1933
2064
  this.state.viewer.onMove = false;
1934
2065
  setArrow(this.state);
1935
- this.commit();
2066
+ if (this.state.viewer.isOpen) {
2067
+ this.commit();
2068
+ }
1936
2069
  if (this.state.viewer.oldIndex !== this.state.viewer.currentIndex) {
1937
2070
  this.fireEvent("change");
1938
2071
  }
@@ -2017,7 +2150,21 @@ var SmartPhoto = class {
2017
2150
  }
2018
2151
  this.slideList();
2019
2152
  },
2020
- onTap: () => this.zoomPhoto(),
2153
+ // 主要ライトボックスのデファクトスタンダード(PhotoSwipe: bgClickAction
2154
+ // 'close' / imageClickAction 'zoom-or-close'、Fancybox: backdropClick
2155
+ // 'close'、GLightbox: closeOnOutsideClick、lightGallery: closable)に合わせ、
2156
+ // 写真の上のタップはズーム、写真の外(背景)のタップは閉じる。ズームの
2157
+ // 余地がない(scaleBorder <= 1 で zoomPhoto() が何もしない)写真の上の
2158
+ // タップも、無反応にせず 'zoom-or-close' に倣って閉じる
2159
+ onTap: (target) => {
2160
+ const img = this.currentImgElement();
2161
+ const onPhoto = !!img && target instanceof Node && img.contains(target);
2162
+ if (onPhoto && this.canZoomCurrentPhoto()) {
2163
+ this.zoomPhoto();
2164
+ } else {
2165
+ this.hidePhoto();
2166
+ }
2167
+ },
2021
2168
  onGestureStart: () => {
2022
2169
  this.fireEvent("gesturestart");
2023
2170
  this.view.updatePhotoTransform(this.state);
@@ -13,6 +13,8 @@ export default class SmartPhoto {
13
13
  private lastTriggerElement;
14
14
  private isFiringPublicCloseEvent;
15
15
  private finishHideEffect;
16
+ private isViewTransitionActive;
17
+ private pendingViewTransitionResync;
16
18
  private timeouts;
17
19
  private loadAllFired;
18
20
  private syncedGroupId;
@@ -25,6 +27,7 @@ export default class SmartPhoto {
25
27
  gotoSlide(index: number): void;
26
28
  hidePhoto(dir?: "top" | "bottom"): void;
27
29
  zoomPhoto(): void;
30
+ private canZoomCurrentPhoto;
28
31
  zoomOutPhoto(): void;
29
32
  addNewItem(element: HTMLElement): SmartPhotoItem;
30
33
  show(indexOrId?: ItemId, opts?: ShowOptions): void;
@@ -47,9 +50,11 @@ export default class SmartPhoto {
47
50
  private setSizeByScreen;
48
51
  private resetTranslateCurrent;
49
52
  private currentImgElement;
53
+ private currentLiElement;
50
54
  private syncDialog;
51
55
  private commit;
52
56
  private initPhoto;
57
+ private syncFillScale;
53
58
  private supportsViewTransition;
54
59
  private openPhotoWithViewTransition;
55
60
  private addAppearEffect;
@@ -40,6 +40,7 @@ export interface ResolvedSmartPhotoOptions {
40
40
  verticalGravity: boolean;
41
41
  useOrientationApi: boolean;
42
42
  useHistoryApi: boolean;
43
+ useViewTransitionApi: boolean;
43
44
  swipeTopToClose: boolean;
44
45
  swipeBottomToClose: boolean;
45
46
  swipeOffset: number;
@@ -141,7 +142,7 @@ export interface GestureCallbacks {
141
142
  onSwipeStart(): void;
142
143
  onSwipeMove(): void;
143
144
  onSwipeEnd(result: SwipeEndResult): void;
144
- onTap(): void;
145
+ onTap(target: EventTarget | null): void;
145
146
  onGestureStart(): void;
146
147
  onGestureMove(): void;
147
148
  onGestureEnd(): void;
@@ -34,6 +34,7 @@ export interface View {
34
34
  render(state: State): void;
35
35
  syncSlides(items: Item[], state: State): void;
36
36
  updatePhotoTransform(state: State): void;
37
+ applyViewTransitionLayout(state: State): void;
37
38
  updateListTransform(state: State): void;
38
39
  showAppearEffect(effect: AppearEffect): void;
39
40
  removeAppearEffect(): void;
@@ -1,4 +1,5 @@
1
1
  export declare const isSmartPhone: () => boolean;
2
+ export declare const isWebKit: () => boolean;
2
3
  declare function deepExtend(out: Record<string, unknown>, ...args: unknown[]): Record<string, unknown>;
3
4
  export declare const extend: typeof deepExtend;
4
5
  export declare const triggerEvent: (el: Element, eventName: string, options?: unknown) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartphoto",
3
- "version": "2.1.12",
3
+ "version": "2.1.13",
4
4
  "description": "smartphoto",
5
5
  "homepage": "https://developer.a-blogcms.jp",
6
6
  "main": "./lib/smartphoto.js",