@jsenv/navi 0.29.408 → 0.29.410

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.
@@ -13145,12 +13145,13 @@ const useBoundAction = (action, actionParamsSignal) => {
13145
13145
  if (existingNoopAction) {
13146
13146
  return existingNoopAction;
13147
13147
  }
13148
+ // Not bound to the params signal: nothing reads what a noop runs with, and
13149
+ // a proxy following the signal would retarget (a new child action for each
13150
+ // new value) at every change — for every button of a form, at every change
13151
+ // of any of its fields.
13148
13152
  const noopAction = createAction(() => {}, { params: undefined });
13149
- const noopActionBound = actionParamsSignal
13150
- ? noopAction.bindParams(actionParamsSignal)
13151
- : noopAction;
13152
- noopActionRef.current = noopActionBound;
13153
- return noopActionBound;
13153
+ noopActionRef.current = noopAction;
13154
+ return noopAction;
13154
13155
  }
13155
13156
  const isFunction = typeof action === "function";
13156
13157
  if (!isFunction) {
@@ -13919,7 +13920,9 @@ const createControlValidation = (
13919
13920
 
13920
13921
  let newConstraintValidityState = { valid: true };
13921
13922
  const constraintSet = getConstraintSet();
13922
- const elementSig = getElementSignature(controller.ref.current);
13923
+ // Only for the debug lines of a failure or a change: computed when one of
13924
+ // them is written, not at every check (every notch of a wheel).
13925
+ const elementSig = () => getElementSignature(controller.ref.current);
13923
13926
  // Not logged: every control checks its constraints on every interaction and
13924
13927
  // almost always passes, so this line alone was most of the debug output —
13925
13928
  // and with devtools open, formatting it costs a frame. What matters is a
@@ -13945,7 +13948,7 @@ const createControlValidation = (
13945
13948
  constraintValidityInfo.message,
13946
13949
  );
13947
13950
  debugUIState(
13948
- `${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13951
+ `${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13949
13952
  );
13950
13953
  const thisConstraintFailureInfo = {
13951
13954
  name: constraint.name,
@@ -14017,7 +14020,7 @@ const createControlValidation = (
14017
14020
  if (element) {
14018
14021
  debugUIState(
14019
14022
  event,
14020
- `${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
14023
+ `${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
14021
14024
  );
14022
14025
  dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
14023
14026
  }
@@ -14063,7 +14066,7 @@ const createControlValidation = (
14063
14066
  event,
14064
14067
  { report = false, fromRequestAction = false } = {},
14065
14068
  ) => {
14066
- const elementSig = getElementSignature(controller.ref.current);
14069
+ const elementSig = () => getElementSignature(controller.ref.current);
14067
14070
  const isValid = checkValidity({ event, fromRequestAction });
14068
14071
  if (failingManagedControlValidity) {
14069
14072
  // Group/form case: find the actual failing leaf and report on it.
@@ -14085,7 +14088,7 @@ const createControlValidation = (
14085
14088
  if (report) {
14086
14089
  debugUIState(
14087
14090
  event,
14088
- `syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
14091
+ `syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
14089
14092
  );
14090
14093
  reportValidity({ event });
14091
14094
  } else if (failedConstraintInfo.status === "error") {
@@ -14095,12 +14098,12 @@ const createControlValidation = (
14095
14098
  // on the next action attempt via autoResetOnAction.
14096
14099
  debugUIState(
14097
14100
  event,
14098
- `syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
14101
+ `syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
14099
14102
  );
14100
14103
  } else {
14101
14104
  debugUIState(
14102
14105
  event,
14103
- `syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
14106
+ `syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
14104
14107
  );
14105
14108
  callout.removeOpenToken(VALIDATION_TOKEN, event);
14106
14109
  }
@@ -28087,6 +28090,7 @@ This prevents cross-test pollution and ensures clean state.`,
28087
28090
 
28088
28091
  // Store previous route states to detect changes
28089
28092
  const routePreviousStateMap = new WeakMap();
28093
+ let previousPath = null;
28090
28094
  const updateRoutes = (
28091
28095
  url,
28092
28096
  {
@@ -28095,6 +28099,14 @@ This prevents cross-test pollution and ensures clean state.`,
28095
28099
  } = {},
28096
28100
  ) => {
28097
28101
  currentUrl = url;
28102
+ // Whether a route matches is decided by the path alone: the search string
28103
+ // only fills the params (see matchUrl). An address that keeps its path — a
28104
+ // search param written, at every notch of a wheel bound to it — cannot make
28105
+ // a route start or stop matching, so only the routes already matching are
28106
+ // read again.
28107
+ const path = urlWithoutSearchAndHash(url);
28108
+ const pathKept = path === previousPath;
28109
+ previousPath = path;
28098
28110
  const returnValue = {};
28099
28111
  const routeMatchInfoSet = new Set();
28100
28112
  for (const route of routeSet) {
@@ -28110,7 +28122,8 @@ This prevents cross-test pollution and ensures clean state.`,
28110
28122
  const oldParams = previousState.params;
28111
28123
  const oldActionParams = previousState.actionParams;
28112
28124
 
28113
- let extractedParams = routePattern.applyOn(url);
28125
+ let extractedParams =
28126
+ pathKept && !oldMatching ? null : routePattern.applyOn(url);
28114
28127
  let newMatching = Boolean(extractedParams);
28115
28128
  let newParams;
28116
28129
  if (extractedParams) {
@@ -28179,6 +28192,25 @@ This prevents cross-test pollution and ensures clean state.`,
28179
28192
  }
28180
28193
  }
28181
28194
 
28195
+ // What the matching routes say, read once for every connection of every
28196
+ // route that does not match (below), rather than by walking all the
28197
+ // routes again for each of those connections.
28198
+ const matchingRawParamsArray = [];
28199
+ const paramNameDeclaredByMatchingRouteSet = new Set();
28200
+ const matchingFamilyRootSet = new Set();
28201
+ for (const matchingRoute of matchingRouteSet) {
28202
+ matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
28203
+ const matchingPattern =
28204
+ getRoutePrivateProperties(matchingRoute).routePattern;
28205
+ for (const paramName of matchingPattern.queryConnectionMap.keys()) {
28206
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
28207
+ }
28208
+ for (const paramName of matchingPattern.pathConnectionMap.keys()) {
28209
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
28210
+ }
28211
+ matchingFamilyRootSet.add(matchingPattern.familyRoot);
28212
+ }
28213
+
28182
28214
  for (const {
28183
28215
  route,
28184
28216
  routePrivateProperties,
@@ -28194,40 +28226,21 @@ This prevents cross-test pollution and ensures clean state.`,
28194
28226
  const urlParamValue = rawParams[paramName];
28195
28227
 
28196
28228
  if (!newMatching) {
28197
- // Route doesn't match - check if any matching route extracts this parameter
28198
- let parameterExtractedByMatchingRoute = false;
28199
- let parameterDeclaredByMatchingRoute = false;
28200
- let matchingRouteInSameFamily = false;
28201
-
28202
- for (const otherRoute of routeSet) {
28203
- if (otherRoute === route || !otherRoute.matching) {
28204
- continue;
28205
- }
28206
- const otherRawParams = otherRoute.rawParamsSignal.value;
28207
-
28208
- // Check if this matching route extracts the parameter
28209
- if (paramName in otherRawParams) {
28210
- parameterExtractedByMatchingRoute = true;
28211
- }
28212
-
28213
- // Same param, on the page one is arriving at: what it is worth
28214
- // there is that page's business, url or no url — it may open on
28215
- // a default of its own (see searchParams' `{ signal, default }`).
28216
- const otherPattern =
28217
- getRoutePrivateProperties(otherRoute).routePattern;
28218
- if (
28219
- otherPattern.queryConnectionMap.has(paramName) ||
28220
- otherPattern.pathConnectionMap.has(paramName)
28221
- ) {
28222
- parameterDeclaredByMatchingRoute = true;
28223
- }
28224
-
28225
- // Same family = same topmost ancestor
28226
- // (familyRoot, computed in setupRoutePatterns)
28227
- if (otherPattern.familyRoot === routePattern.familyRoot) {
28228
- matchingRouteInSameFamily = true;
28229
- }
28230
- }
28229
+ // Whether a matching route extracts this parameter from the url
28230
+ const parameterExtractedByMatchingRoute =
28231
+ matchingRawParamsArray.some(
28232
+ (matchingRawParams) => paramName in matchingRawParams,
28233
+ );
28234
+ // Same param, on the page one is arriving at: what it is worth
28235
+ // there is that page's business, url or no url — it may open on
28236
+ // a default of its own (see searchParams' `{ signal, default }`).
28237
+ const parameterDeclaredByMatchingRoute =
28238
+ paramNameDeclaredByMatchingRouteSet.has(paramName);
28239
+ // Same family = same topmost ancestor
28240
+ // (familyRoot, computed in setupRoutePatterns)
28241
+ const matchingRouteInSameFamily = matchingFamilyRootSet.has(
28242
+ routePattern.familyRoot,
28243
+ );
28231
28244
 
28232
28245
  // A weak param qualifies one visit: leaving the route ends it,
28233
28246
  // whatever the family and whatever the default. Coming back by a
@@ -28479,6 +28492,11 @@ const getParamsCacheKey = (params) => {
28479
28492
  return key;
28480
28493
  };
28481
28494
 
28495
+ const urlWithoutSearchAndHash = (url) => {
28496
+ const urlObject = new URL(url);
28497
+ return `${urlObject.origin}${urlObject.pathname}`;
28498
+ };
28499
+
28482
28500
  /**
28483
28501
  * Is there an entry of THIS document behind the current one? And ahead of it?
28484
28502
  *
@@ -28922,7 +28940,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
28922
28940
  }
28923
28941
  };
28924
28942
 
28925
- const getHrefTargetInfo = (href) => {
28943
+ // `currentUrlString`: the address the href is compared with — the document's by
28944
+ // default. A caller subscribed to documentUrlSignal passes the value it read,
28945
+ // which is ahead of window.location while a debounced write is pending.
28946
+ const getHrefTargetInfo = (
28947
+ href,
28948
+ currentUrlString = window.location.href,
28949
+ ) => {
28926
28950
  href = String(href);
28927
28951
 
28928
28952
  if (!href || href.trim() === "") {
@@ -28935,8 +28959,8 @@ const getHrefTargetInfo = (href) => {
28935
28959
  };
28936
28960
  }
28937
28961
 
28938
- const currentUrl = new URL(window.location.href);
28939
- const targetUrl = new URL(href, window.location.href);
28962
+ const currentUrl = new URL(currentUrlString);
28963
+ const targetUrl = new URL(href, currentUrlString);
28940
28964
 
28941
28965
  let isCurrent = false;
28942
28966
  {
@@ -28978,6 +29002,21 @@ const getHrefTargetInfo = (href) => {
28978
29002
  };
28979
29003
  };
28980
29004
 
29005
+ // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
29006
+ // isAnchor), read through a computed of that one boolean: the caller re-renders
29007
+ // when it flips, not at every address write — a search param bound to a wheel
29008
+ // writes the address at every notch.
29009
+ const useHrefTargetFlag = (href, flagName) => {
29010
+ const flagSignal = useMemo(
29011
+ () =>
29012
+ computed(
29013
+ () => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
29014
+ ),
29015
+ [href, flagName],
29016
+ );
29017
+ return flagSignal.value;
29018
+ };
29019
+
28981
29020
  /*
28982
29021
  * An app can ship more than one document — an admin panel, a status page, an
28983
29022
  * entry point of its own — and a link to one of them is not a route, it is a
@@ -47869,19 +47908,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
47869
47908
 
47870
47909
  /**
47871
47910
  * Hook that reactively checks if a URL is visited.
47872
- * Re-renders when the visited URL set changes.
47911
+ * Re-renders when that answer changes — not at every url added to the visited
47912
+ * set, which grows at every new address (a search param bound to a wheel
47913
+ * writes one at every notch).
47873
47914
  *
47874
47915
  * @param {string} url - The URL to check
47875
47916
  * @returns {boolean} Whether the URL has been visited
47876
47917
  */
47877
47918
  const useIsVisited = (url) => {
47878
- return useMemo(() => {
47879
- // Access the signal to create reactive dependency
47880
- // eslint-disable-next-line no-unused-expressions
47881
- visitedUrlsSignal.value;
47882
-
47883
- return isVisited(url);
47884
- }, [url, visitedUrlsSignal.value]);
47919
+ const visitedSignal = useMemo(
47920
+ () =>
47921
+ computed(() => {
47922
+ // Both are what the answer depends on: the set, and the address a
47923
+ // relative url is resolved against (see isVisited).
47924
+ // eslint-disable-next-line no-unused-expressions
47925
+ visitedUrlsSignal.value;
47926
+ // eslint-disable-next-line no-unused-expressions
47927
+ documentUrlSignal.value;
47928
+ return isVisited(url);
47929
+ }),
47930
+ [url],
47931
+ );
47932
+ return visitedSignal.value;
47885
47933
  };
47886
47934
 
47887
47935
  /**
@@ -48550,13 +48598,14 @@ const LinkPlain = props => {
48550
48598
  const loading = basePseudoState[":-navi-loading"];
48551
48599
  const shouldDimColor = readOnly || disabled;
48552
48600
  useDimColorWhen(props.ref, shouldDimColor);
48553
- // subscribe to document url to re-render and re-compute getHrefTargetInfo
48554
- useDocumentUrl();
48601
+ // Re-rendered when one of these flips, not at every address write (see
48602
+ // useHrefTargetFlag). Whether the href leaves the site does not depend on
48603
+ // the address.
48604
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
48605
+ const isAnchor = useHrefTargetFlag(href, "isAnchor");
48555
48606
  const {
48556
- isSameSite,
48557
- isAnchor,
48558
- isCurrent
48559
- } = getHrefTargetInfo(href);
48607
+ isSameSite
48608
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
48560
48609
  // A tab that is a SLIDE is current when the container is on it — which the
48561
48610
  // <Nav> around reads off that container, so nothing here has to be told.
48562
48611
  const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
@@ -53522,10 +53571,9 @@ const ButtonHrefResolver = props => {
53522
53571
  });
53523
53572
  };
53524
53573
 
53525
- // What the href means next to the page one is on depends on the document url,
53526
- // so a button with an href re-renders at every address write. One without
53527
- // never reaches here: it has nothing to re-compute, and every button on screen
53528
- // would otherwise pay for each write.
53574
+ // Whether the href is the page one is on depends on the address, and re-renders
53575
+ // the button only when it flips (see useHrefTargetFlag). Whether it leaves the
53576
+ // site does not depend on the address.
53529
53577
  const ButtonWithHref = props => {
53530
53578
  const Next = useNextResolver();
53531
53579
  const {
@@ -53534,12 +53582,10 @@ const ButtonWithHref = props => {
53534
53582
  rel,
53535
53583
  pseudoState
53536
53584
  } = props;
53537
- // Read for the subscription: getHrefTargetInfo reads window.location.
53538
- useDocumentUrl();
53585
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
53539
53586
  const {
53540
- isSameSite,
53541
- isCurrent
53542
- } = getHrefTargetInfo(href);
53587
+ isSameSite
53588
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
53543
53589
  return jsx(Next, {
53544
53590
  ...props,
53545
53591
  target: target === undefined ? isSameSite ? undefined : "_blank" : target,
@@ -83110,7 +83156,6 @@ const useWheelInteractions = ({
83110
83156
  attemptInteraction,
83111
83157
  cancelAnim,
83112
83158
  setPos,
83113
- releaseScrub,
83114
83159
  snapPosToRow,
83115
83160
  getItemSize,
83116
83161
  viewportMain,
@@ -83519,8 +83564,6 @@ const useWheelInteractions = ({
83519
83564
  clearTimeout(settleTimer);
83520
83565
  stopClaimingGesture();
83521
83566
  cancelAnim();
83522
- // Nothing is left to settle the scrub in progress.
83523
- releaseScrub();
83524
83567
  document.removeEventListener("pointerdown", onDocumentPointerDown, {
83525
83568
  capture: true
83526
83569
  });
@@ -83686,11 +83729,11 @@ function WheelUI(props) {
83686
83729
  ...props,
83687
83730
  ref: inputRef,
83688
83731
  type: "navi_js",
83689
- // An arrow or a tap changes the value at once while the row glides into
83690
- // place, and the action must wait for the wheel to come to rest. "custom"
83691
- // stops setUIState from auto-firing the action on every value change;
83692
- // commitSelection dispatches it explicitly on settle. A caller can still
83693
- // override actionEvent.
83732
+ // uiAction fires live (every center-crossing, via setPos live reports);
83733
+ // the committed action must fire only when the wheel stabilizes. "custom"
83734
+ // stops setUIState from auto-firing the action on every value change, so
83735
+ // the live reports are uiAction-only; commitSelection then dispatches the
83736
+ // action explicitly on settle. A caller can still override actionEvent.
83694
83737
  actionEvent: props.actionEvent ?? "custom"
83695
83738
  }, {
83696
83739
  controlType: "input"
@@ -83786,11 +83829,6 @@ function WheelUI(props) {
83786
83829
  // while our own scroll-driven selection does not scroll a second time. Holds the
83787
83830
  // TARGET so rapid inputs step from it and accumulate. null = nothing pending.
83788
83831
  const centeredIndexRef = useRef(null);
83789
- // A user scrub moved the wheel onto a row that is not the value yet: set at
83790
- // the first row it crosses, cleared when it settles (commitSelection), when
83791
- // an arrow or a tap sets a value at once (glideToIndex), or when it is
83792
- // dropped (releaseScrub). See setPos.
83793
- const scrubHoldsRowRef = useRef(false);
83794
83832
  // The wheel does NOT use native scroll. `posRef` is our own virtual scroll
83795
83833
  // position (px, main axis); the list track is translated by -pos. This is the
83796
83834
  // single source of truth, wrapped modulo the real-list extent in loop mode, so
@@ -83979,14 +84017,12 @@ function WheelUI(props) {
83979
84017
  posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
83980
84018
  };
83981
84019
 
83982
- // `live` marks a user scrub (drag, momentum, wheel scroll). A scrub moves the
83983
- // drawing, not the value: the row it reaches is held by the wheel, and only
83984
- // commitSelection turns it into the value, once the motion settles. Reported
83985
- // at each crossing, every notch would be a value change for all that is
83986
- // bound to the wheel — its group, its form, a signal, the address, which
83987
- // records where a value was put down, not the path the finger took
83988
- // (navigation.md, "A value written per frame"). A programmatic move
83989
- // (centerOnIndex) omits `live`: it places the value the wheel already has.
84020
+ // `live` (user scrubs: drag, momentum, wheel scroll) reports the value as each
84021
+ // item crosses the center, so uiAction tracks the wheel in real time — like
84022
+ // every other control. The committed action still fires only once it settles
84023
+ // (commitSelection). A programmatic move (centerOnIndex on an external value
84024
+ // sync) omits it, so scrolling the wheel into place doesn't echo the value
84025
+ // back out.
83990
84026
  const setPos = (vp, pos, {
83991
84027
  live = false
83992
84028
  } = {}) => {
@@ -83997,25 +84033,18 @@ function WheelUI(props) {
83997
84033
  renderPos(vp);
83998
84034
  if (live && interactive) {
83999
84035
  const index = centeredIndex(vp);
84000
- if (index !== centeredIndexRef.current && trackedItemsRef.current[index]) {
84001
- // In step with the scrub so the next arrow or tap steps from the row
84002
- // the wheel is on (see currentTargetIndex).
84003
- centeredIndexRef.current = index;
84004
- scrubHoldsRowRef.current = true;
84036
+ if (index !== centeredIndexRef.current) {
84037
+ const item = trackedItemsRef.current[index];
84038
+ if (item) {
84039
+ // Keep centeredIndexRef in step so the every-render
84040
+ // syncCenterToSelection doesn't read this as an external change and
84041
+ // fight the in-progress motion.
84042
+ centeredIndexRef.current = index;
84043
+ requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
84044
+ }
84005
84045
  }
84006
84046
  }
84007
84047
  };
84008
- // The scrub was dropped before it settled (the wheel stopped being
84009
- // interactive, or its input bindings were torn down): the row it held never
84010
- // became the value. Forgetting it lets the next sync put the wheel back on
84011
- // the value.
84012
- const releaseScrub = () => {
84013
- if (!scrubHoldsRowRef.current) {
84014
- return;
84015
- }
84016
- scrubHoldsRowRef.current = false;
84017
- centeredIndexRef.current = null;
84018
- };
84019
84048
 
84020
84049
  // Returns whether it actually interrupted a motion in flight — lets the caller
84021
84050
  // tell a fresh gesture from one that "caught" a spinning/gliding wheel.
@@ -84051,7 +84080,6 @@ function WheelUI(props) {
84051
84080
  renderPos(vp);
84052
84081
  }
84053
84082
  centeredIndexRef.current = index;
84054
- scrubHoldsRowRef.current = false;
84055
84083
  if (!interactive || glideFromOutsideRef.current) {
84056
84084
  return;
84057
84085
  }
@@ -84256,7 +84284,6 @@ function WheelUI(props) {
84256
84284
  // inputs accumulate.
84257
84285
  const glideToIndex = (vp, index, event) => {
84258
84286
  centeredIndexRef.current = index;
84259
- scrubHoldsRowRef.current = false;
84260
84287
  requestSelectValue(trackedItemsRef.current[index].value, event);
84261
84288
  glideTo(vp, glideTargetFor(vp, index));
84262
84289
  };
@@ -84287,12 +84314,12 @@ function WheelUI(props) {
84287
84314
  // Sync the center with the current value — used on first display and whenever
84288
84315
  // the controlled value changes from outside.
84289
84316
  const syncCenterToSelection = (viewportEl, behavior) => {
84290
- // A fling in flight, or a scrub holding a row, commits on settle: the finger
84291
- // owns the wheel until then, and the row it leaves the wheel on overrules a
84292
- // value arriving meanwhile. A glide is different: its target is re-aimed
84293
- // below (glideTo never restarts the loop), so a value set from outside
84294
- // mid-glide is where the wheel ends up.
84295
- if (momentumRef.current !== null || scrubHoldsRowRef.current) {
84317
+ // A fling in flight reports its own value at every row crossing and commits
84318
+ // on settle: the finger that threw it owns the wheel until then, and a value
84319
+ // arriving meanwhile is re-stated by the next crossing anyway. A glide is
84320
+ // different: its target is re-aimed below (glideTo never restarts the
84321
+ // loop), so a value set from outside mid-glide is where the wheel ends up.
84322
+ if (momentumRef.current !== null) {
84296
84323
  return;
84297
84324
  }
84298
84325
  if (trackedItemsRef.current.length === 0) {
@@ -84375,7 +84402,6 @@ function WheelUI(props) {
84375
84402
  attemptInteraction,
84376
84403
  cancelAnim,
84377
84404
  setPos,
84378
- releaseScrub,
84379
84405
  snapPosToRow,
84380
84406
  getItemSize,
84381
84407
  viewportMain,