smartphoto 2.1.3 → 2.1.6

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/lib/smartphoto.js CHANGED
@@ -124,6 +124,7 @@ var defaults = {
124
124
  swipeTopToClose: false,
125
125
  swipeBottomToClose: true,
126
126
  swipeOffset: 100,
127
+ swipeVelocity: 0.5,
127
128
  headerHeight: 60,
128
129
  footerHeight: 60,
129
130
  forceInterval: 10,
@@ -131,7 +132,7 @@ var defaults = {
131
132
  loadOffset: 2,
132
133
  resizeStyle: "fit",
133
134
  lazyAttribute: "data-src",
134
- animationSpeed: 300
135
+ animationSpeed: 450
135
136
  };
136
137
  function deepFreeze(obj) {
137
138
  Object.keys(obj).forEach((key) => {
@@ -145,7 +146,11 @@ function deepFreeze(obj) {
145
146
  function createState(settings) {
146
147
  return {
147
148
  options: deepFreeze(
148
- extend({}, defaults, settings)
149
+ extend(
150
+ {},
151
+ defaults,
152
+ settings
153
+ )
149
154
  ),
150
155
  viewer: {
151
156
  isOpen: false,
@@ -381,6 +386,7 @@ function distance(p1, p2) {
381
386
  const y = p1.y - p2.y;
382
387
  return Math.sqrt(x * x + y * y);
383
388
  }
389
+ var MIN_FLICK_DISTANCE = 10;
384
390
  function getForceAndTheta(x, y) {
385
391
  return { force: Math.sqrt(x * x + y * y), theta: Math.atan2(y, x) };
386
392
  }
@@ -398,6 +404,7 @@ function createGestures({ state, callbacks }, { signal }) {
398
404
  let firstPos = null;
399
405
  let oldPos = null;
400
406
  let moveDir = null;
407
+ let swipeStartTime = 0;
401
408
  let photoSwipable = false;
402
409
  let firstPhotoPos = null;
403
410
  let oldPhotoPos = null;
@@ -405,6 +412,7 @@ function createGestures({ state, callbacks }, { signal }) {
405
412
  let photoVY = 0;
406
413
  let pinching = false;
407
414
  let oldDistance = 0;
415
+ let pinchMoveFrame = null;
408
416
  let vx = 0;
409
417
  let vy = 0;
410
418
  function isSmartPhone2() {
@@ -526,6 +534,7 @@ function createGestures({ state, callbacks }, { signal }) {
526
534
  dragStart = true;
527
535
  firstPos = pos;
528
536
  oldPos = pos;
537
+ swipeStartTime = Date.now();
529
538
  }
530
539
  function startPhotoDrag(e) {
531
540
  photoSwipable = true;
@@ -549,6 +558,23 @@ function createGestures({ state, callbacks }, { signal }) {
549
558
  }
550
559
  startSwipe(e);
551
560
  }
561
+ function scheduleGestureMove() {
562
+ if (pinchMoveFrame !== null) {
563
+ return;
564
+ }
565
+ pinchMoveFrame = requestAnimationFrame(() => {
566
+ pinchMoveFrame = null;
567
+ callbacks.onGestureMove();
568
+ });
569
+ }
570
+ function flushGestureMove() {
571
+ if (pinchMoveFrame === null) {
572
+ return;
573
+ }
574
+ cancelAnimationFrame(pinchMoveFrame);
575
+ pinchMoveFrame = null;
576
+ callbacks.onGestureMove();
577
+ }
552
578
  function movePinch() {
553
579
  const points = Array.from(activePointers.values());
554
580
  const dist = distance(
@@ -573,7 +599,7 @@ function createGestures({ state, callbacks }, { signal }) {
573
599
  state.viewer.hideUi = state.viewer.scaleSize < 1 || state.viewer.scaleSize > border;
574
600
  }
575
601
  oldDistance = dist;
576
- callbacks.onGestureMove();
602
+ scheduleGestureMove();
577
603
  }
578
604
  function moveSwipe(e) {
579
605
  const pos = getPos(e);
@@ -622,6 +648,7 @@ function createGestures({ state, callbacks }, { signal }) {
622
648
  }
623
649
  function endPinch() {
624
650
  pinching = false;
651
+ flushGestureMove();
625
652
  const item = currentItem(state);
626
653
  if (!item) {
627
654
  return;
@@ -658,9 +685,11 @@ function createGestures({ state, callbacks }, { signal }) {
658
685
  const items = currentItems(state) ?? [];
659
686
  if (moveDir === "horizontal") {
660
687
  let result = "stay";
661
- if (swipeWidth >= state.options.swipeOffset && state.viewer.currentIndex !== 0) {
688
+ const elapsedMs = Math.max(now - swipeStartTime, 1);
689
+ const isFlick = Math.abs(swipeWidth) >= MIN_FLICK_DISTANCE && Math.abs(swipeWidth) / elapsedMs >= state.options.swipeVelocity;
690
+ if ((swipeWidth >= state.options.swipeOffset || isFlick && swipeWidth > 0) && state.viewer.currentIndex !== 0) {
662
691
  result = "prev";
663
- } else if (swipeWidth <= -state.options.swipeOffset && state.viewer.currentIndex !== items.length - 1) {
692
+ } else if ((swipeWidth <= -state.options.swipeOffset || isFlick && swipeWidth < 0) && state.viewer.currentIndex !== items.length - 1) {
664
693
  result = "next";
665
694
  }
666
695
  callbacks.onSwipeEnd(result);
@@ -751,6 +780,10 @@ function createGestures({ state, callbacks }, { signal }) {
751
780
  }
752
781
  function detach() {
753
782
  clearInterval(interval);
783
+ if (pinchMoveFrame !== null) {
784
+ cancelAnimationFrame(pinchMoveFrame);
785
+ pinchMoveFrame = null;
786
+ }
754
787
  }
755
788
  return { attach, detach };
756
789
  }
@@ -1082,7 +1115,11 @@ function getWindowWidth() {
1082
1115
  return document.documentElement.clientWidth;
1083
1116
  }
1084
1117
  function getWindowHeight() {
1085
- return window.visualViewport?.height ?? document.documentElement.clientHeight;
1118
+ const visualViewport = window.visualViewport;
1119
+ if (visualViewport) {
1120
+ return visualViewport.height * visualViewport.scale;
1121
+ }
1122
+ return document.documentElement.clientHeight;
1086
1123
  }
1087
1124
  function isElementArray(source) {
1088
1125
  return source.length > 0 && source[0] instanceof Element;
@@ -1113,6 +1150,44 @@ var SmartPhoto = class {
1113
1150
  this.timeouts = [];
1114
1151
  this.loadAllFired = /* @__PURE__ */ new Set();
1115
1152
  this.syncedGroupId = null;
1153
+ // 文字列セレクタで構築された場合のみ設定される。Ajax等で後から追加された
1154
+ // 要素をクリック時に動的検出するための再スキャン起点(§discoverGroupElements)
1155
+ this.rootSelector = null;
1156
+ // クリックされた要素(またはその祖先)から登録済み Item を逆引きするための
1157
+ // キャッシュ。documentへのイベントデリゲーションで祖先チェーンを辿る際に使う
1158
+ this.itemsByElement = /* @__PURE__ */ new Map();
1159
+ // サムネイルクリックはこの1本の document デリゲーションリスナーだけで処理する
1160
+ // (個別バインドは廃止)。同じクリックイベントを複数の SmartPhoto インスタンスが
1161
+ // 二重処理しないよう、処理済みマーカーをイベント自体に立てて後続インスタンスに
1162
+ // 早期returnさせる(セレクタが重複するインスタンスが同時に存在するケース)
1163
+ this.handleDocumentClick = (e) => {
1164
+ if (!(e.target instanceof Element)) {
1165
+ return;
1166
+ }
1167
+ const marker = e;
1168
+ if (marker.__smartphotoClaimed) {
1169
+ return;
1170
+ }
1171
+ let matched = this.findRegisteredAncestor(e.target);
1172
+ if (!matched && this.rootSelector) {
1173
+ matched = e.target.closest(this.rootSelector);
1174
+ }
1175
+ if (!matched) {
1176
+ return;
1177
+ }
1178
+ e.preventDefault();
1179
+ marker.__smartphotoClaimed = true;
1180
+ if (!this.itemsByElement.has(matched) && this.rootSelector) {
1181
+ const groupId = groupIdFromElement(matched);
1182
+ if (this.resyncGroupFromDom(groupId)) {
1183
+ this.syncCurrentGroupView();
1184
+ }
1185
+ }
1186
+ const item = this.itemsByElement.get(matched);
1187
+ if (item) {
1188
+ this.openPhoto(item, matched);
1189
+ }
1190
+ };
1116
1191
  // ---- 内部: window イベント ----
1117
1192
  this.updateViewportHeight = () => {
1118
1193
  this.view.refs.dialog.style.setProperty(
@@ -1124,6 +1199,7 @@ var SmartPhoto = class {
1124
1199
  if (!currentItems(this.state)) {
1125
1200
  return;
1126
1201
  }
1202
+ this.updateViewportHeight();
1127
1203
  this.resetTranslateCurrent();
1128
1204
  this.setPosByCurrentIndex();
1129
1205
  this.setSizeByScreen();
@@ -1146,6 +1222,7 @@ var SmartPhoto = class {
1146
1222
  if (!currentItems(this.state)) {
1147
1223
  return;
1148
1224
  }
1225
+ this.updateViewportHeight();
1149
1226
  this.resetTranslateCurrent();
1150
1227
  this.setPosByCurrentIndex();
1151
1228
  this.setHashByCurrentIndex();
@@ -1156,6 +1233,7 @@ var SmartPhoto = class {
1156
1233
  const poll = (time) => {
1157
1234
  this.scheduleTimeout(() => {
1158
1235
  if (prevWidth !== getWindowWidth()) {
1236
+ this.updateViewportHeight();
1159
1237
  this.resetTranslateCurrent();
1160
1238
  this.setPosByCurrentIndex();
1161
1239
  this.setHashByCurrentIndex();
@@ -1168,6 +1246,7 @@ var SmartPhoto = class {
1168
1246
  };
1169
1247
  poll(0);
1170
1248
  };
1249
+ this.rootSelector = typeof source === "string" ? source : null;
1171
1250
  this.state = createState(settings ?? {});
1172
1251
  this.view = createView(
1173
1252
  { id: this.id, options: this.state.options },
@@ -1189,15 +1268,14 @@ var SmartPhoto = class {
1189
1268
  },
1190
1269
  { signal: this.abortController.signal }
1191
1270
  );
1271
+ document.addEventListener("click", this.handleDocumentClick, {
1272
+ signal: this.abortController.signal
1273
+ });
1192
1274
  this.ingestSource(source);
1193
1275
  this.syncCurrentGroupView();
1194
1276
  const restored = this.restoreFromHash();
1195
1277
  if (restored) {
1196
- if (restored.element) {
1197
- triggerEvent(restored.element, "click");
1198
- } else {
1199
- this.openPhoto(restored, null);
1200
- }
1278
+ this.openPhoto(restored, restored.element);
1201
1279
  }
1202
1280
  this.updateViewportHeight();
1203
1281
  if (window.visualViewport) {
@@ -1242,6 +1320,7 @@ var SmartPhoto = class {
1242
1320
  clearTimeout(id);
1243
1321
  });
1244
1322
  this.timeouts = [];
1323
+ this.itemsByElement.clear();
1245
1324
  this.gestures.detach();
1246
1325
  this.view.destroy();
1247
1326
  }
@@ -1354,7 +1433,7 @@ var SmartPhoto = class {
1354
1433
  this.gotoSlide(this.state.viewer.prev);
1355
1434
  }
1356
1435
  addItem(slideOrElement) {
1357
- const item = slideOrElement instanceof Element ? this.addElementItem(slideOrElement) : this.addSlideItem(slideOrElement);
1436
+ const item = slideOrElement instanceof HTMLElement ? this.addElementItem(slideOrElement) : this.addSlideItem(slideOrElement);
1358
1437
  this.syncCurrentGroupView();
1359
1438
  return item;
1360
1439
  }
@@ -1385,7 +1464,7 @@ var SmartPhoto = class {
1385
1464
  );
1386
1465
  addItemToGroup(this.state, item);
1387
1466
  this.loadAllFired.delete(groupId);
1388
- this.bindThumbnailClick(element, item);
1467
+ this.itemsByElement.set(element, item);
1389
1468
  return item;
1390
1469
  }
1391
1470
  addSlideItem(slide) {
@@ -1396,15 +1475,72 @@ var SmartPhoto = class {
1396
1475
  this.loadAllFired.delete(groupId);
1397
1476
  return item;
1398
1477
  }
1399
- bindThumbnailClick(element, item) {
1400
- element.addEventListener(
1401
- "click",
1402
- (e) => {
1403
- e.preventDefault();
1404
- this.openPhoto(item, element);
1405
- },
1406
- { signal: this.abortController.signal }
1407
- );
1478
+ // クリックされた要素(またはその祖先。<a><img></a> の img がクリックされる
1479
+ // ケースを含む)から、登録済み Item を持つ要素まで祖先チェーンを遡って探す
1480
+ findRegisteredAncestor(target) {
1481
+ let node = target;
1482
+ while (node) {
1483
+ if (this.itemsByElement.has(node)) {
1484
+ return node;
1485
+ }
1486
+ node = node.parentElement;
1487
+ }
1488
+ return null;
1489
+ }
1490
+ // rootSelector(文字列セレクタで構築した場合のみ設定される)を再スキャンし、
1491
+ // 指定グループを現在のDOM状態に合わせて丸ごと再構築する(追加・削除・並び順の
1492
+ // 変化を一度に反映)。既知要素は既存の Item オブジェクトをそのまま再利用する
1493
+ // ため loaded/width/height は保持され、DOM から消えた要素だけ itemsByElement
1494
+ // からも除去する。index/translateX は resetTranslate() で再計算する。
1495
+ // 呼び出し元(openPhoto)がこの結果を使い、変化があった場合のみ view を
1496
+ // 再同期する(§ダイアログを開く瞬間に整合を取る。開いている間の next()/prev()
1497
+ // では呼ばれないため、この間の追加/削除は次に開き直すまで反映されない)
1498
+ resyncGroupFromDom(groupId) {
1499
+ if (!this.rootSelector) {
1500
+ return false;
1501
+ }
1502
+ const domElements = Array.from(
1503
+ document.querySelectorAll(this.rootSelector)
1504
+ ).filter((el) => groupIdFromElement(el) === groupId);
1505
+ const domElementSet = new Set(domElements);
1506
+ const previous = this.state.groups.get(groupId) ?? [];
1507
+ const previousByElement = /* @__PURE__ */ new Map();
1508
+ previous.forEach((it) => {
1509
+ if (it.element) {
1510
+ previousByElement.set(it.element, it);
1511
+ }
1512
+ });
1513
+ let changed = domElements.length !== previous.length;
1514
+ const rebuilt = domElements.map((el, index) => {
1515
+ const existing = previousByElement.get(el);
1516
+ if (existing) {
1517
+ if (existing.index !== index) {
1518
+ changed = true;
1519
+ existing.index = index;
1520
+ }
1521
+ return existing;
1522
+ }
1523
+ changed = true;
1524
+ const item = itemFromElement(
1525
+ el,
1526
+ this.state.options,
1527
+ index,
1528
+ getWindowWidth()
1529
+ );
1530
+ this.itemsByElement.set(el, item);
1531
+ return item;
1532
+ });
1533
+ previous.forEach((it) => {
1534
+ if (it.element && !domElementSet.has(it.element)) {
1535
+ this.itemsByElement.delete(it.element);
1536
+ }
1537
+ });
1538
+ resetTranslate(rebuilt, getWindowWidth());
1539
+ this.state.groups.set(groupId, rebuilt);
1540
+ if (changed) {
1541
+ this.loadAllFired.delete(groupId);
1542
+ }
1543
+ return changed;
1408
1544
  }
1409
1545
  syncCurrentGroupView() {
1410
1546
  const items = currentItems(this.state);
@@ -1658,10 +1794,11 @@ var SmartPhoto = class {
1658
1794
  });
1659
1795
  }
1660
1796
  openPhoto(item, trigger) {
1797
+ const groupChanged = this.rootSelector ? this.resyncGroupFromDom(item.groupId) : false;
1661
1798
  this.lastTriggerElement = trigger;
1662
1799
  this.state.viewer.currentGroup = item.groupId;
1663
1800
  this.state.viewer.currentIndex = item.index;
1664
- if (this.syncedGroupId !== item.groupId) {
1801
+ if (this.syncedGroupId !== item.groupId || groupChanged) {
1665
1802
  this.syncCurrentGroupView();
1666
1803
  }
1667
1804
  this.setHashByCurrentIndex();
@@ -1752,6 +1889,7 @@ var SmartPhoto = class {
1752
1889
  this.state.viewer.photoPosX = 0;
1753
1890
  this.state.viewer.photoPosY = 0;
1754
1891
  this.state.viewer.onMove = true;
1892
+ this.updateViewportHeight();
1755
1893
  this.setPosByCurrentIndex();
1756
1894
  this.setHashByCurrentIndex();
1757
1895
  this.setSizeByScreen();