virtua 0.47.2 → 0.48.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.
@@ -212,24 +212,18 @@ const updateCacheLength = (cache, length, isShift) => {
212
212
  * @internal
213
213
  */
214
214
  const isBrowser = typeof window !== "undefined";
215
- const getDocumentElement = () => document.documentElement;
216
215
  /**
217
216
  * @internal
218
217
  */
219
- const getCurrentDocument = (node) => node.ownerDocument;
218
+ const getDocumentElement = (doc) => doc.documentElement;
220
219
  /**
221
220
  * @internal
222
221
  */
223
- const getCurrentWindow = (doc) => doc.defaultView;
222
+ const getCurrentDocument = (node) => node.ownerDocument;
224
223
  /**
225
224
  * @internal
226
225
  */
227
- const isRTLDocument = /*#__PURE__*/ once(() => {
228
- // TODO support SSR in rtl
229
- return isBrowser
230
- ? getComputedStyle(getDocumentElement()).direction === "rtl"
231
- : false;
232
- });
226
+ const getCurrentWindow = (doc) => doc.defaultView;
233
227
  /**
234
228
  * Currently, all browsers on iOS/iPadOS are WebKit, including WebView.
235
229
  * @internal
@@ -248,7 +242,7 @@ const isIOSWebKit = /*#__PURE__*/ once(() => {
248
242
  * @internal
249
243
  */
250
244
  const isSmoothScrollSupported = /*#__PURE__*/ once(() => {
251
- return "scrollBehavior" in getDocumentElement().style;
245
+ return "scrollBehavior" in getDocumentElement(document).style;
252
246
  });
253
247
 
254
248
  const MAX_INT_32 = 0x7fffffff;
@@ -316,8 +310,12 @@ const createVirtualStore = (elementsCount, itemSize = 40, ssrCount = 0, cacheSna
316
310
  return computeRange(cache, startOffset, endOffset, _prevRange[0]);
317
311
  };
318
312
  const getTotalSize = () => getItemOffset(cache, cache._length);
319
- const getItemOffset$1 = (index) => {
320
- return getItemOffset(cache, index) - pendingJump;
313
+ const getItemOffset$1 = (index, fromEnd) => {
314
+ const offset = getItemOffset(cache, index) - pendingJump;
315
+ if (fromEnd) {
316
+ return getTotalSize() - offset - getItemSize$1(index);
317
+ }
318
+ return offset;
321
319
  };
322
320
  const getItemSize$1 = (index) => {
323
321
  return getItemSize(cache, index);
@@ -598,20 +596,18 @@ const debounce = (fn, ms) => {
598
596
  return debouncedFn;
599
597
  };
600
598
  /**
601
- * scrollLeft is negative value in rtl direction.
599
+ * scrollTop/scrollLeft can be negative value under certain styles.
600
+ * - direction: rtl https://github.com/othree/jquery.rtl-scroll-type
601
+ * - writing-mode https://people.igalia.com/fwang/scrollable-elements-in-non-default-writing-modes/
602
+ * - flex-direction: column-reverse/row-reverse
602
603
  *
603
- * left right
604
- * 0 100 spec compliant (ltr)
605
- * -100 0 spec compliant (rtl)
606
- * https://github.com/othree/jquery.rtl-scroll-type
604
+ * top/left bottom/right
605
+ * 0 100 spec compliant bottom/right overflow, or possibly top/left overflow in Chrome earlier than v85
606
+ * -100 0 spec compliant top/left overflow
607
+ * https://drafts.csswg.org/cssom-view/#scroll-an-element
607
608
  */
608
- const normalizeOffset = (offset, isHorizontal) => {
609
- if (isHorizontal && isRTLDocument()) {
610
- return -offset;
611
- }
612
- else {
613
- return offset;
614
- }
609
+ const normalizeScrollOffset = (offset, isNegative) => {
610
+ return isNegative ? -offset : offset;
615
611
  };
616
612
  const createScrollObserver = (store, viewport, isHorizontal, getScrollOffset, updateScrollOffset, getStartOffset) => {
617
613
  const now = Date.now;
@@ -795,10 +791,11 @@ const createScroller = (store, isHorizontal) => {
795
791
  let viewportElement;
796
792
  let scrollObserver;
797
793
  let initialized = createPromise();
794
+ let isNegative = false;
798
795
  const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
799
796
  const overflowKey = isHorizontal ? "overflowX" : "overflowY";
800
797
  const [scheduleScroll, cancelScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
801
- offset = normalizeOffset(offset, isHorizontal);
798
+ offset = normalizeScrollOffset(offset, isNegative);
802
799
  if (smooth) {
803
800
  viewportElement.scrollTo({
804
801
  [isHorizontal ? "left" : "top"]: offset,
@@ -812,7 +809,23 @@ const createScroller = (store, isHorizontal) => {
812
809
  return {
813
810
  $observe(viewport) {
814
811
  viewportElement = viewport;
815
- scrollObserver = createScrollObserver(store, viewport, isHorizontal, () => normalizeOffset(viewport[scrollOffsetKey], isHorizontal), (jump, shift, isMomentumScrolling) => {
812
+ const clean = store.$subscribe(UPDATE_SIZE_EVENT, () => {
813
+ const viewportSize = store.$getViewportSize();
814
+ if (viewportSize) {
815
+ const prev = viewport[scrollOffsetKey];
816
+ // Detect overflowed direction after the initial viewport measurement
817
+ const dummy = getCurrentDocument(viewport).createElement("div");
818
+ dummy.style.cssText = `visibility:hidden;min-${isHorizontal ? "width" : "height"}:${viewportSize + 1}px`;
819
+ viewport.appendChild(dummy);
820
+ viewport[scrollOffsetKey] = 1;
821
+ // It can be positive under some specific situations even if negative mode, so we use `<` for now.
822
+ isNegative = viewport[scrollOffsetKey] < 1;
823
+ viewport.removeChild(dummy);
824
+ viewport[scrollOffsetKey] = prev;
825
+ clean();
826
+ }
827
+ });
828
+ scrollObserver = createScrollObserver(store, viewport, isHorizontal, () => normalizeScrollOffset(viewport[scrollOffsetKey], isNegative), (jump, shift, isMomentumScrolling) => {
816
829
  // If we update scroll position while touching on iOS, the position will be reverted.
817
830
  // However iOS WebKit fires touch events only once at the beginning of momentum scrolling.
818
831
  // That means we have no reliable way to confirm still touched or not if user touches more than once during momentum scrolling...
@@ -827,7 +840,7 @@ const createScroller = (store, isHorizontal) => {
827
840
  }
828
841
  // Use absolute position not to exceed scrollable bounds
829
842
  // https://github.com/inokawa/virtua/discussions/475
830
- viewport[scrollOffsetKey] = normalizeOffset(store.$getScrollOffset() + jump, isHorizontal);
843
+ viewport[scrollOffsetKey] = normalizeScrollOffset(store.$getScrollOffset() + jump, isNegative);
831
844
  if (shift) {
832
845
  // https://github.com/inokawa/virtua/issues/357
833
846
  cancelScroll();
@@ -841,6 +854,7 @@ const createScroller = (store, isHorizontal) => {
841
854
  // https://github.com/inokawa/virtua/pull/765
842
855
  initialized = createPromise();
843
856
  },
857
+ $isNegative: () => isNegative,
844
858
  $scrollTo(offset) {
845
859
  scheduleScroll(() => offset);
846
860
  },
@@ -888,9 +902,10 @@ const createWindowScroller = (store, isHorizontal) => {
888
902
  let containerElement;
889
903
  let scrollObserver;
890
904
  let initialized = createPromise();
905
+ let isNegative = false;
891
906
  const scrollToKey = isHorizontal ? "left" : "top";
892
907
  const [scheduleScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
893
- offset = normalizeOffset(offset, isHorizontal);
908
+ offset = normalizeScrollOffset(offset, isNegative);
894
909
  const window = getCurrentWindow(getCurrentDocument(containerElement));
895
910
  if (smooth) {
896
911
  window.scroll({
@@ -908,7 +923,7 @@ const createWindowScroller = (store, isHorizontal) => {
908
923
  // TODO calc offset only when it changes (maybe impossible)
909
924
  const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
910
925
  const offsetSum = offset +
911
- (isHorizontal && isRTLDocument()
926
+ (isHorizontal && isNegative
912
927
  ? window.innerWidth - node[offsetKey] - node.offsetWidth
913
928
  : node[offsetKey]);
914
929
  const parent = node.offsetParent;
@@ -923,18 +938,23 @@ const createWindowScroller = (store, isHorizontal) => {
923
938
  const scrollOffsetKey = isHorizontal ? "scrollX" : "scrollY";
924
939
  const document = getCurrentDocument(container);
925
940
  const window = getCurrentWindow(document);
926
- scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeOffset(window[scrollOffsetKey], isHorizontal), (jump, shift) => {
941
+ if (isHorizontal) {
942
+ // Detect RTL document
943
+ isNegative =
944
+ getComputedStyle(getDocumentElement(document)).direction === "rtl";
945
+ }
946
+ scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeScrollOffset(window[scrollOffsetKey], isNegative), (jump, shift) => {
927
947
  // TODO support case two window scrollers exist in the same view
928
948
  if (shift) {
929
949
  // Use absolute position not to exceed scrollable bounds
930
950
  window.scroll({
931
- [scrollToKey]: normalizeOffset(store.$getScrollOffset() + jump, isHorizontal),
951
+ [scrollToKey]: normalizeScrollOffset(store.$getScrollOffset() + jump, isNegative),
932
952
  });
933
953
  }
934
954
  else {
935
955
  // Use window.scrollBy here, which causes less layout shift for some reason.
936
956
  window.scrollBy({
937
- [scrollToKey]: normalizeOffset(jump, isHorizontal),
957
+ [scrollToKey]: normalizeScrollOffset(jump, isNegative),
938
958
  });
939
959
  }
940
960
  }, () => calcOffsetToViewport(container, document.body, window, isHorizontal));
@@ -947,6 +967,7 @@ const createWindowScroller = (store, isHorizontal) => {
947
967
  // https://github.com/inokawa/virtua/pull/765
948
968
  initialized = createPromise();
949
969
  },
970
+ $isNegative: () => isNegative,
950
971
  $fixScrollJump: () => {
951
972
  scrollObserver && scrollObserver._fixScrollJump();
952
973
  },
@@ -970,7 +991,7 @@ const createWindowScroller = (store, isHorizontal) => {
970
991
  }
971
992
  const document = getCurrentDocument(containerElement);
972
993
  const window = getCurrentWindow(document);
973
- const html = document.documentElement;
994
+ const html = getDocumentElement(document);
974
995
  const getScrollbarSize = () => store.$getViewportSize() -
975
996
  (isHorizontal ? html.clientWidth : html.clientHeight);
976
997
  scheduleScroll(() => {
@@ -1006,6 +1027,7 @@ const createGridScroller = (rowStore, colStore) => {
1006
1027
  rowScroller.$dispose();
1007
1028
  colScroller.$dispose();
1008
1029
  },
1030
+ $isNegative: colScroller.$isNegative,
1009
1031
  $scrollTo(row, col) {
1010
1032
  if (row != null) {
1011
1033
  rowScroller.$scrollTo(row);
@@ -1266,9 +1288,6 @@ const createGridResizer = (rowStore, colStore) => {
1266
1288
  };
1267
1289
  };
1268
1290
 
1269
- /**
1270
- * @jsxImportSource solid-js
1271
- */
1272
1291
  /**
1273
1292
  * @internal
1274
1293
  */
@@ -1288,7 +1307,7 @@ const ListItem = (props) => {
1288
1307
  position: "absolute",
1289
1308
  [isHorizontal ? "height" : "width"]: "100%",
1290
1309
  [isHorizontal ? "top" : "left"]: "0px",
1291
- [isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]: props._offset + "px",
1310
+ [isHorizontal ? "left" : "top"]: props._offset + "px",
1292
1311
  visibility: props._hide ? "hidden" : undefined,
1293
1312
  };
1294
1313
  if (isHorizontal) {
@@ -1343,6 +1362,7 @@ const Virtualizer = (props) => {
1343
1362
  });
1344
1363
  const isScrolling = createMemo(() => stateVersion() && store.$isScrolling());
1345
1364
  const totalSize = createMemo(() => stateVersion() && store.$getTotalSize());
1365
+ const isNegative = createMemo(() => stateVersion() && scroller.$isNegative());
1346
1366
  onMount(() => {
1347
1367
  if (props.ref) {
1348
1368
  props.ref({
@@ -1393,6 +1413,7 @@ const Virtualizer = (props) => {
1393
1413
  store.$update(ACTION_ITEMS_LENGTH_CHANGE, [count, props.shift]);
1394
1414
  }
1395
1415
  });
1416
+ const pushKey = !horizontal && isNegative() ? "unshift" : "push";
1396
1417
  const items = [];
1397
1418
  const indexes = [];
1398
1419
  if (props.keepMounted) {
@@ -1401,14 +1422,14 @@ const Virtualizer = (props) => {
1401
1422
  mounted.add(i);
1402
1423
  }
1403
1424
  sort([...mounted]).forEach((index) => {
1404
- items.push(props.data[index]);
1405
- indexes.push(index);
1425
+ items[pushKey](props.data[index]);
1426
+ indexes[pushKey](index);
1406
1427
  });
1407
1428
  }
1408
1429
  else {
1409
1430
  for (let [i, j] = range(); i <= j; i++) {
1410
- items.push(props.data[i]);
1411
- indexes.push(i);
1431
+ items[pushKey](props.data[i]);
1432
+ indexes[pushKey](i);
1412
1433
  }
1413
1434
  }
1414
1435
  return { _items: items, _indexes: indexes };
@@ -1416,7 +1437,7 @@ const Virtualizer = (props) => {
1416
1437
  const renderItem = (data, index) => {
1417
1438
  const offset = createMemo(() => {
1418
1439
  stateVersion();
1419
- return store.$getItemOffset(index());
1440
+ return store.$getItemOffset(index(), isNegative());
1420
1441
  });
1421
1442
  const hide = createMemo(() => {
1422
1443
  stateVersion();
@@ -1517,6 +1538,7 @@ const WindowVirtualizer = (props) => {
1517
1538
  });
1518
1539
  const isScrolling = createMemo(() => stateVersion() && store.$isScrolling());
1519
1540
  const totalSize = createMemo(() => stateVersion() && store.$getTotalSize());
1541
+ const isNegative = createMemo(() => stateVersion() && scroller.$isNegative());
1520
1542
  onMount(() => {
1521
1543
  if (props.ref) {
1522
1544
  props.ref({
@@ -1556,9 +1578,10 @@ const WindowVirtualizer = (props) => {
1556
1578
  store.$update(ACTION_ITEMS_LENGTH_CHANGE, [count, props.shift]);
1557
1579
  }
1558
1580
  });
1581
+ const pushKey = !horizontal && isNegative() ? "unshift" : "push";
1559
1582
  const items = [];
1560
1583
  for (let [i, j] = range(); i <= j; i++) {
1561
- items.push(props.data[i]);
1584
+ items[pushKey](props.data[i]);
1562
1585
  }
1563
1586
  return items;
1564
1587
  });
@@ -1576,7 +1599,7 @@ const WindowVirtualizer = (props) => {
1576
1599
  const itemIndex = createMemo(() => range()[0] + index());
1577
1600
  const offset = createMemo(() => {
1578
1601
  stateVersion();
1579
- return store.$getItemOffset(itemIndex());
1602
+ return store.$getItemOffset(itemIndex(), isNegative());
1580
1603
  });
1581
1604
  const hide = createMemo(() => {
1582
1605
  stateVersion();