virtua 0.44.3 → 0.45.1

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.
@@ -292,12 +292,6 @@ const UPDATE_SCROLL_END_EVENT = 0b1000;
292
292
  const getScrollSize = (store) => {
293
293
  return max(store.$getTotalSize(), store.$getViewportSize());
294
294
  };
295
- /**
296
- * @internal
297
- */
298
- const isInitialMeasurementDone = (store) => {
299
- return !!store.$getViewportSize();
300
- };
301
295
  /**
302
296
  * @internal
303
297
  */
@@ -686,69 +680,81 @@ const createScrollObserver = (store, viewport, isHorizontal, getScrollOffset, up
686
680
  },
687
681
  };
688
682
  };
689
- /**
690
- * @internal
691
- */
692
- const createScroller = (store, isHorizontal) => {
693
- let viewportElement;
694
- let scrollObserver;
683
+ const createScrollScheduler = (store, initialized, scroll) => {
695
684
  let cancelScroll;
696
- let initialized = createPromise();
697
- const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
698
- const overflowKey = isHorizontal ? "overflowX" : "overflowY";
699
685
  // The given offset will be clamped by browser
700
686
  // https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
701
- const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
702
- // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
703
- // https://github.com/inokawa/virtua/pull/733
704
- // https://github.com/inokawa/virtua/pull/750
705
- if (!(await initialized[0])) {
706
- return;
707
- }
708
- if (cancelScroll) {
709
- // Cancel waiting scrollTo
710
- cancelScroll();
711
- }
712
- const waitForMeasurement = () => {
713
- // Wait for the scroll destination items to be measured.
714
- // The measurement will be done asynchronously and the timing is not predictable so we use promise.
715
- const [promise, resolve] = createPromise();
716
- cancelScroll = () => {
717
- resolve(false);
718
- };
719
- // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
720
- // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
721
- // https://github.com/inokawa/virtua/issues/450
722
- if (isInitialMeasurementDone(store)) {
723
- // Cancel when items around scroll destination completely measured
724
- timeout(cancelScroll, 150);
687
+ return [
688
+ async (getTargetOffset, smooth) => {
689
+ // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
690
+ // https://github.com/inokawa/virtua/pull/733
691
+ // https://github.com/inokawa/virtua/pull/750
692
+ if (!(await initialized())) {
693
+ return;
725
694
  }
726
- return [
727
- promise,
728
- store.$subscribe(UPDATE_SIZE_EVENT, () => {
729
- resolve(true);
730
- }),
731
- ];
732
- };
733
- if (smooth && isSmoothScrollSupported()) {
734
- store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
735
- // https://github.com/inokawa/virtua/issues/590
736
- microtask(async () => {
737
- while (true) {
738
- let done = true;
739
- for (let [i, end] = store.$getRange(); i <= end; i++) {
740
- if (store.$isUnmeasuredItem(i)) {
741
- done = false;
695
+ if (cancelScroll) {
696
+ // Cancel waiting scrollTo
697
+ cancelScroll();
698
+ }
699
+ const waitForMeasurement = () => {
700
+ // Wait for the scroll destination items to be measured.
701
+ // The measurement will be done asynchronously and the timing is not predictable so we use promise.
702
+ const [promise, resolve] = createPromise();
703
+ cancelScroll = () => {
704
+ resolve(false);
705
+ };
706
+ // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
707
+ // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
708
+ // https://github.com/inokawa/virtua/issues/450
709
+ if (store.$getViewportSize()) {
710
+ // Cancel when items around scroll destination completely measured
711
+ timeout(cancelScroll, 150);
712
+ }
713
+ return [
714
+ promise,
715
+ store.$subscribe(UPDATE_SIZE_EVENT, () => {
716
+ resolve(true);
717
+ }),
718
+ ];
719
+ };
720
+ if (smooth && isSmoothScrollSupported()) {
721
+ store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
722
+ // https://github.com/inokawa/virtua/issues/590
723
+ microtask(async () => {
724
+ while (true) {
725
+ let done = true;
726
+ for (let [i, end] = store.$getRange(); i <= end; i++) {
727
+ if (store.$isUnmeasuredItem(i)) {
728
+ done = false;
729
+ break;
730
+ }
731
+ }
732
+ if (done) {
742
733
  break;
743
734
  }
735
+ const [promise, unsubscribe] = waitForMeasurement();
736
+ try {
737
+ if (!(await promise)) {
738
+ // canceled
739
+ return;
740
+ }
741
+ }
742
+ finally {
743
+ unsubscribe();
744
+ }
744
745
  }
745
- if (done) {
746
- break;
747
- }
746
+ store.$update(ACTION_MANUAL_SCROLL);
747
+ scroll(getTargetOffset(), smooth);
748
+ });
749
+ }
750
+ else {
751
+ while (true) {
748
752
  const [promise, unsubscribe] = waitForMeasurement();
749
753
  try {
754
+ store.$update(ACTION_MANUAL_SCROLL);
755
+ scroll(getTargetOffset());
750
756
  if (!(await promise)) {
751
- // canceled
757
+ // canceled or finished
752
758
  return;
753
759
  }
754
760
  }
@@ -756,30 +762,34 @@ const createScroller = (store, isHorizontal) => {
756
762
  unsubscribe();
757
763
  }
758
764
  }
759
- store.$update(ACTION_MANUAL_SCROLL);
760
- viewportElement.scrollTo({
761
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
762
- behavior: "smooth",
763
- });
765
+ }
766
+ },
767
+ () => {
768
+ cancelScroll && cancelScroll();
769
+ },
770
+ ];
771
+ };
772
+ /**
773
+ * @internal
774
+ */
775
+ const createScroller = (store, isHorizontal) => {
776
+ let viewportElement;
777
+ let scrollObserver;
778
+ let initialized = createPromise();
779
+ const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
780
+ const overflowKey = isHorizontal ? "overflowX" : "overflowY";
781
+ const [scheduleScroll, cancelScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
782
+ offset = normalizeOffset(offset, isHorizontal);
783
+ if (smooth) {
784
+ viewportElement.scrollTo({
785
+ [isHorizontal ? "left" : "top"]: offset,
786
+ behavior: "smooth",
764
787
  });
765
788
  }
766
789
  else {
767
- while (true) {
768
- const [promise, unsubscribe] = waitForMeasurement();
769
- try {
770
- store.$update(ACTION_MANUAL_SCROLL);
771
- viewportElement[scrollOffsetKey] = normalizeOffset(getTargetOffset(), isHorizontal);
772
- if (!(await promise)) {
773
- // canceled or finished
774
- return;
775
- }
776
- }
777
- finally {
778
- unsubscribe();
779
- }
780
- }
790
+ viewportElement[scrollOffsetKey] = offset;
781
791
  }
782
- };
792
+ });
783
793
  return {
784
794
  $observe(viewport) {
785
795
  viewportElement = viewport;
@@ -801,7 +811,7 @@ const createScroller = (store, isHorizontal) => {
801
811
  viewport[scrollOffsetKey] = store.$getScrollOffset() + jump;
802
812
  if (shift) {
803
813
  // https://github.com/inokawa/virtua/issues/357
804
- cancelScroll && cancelScroll();
814
+ cancelScroll();
805
815
  }
806
816
  });
807
817
  initialized[1](true);
@@ -813,11 +823,11 @@ const createScroller = (store, isHorizontal) => {
813
823
  initialized = createPromise();
814
824
  },
815
825
  $scrollTo(offset) {
816
- scheduleImperativeScroll(() => offset);
826
+ scheduleScroll(() => offset);
817
827
  },
818
828
  $scrollBy(offset) {
819
829
  offset += store.$getScrollOffset();
820
- scheduleImperativeScroll(() => offset);
830
+ scheduleScroll(() => offset);
821
831
  },
822
832
  $scrollToIndex(index, { align, smooth, offset = 0 } = {}) {
823
833
  index = clamp(index, 0, store.$getItemsLength() - 1);
@@ -836,7 +846,7 @@ const createScroller = (store, isHorizontal) => {
836
846
  return;
837
847
  }
838
848
  }
839
- scheduleImperativeScroll(() => {
849
+ scheduleScroll(() => {
840
850
  return (offset +
841
851
  store.$getStartSpacerSize() +
842
852
  store.$getItemOffset(index) +
@@ -858,8 +868,23 @@ const createScroller = (store, isHorizontal) => {
858
868
  const createWindowScroller = (store, isHorizontal) => {
859
869
  let containerElement;
860
870
  let scrollObserver;
861
- let cancelScroll;
862
871
  let initialized = createPromise();
872
+ const scrollToKey = isHorizontal ? "left" : "top";
873
+ const [scheduleScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
874
+ offset = normalizeOffset(offset, isHorizontal);
875
+ const window = getCurrentWindow(getCurrentDocument(containerElement));
876
+ if (smooth) {
877
+ window.scroll({
878
+ [scrollToKey]: offset,
879
+ behavior: "smooth",
880
+ });
881
+ }
882
+ else {
883
+ window.scroll({
884
+ [scrollToKey]: offset,
885
+ });
886
+ }
887
+ });
863
888
  const calcOffsetToViewport = (node, viewport, window, isHorizontal, offset = 0) => {
864
889
  // TODO calc offset only when it changes (maybe impossible)
865
890
  const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
@@ -873,104 +898,20 @@ const createWindowScroller = (store, isHorizontal) => {
873
898
  }
874
899
  return calcOffsetToViewport(parent, viewport, window, isHorizontal, offsetSum);
875
900
  };
876
- const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
877
- // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
878
- // https://github.com/inokawa/virtua/pull/733
879
- // https://github.com/inokawa/virtua/pull/750
880
- if (!(await initialized[0])) {
881
- return;
882
- }
883
- if (cancelScroll) {
884
- cancelScroll();
885
- }
886
- const waitForMeasurement = () => {
887
- // Wait for the scroll destination items to be measured.
888
- // The measurement will be done asynchronously and the timing is not predictable so we use promise.
889
- const [promise, resolve] = createPromise();
890
- cancelScroll = () => {
891
- resolve(false);
892
- };
893
- // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
894
- // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
895
- // https://github.com/inokawa/virtua/issues/450
896
- if (isInitialMeasurementDone(store)) {
897
- // Cancel when items around scroll destination completely measured
898
- timeout(cancelScroll, 150);
899
- }
900
- return [
901
- promise,
902
- store.$subscribe(UPDATE_SIZE_EVENT, () => {
903
- resolve(true);
904
- }),
905
- ];
906
- };
907
- const window = getCurrentWindow(getCurrentDocument(containerElement));
908
- if (smooth && isSmoothScrollSupported()) {
909
- store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
910
- // https://github.com/inokawa/virtua/issues/590
911
- microtask(async () => {
912
- while (true) {
913
- let done = true;
914
- for (let [i, end] = store.$getRange(); i <= end; i++) {
915
- if (store.$isUnmeasuredItem(i)) {
916
- done = false;
917
- break;
918
- }
919
- }
920
- if (done) {
921
- break;
922
- }
923
- const [promise, unsubscribe] = waitForMeasurement();
924
- try {
925
- if (!(await promise)) {
926
- // canceled
927
- return;
928
- }
929
- }
930
- finally {
931
- unsubscribe();
932
- }
933
- }
934
- store.$update(ACTION_MANUAL_SCROLL);
935
- window.scroll({
936
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
937
- behavior: "smooth",
938
- });
939
- });
940
- }
941
- else {
942
- while (true) {
943
- const [promise, unsubscribe] = waitForMeasurement();
944
- try {
945
- store.$update(ACTION_MANUAL_SCROLL);
946
- window.scroll({
947
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
948
- });
949
- if (!(await promise)) {
950
- return;
951
- }
952
- }
953
- finally {
954
- unsubscribe();
955
- }
956
- }
957
- }
958
- };
959
901
  return {
960
902
  $observe(container) {
961
903
  containerElement = container;
962
904
  const scrollOffsetKey = isHorizontal ? "scrollX" : "scrollY";
963
905
  const document = getCurrentDocument(container);
964
906
  const window = getCurrentWindow(document);
965
- const documentBody = document.body;
966
907
  scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeOffset(window[scrollOffsetKey], isHorizontal), (jump) => {
967
908
  // Use absolute position not to exceed scrollable bounds
968
909
  // https://github.com/inokawa/virtua/discussions/475
969
910
  // TODO support case two window scrollers exist in the same view
970
911
  window.scroll({
971
- [isHorizontal ? "left" : "top"]: store.$getScrollOffset() + jump,
912
+ [scrollToKey]: store.$getScrollOffset() + jump,
972
913
  });
973
- }, () => calcOffsetToViewport(container, documentBody, window, isHorizontal));
914
+ }, () => calcOffsetToViewport(container, document.body, window, isHorizontal));
974
915
  initialized[1](true);
975
916
  },
976
917
  $dispose() {
@@ -1006,7 +947,7 @@ const createWindowScroller = (store, isHorizontal) => {
1006
947
  const html = document.documentElement;
1007
948
  const getScrollbarSize = () => store.$getViewportSize() -
1008
949
  (isHorizontal ? html.clientWidth : html.clientHeight);
1009
- scheduleImperativeScroll(() => {
950
+ scheduleScroll(() => {
1010
951
  return (offset +
1011
952
  // Calculate target scroll position including container's offset from document
1012
953
  calcOffsetToViewport(containerElement, document.body, window, isHorizontal) +
@@ -1027,9 +968,9 @@ const createWindowScroller = (store, isHorizontal) => {
1027
968
  /**
1028
969
  * @internal
1029
970
  */
1030
- const createGridScroller = (vStore, hStore) => {
1031
- const vScroller = createScroller(vStore, false);
1032
- const hScroller = createScroller(hStore, true);
971
+ const createGridScroller = (rowStore, colStore) => {
972
+ const vScroller = createScroller(rowStore, false);
973
+ const hScroller = createScroller(colStore, true);
1033
974
  return {
1034
975
  $observe(viewportElement) {
1035
976
  vScroller.$observe(viewportElement);
@@ -1170,10 +1111,8 @@ const createWindowResizer = (store, isHorizontal) => {
1170
1111
  /**
1171
1112
  * @internal
1172
1113
  */
1173
- const createGridResizer = (vStore, hStore) => {
1114
+ const createGridResizer = (rowStore, colStore) => {
1174
1115
  let viewportElement;
1175
- const heightKey = "height";
1176
- const widthKey = "width";
1177
1116
  const mountedIndexes = new WeakMap();
1178
1117
  const maybeCachedRowIndexes = new Set();
1179
1118
  const maybeCachedColIndexes = new Set();
@@ -1182,13 +1121,13 @@ const createGridResizer = (vStore, hStore) => {
1182
1121
  const resizeObserver = createResizeObserver((entries) => {
1183
1122
  const resizedRows = new Set();
1184
1123
  const resizedCols = new Set();
1185
- for (const { target, contentRect } of entries) {
1124
+ for (const { target, contentRect: { width, height }, } of entries) {
1186
1125
  // Skip zero-sized rects that may be observed under `display: none` style
1187
1126
  if (!target.offsetParent)
1188
1127
  continue;
1189
1128
  if (target === viewportElement) {
1190
- vStore.$update(ACTION_VIEWPORT_RESIZE, contentRect[heightKey]);
1191
- hStore.$update(ACTION_VIEWPORT_RESIZE, contentRect[widthKey]);
1129
+ rowStore.$update(ACTION_VIEWPORT_RESIZE, height);
1130
+ colStore.$update(ACTION_VIEWPORT_RESIZE, width);
1192
1131
  }
1193
1132
  else {
1194
1133
  const cell = mountedIndexes.get(target);
@@ -1196,20 +1135,16 @@ const createGridResizer = (vStore, hStore) => {
1196
1135
  const [rowIndex, colIndex] = cell;
1197
1136
  const key = getKey(rowIndex, colIndex);
1198
1137
  const prevSize = sizeCache.get(key);
1199
- const size = [
1200
- contentRect[heightKey],
1201
- contentRect[widthKey],
1202
- ];
1203
1138
  let rowResized;
1204
1139
  let colResized;
1205
1140
  if (!prevSize) {
1206
1141
  rowResized = colResized = true;
1207
1142
  }
1208
1143
  else {
1209
- if (prevSize[0] !== size[0]) {
1144
+ if (prevSize[0] !== height) {
1210
1145
  rowResized = true;
1211
1146
  }
1212
- if (prevSize[1] !== size[1]) {
1147
+ if (prevSize[1] !== width) {
1213
1148
  colResized = true;
1214
1149
  }
1215
1150
  }
@@ -1220,7 +1155,7 @@ const createGridResizer = (vStore, hStore) => {
1220
1155
  resizedCols.add(colIndex);
1221
1156
  }
1222
1157
  if (rowResized || colResized) {
1223
- sizeCache.set(key, size);
1158
+ sizeCache.set(key, [height, width]);
1224
1159
  }
1225
1160
  }
1226
1161
  }
@@ -1239,7 +1174,7 @@ const createGridResizer = (vStore, hStore) => {
1239
1174
  heightResizes.push([rowIndex, maxHeight]);
1240
1175
  }
1241
1176
  });
1242
- vStore.$update(ACTION_ITEM_RESIZE, heightResizes);
1177
+ rowStore.$update(ACTION_ITEM_RESIZE, heightResizes);
1243
1178
  }
1244
1179
  if (resizedCols.size) {
1245
1180
  const widthResizes = [];
@@ -1255,7 +1190,7 @@ const createGridResizer = (vStore, hStore) => {
1255
1190
  widthResizes.push([colIndex, maxWidth]);
1256
1191
  }
1257
1192
  });
1258
- hStore.$update(ACTION_ITEM_RESIZE, widthResizes);
1193
+ colStore.$update(ACTION_ITEM_RESIZE, widthResizes);
1259
1194
  }
1260
1195
  });
1261
1196
  return {
@@ -1274,19 +1209,19 @@ const createGridResizer = (vStore, hStore) => {
1274
1209
  },
1275
1210
  $resizeCols(cols) {
1276
1211
  for (const [c] of cols) {
1277
- for (let r = 0; r < vStore.$getItemsLength(); r++) {
1212
+ for (let r = 0; r < rowStore.$getItemsLength(); r++) {
1278
1213
  sizeCache.delete(getKey(r, c));
1279
1214
  }
1280
1215
  }
1281
- hStore.$update(ACTION_ITEM_RESIZE, cols);
1216
+ colStore.$update(ACTION_ITEM_RESIZE, cols);
1282
1217
  },
1283
1218
  $resizeRows(rows) {
1284
1219
  for (const [r] of rows) {
1285
- for (let c = 0; c < hStore.$getItemsLength(); c++) {
1220
+ for (let c = 0; c < colStore.$getItemsLength(); c++) {
1286
1221
  sizeCache.delete(getKey(r, c));
1287
1222
  }
1288
1223
  }
1289
- vStore.$update(ACTION_ITEM_RESIZE, rows);
1224
+ rowStore.$update(ACTION_ITEM_RESIZE, rows);
1290
1225
  },
1291
1226
  $dispose: resizeObserver._dispose,
1292
1227
  };