@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.
@@ -12662,12 +12662,13 @@ const useBoundAction = (action, actionParamsSignal) => {
12662
12662
  if (existingNoopAction) {
12663
12663
  return existingNoopAction;
12664
12664
  }
12665
+ // Not bound to the params signal: nothing reads what a noop runs with, and
12666
+ // a proxy following the signal would retarget (a new child action for each
12667
+ // new value) at every change — for every button of a form, at every change
12668
+ // of any of its fields.
12665
12669
  const noopAction = createAction(() => {}, { params: undefined });
12666
- const noopActionBound = actionParamsSignal
12667
- ? noopAction.bindParams(actionParamsSignal)
12668
- : noopAction;
12669
- noopActionRef.current = noopActionBound;
12670
- return noopActionBound;
12670
+ noopActionRef.current = noopAction;
12671
+ return noopAction;
12671
12672
  }
12672
12673
  const isFunction = typeof action === "function";
12673
12674
  if (!isFunction) {
@@ -13436,7 +13437,9 @@ const createControlValidation = (
13436
13437
 
13437
13438
  let newConstraintValidityState = { valid: true };
13438
13439
  const constraintSet = getConstraintSet();
13439
- const elementSig = getElementSignature(controller.ref.current);
13440
+ // Only for the debug lines of a failure or a change: computed when one of
13441
+ // them is written, not at every check (every notch of a wheel).
13442
+ const elementSig = () => getElementSignature(controller.ref.current);
13440
13443
  // Not logged: every control checks its constraints on every interaction and
13441
13444
  // almost always passes, so this line alone was most of the debug output —
13442
13445
  // and with devtools open, formatting it costs a frame. What matters is a
@@ -13462,7 +13465,7 @@ const createControlValidation = (
13462
13465
  constraintValidityInfo.message,
13463
13466
  );
13464
13467
  debugUIState(
13465
- `${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13468
+ `${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13466
13469
  );
13467
13470
  const thisConstraintFailureInfo = {
13468
13471
  name: constraint.name,
@@ -13534,7 +13537,7 @@ const createControlValidation = (
13534
13537
  if (element) {
13535
13538
  debugUIState(
13536
13539
  event,
13537
- `${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
13540
+ `${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
13538
13541
  );
13539
13542
  dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
13540
13543
  }
@@ -13580,7 +13583,7 @@ const createControlValidation = (
13580
13583
  event,
13581
13584
  { report = false, fromRequestAction = false } = {},
13582
13585
  ) => {
13583
- const elementSig = getElementSignature(controller.ref.current);
13586
+ const elementSig = () => getElementSignature(controller.ref.current);
13584
13587
  const isValid = checkValidity({ event, fromRequestAction });
13585
13588
  if (failingManagedControlValidity) {
13586
13589
  // Group/form case: find the actual failing leaf and report on it.
@@ -13602,7 +13605,7 @@ const createControlValidation = (
13602
13605
  if (report) {
13603
13606
  debugUIState(
13604
13607
  event,
13605
- `syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
13608
+ `syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
13606
13609
  );
13607
13610
  reportValidity({ event });
13608
13611
  } else if (failedConstraintInfo.status === "error") {
@@ -13612,12 +13615,12 @@ const createControlValidation = (
13612
13615
  // on the next action attempt via autoResetOnAction.
13613
13616
  debugUIState(
13614
13617
  event,
13615
- `syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
13618
+ `syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
13616
13619
  );
13617
13620
  } else {
13618
13621
  debugUIState(
13619
13622
  event,
13620
- `syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
13623
+ `syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
13621
13624
  );
13622
13625
  callout.removeOpenToken(VALIDATION_TOKEN, event);
13623
13626
  }
@@ -27351,6 +27354,7 @@ This prevents cross-test pollution and ensures clean state.`,
27351
27354
 
27352
27355
  // Store previous route states to detect changes
27353
27356
  const routePreviousStateMap = new WeakMap();
27357
+ let previousPath = null;
27354
27358
  const updateRoutes = (
27355
27359
  url,
27356
27360
  {
@@ -27359,6 +27363,14 @@ This prevents cross-test pollution and ensures clean state.`,
27359
27363
  } = {},
27360
27364
  ) => {
27361
27365
  currentUrl = url;
27366
+ // Whether a route matches is decided by the path alone: the search string
27367
+ // only fills the params (see matchUrl). An address that keeps its path — a
27368
+ // search param written, at every notch of a wheel bound to it — cannot make
27369
+ // a route start or stop matching, so only the routes already matching are
27370
+ // read again.
27371
+ const path = urlWithoutSearchAndHash(url);
27372
+ const pathKept = path === previousPath;
27373
+ previousPath = path;
27362
27374
  const returnValue = {};
27363
27375
  const routeMatchInfoSet = new Set();
27364
27376
  for (const route of routeSet) {
@@ -27374,7 +27386,8 @@ This prevents cross-test pollution and ensures clean state.`,
27374
27386
  const oldParams = previousState.params;
27375
27387
  const oldActionParams = previousState.actionParams;
27376
27388
 
27377
- let extractedParams = routePattern.applyOn(url);
27389
+ let extractedParams =
27390
+ pathKept && !oldMatching ? null : routePattern.applyOn(url);
27378
27391
  let newMatching = Boolean(extractedParams);
27379
27392
  let newParams;
27380
27393
  if (extractedParams) {
@@ -27443,6 +27456,25 @@ This prevents cross-test pollution and ensures clean state.`,
27443
27456
  }
27444
27457
  }
27445
27458
 
27459
+ // What the matching routes say, read once for every connection of every
27460
+ // route that does not match (below), rather than by walking all the
27461
+ // routes again for each of those connections.
27462
+ const matchingRawParamsArray = [];
27463
+ const paramNameDeclaredByMatchingRouteSet = new Set();
27464
+ const matchingFamilyRootSet = new Set();
27465
+ for (const matchingRoute of matchingRouteSet) {
27466
+ matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
27467
+ const matchingPattern =
27468
+ getRoutePrivateProperties(matchingRoute).routePattern;
27469
+ for (const paramName of matchingPattern.queryConnectionMap.keys()) {
27470
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
27471
+ }
27472
+ for (const paramName of matchingPattern.pathConnectionMap.keys()) {
27473
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
27474
+ }
27475
+ matchingFamilyRootSet.add(matchingPattern.familyRoot);
27476
+ }
27477
+
27446
27478
  for (const {
27447
27479
  route,
27448
27480
  routePrivateProperties,
@@ -27458,40 +27490,21 @@ This prevents cross-test pollution and ensures clean state.`,
27458
27490
  const urlParamValue = rawParams[paramName];
27459
27491
 
27460
27492
  if (!newMatching) {
27461
- // Route doesn't match - check if any matching route extracts this parameter
27462
- let parameterExtractedByMatchingRoute = false;
27463
- let parameterDeclaredByMatchingRoute = false;
27464
- let matchingRouteInSameFamily = false;
27465
-
27466
- for (const otherRoute of routeSet) {
27467
- if (otherRoute === route || !otherRoute.matching) {
27468
- continue;
27469
- }
27470
- const otherRawParams = otherRoute.rawParamsSignal.value;
27471
-
27472
- // Check if this matching route extracts the parameter
27473
- if (paramName in otherRawParams) {
27474
- parameterExtractedByMatchingRoute = true;
27475
- }
27476
-
27477
- // Same param, on the page one is arriving at: what it is worth
27478
- // there is that page's business, url or no url — it may open on
27479
- // a default of its own (see searchParams' `{ signal, default }`).
27480
- const otherPattern =
27481
- getRoutePrivateProperties(otherRoute).routePattern;
27482
- if (
27483
- otherPattern.queryConnectionMap.has(paramName) ||
27484
- otherPattern.pathConnectionMap.has(paramName)
27485
- ) {
27486
- parameterDeclaredByMatchingRoute = true;
27487
- }
27488
-
27489
- // Same family = same topmost ancestor
27490
- // (familyRoot, computed in setupRoutePatterns)
27491
- if (otherPattern.familyRoot === routePattern.familyRoot) {
27492
- matchingRouteInSameFamily = true;
27493
- }
27494
- }
27493
+ // Whether a matching route extracts this parameter from the url
27494
+ const parameterExtractedByMatchingRoute =
27495
+ matchingRawParamsArray.some(
27496
+ (matchingRawParams) => paramName in matchingRawParams,
27497
+ );
27498
+ // Same param, on the page one is arriving at: what it is worth
27499
+ // there is that page's business, url or no url — it may open on
27500
+ // a default of its own (see searchParams' `{ signal, default }`).
27501
+ const parameterDeclaredByMatchingRoute =
27502
+ paramNameDeclaredByMatchingRouteSet.has(paramName);
27503
+ // Same family = same topmost ancestor
27504
+ // (familyRoot, computed in setupRoutePatterns)
27505
+ const matchingRouteInSameFamily = matchingFamilyRootSet.has(
27506
+ routePattern.familyRoot,
27507
+ );
27495
27508
 
27496
27509
  // A weak param qualifies one visit: leaving the route ends it,
27497
27510
  // whatever the family and whatever the default. Coming back by a
@@ -27724,6 +27737,11 @@ const getParamsCacheKey = (params) => {
27724
27737
  return key;
27725
27738
  };
27726
27739
 
27740
+ const urlWithoutSearchAndHash = (url) => {
27741
+ const urlObject = new URL(url);
27742
+ return `${urlObject.origin}${urlObject.pathname}`;
27743
+ };
27744
+
27727
27745
  /**
27728
27746
  * Is there an entry of THIS document behind the current one? And ahead of it?
27729
27747
  *
@@ -28167,7 +28185,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
28167
28185
  }
28168
28186
  };
28169
28187
 
28170
- const getHrefTargetInfo = (href) => {
28188
+ // `currentUrlString`: the address the href is compared with — the document's by
28189
+ // default. A caller subscribed to documentUrlSignal passes the value it read,
28190
+ // which is ahead of window.location while a debounced write is pending.
28191
+ const getHrefTargetInfo = (
28192
+ href,
28193
+ currentUrlString = window.location.href,
28194
+ ) => {
28171
28195
  href = String(href);
28172
28196
 
28173
28197
  if (!href || href.trim() === "") {
@@ -28180,8 +28204,8 @@ const getHrefTargetInfo = (href) => {
28180
28204
  };
28181
28205
  }
28182
28206
 
28183
- const currentUrl = new URL(window.location.href);
28184
- const targetUrl = new URL(href, window.location.href);
28207
+ const currentUrl = new URL(currentUrlString);
28208
+ const targetUrl = new URL(href, currentUrlString);
28185
28209
 
28186
28210
  let isCurrent = false;
28187
28211
  {
@@ -28223,6 +28247,21 @@ const getHrefTargetInfo = (href) => {
28223
28247
  };
28224
28248
  };
28225
28249
 
28250
+ // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
28251
+ // isAnchor), read through a computed of that one boolean: the caller re-renders
28252
+ // when it flips, not at every address write — a search param bound to a wheel
28253
+ // writes the address at every notch.
28254
+ const useHrefTargetFlag = (href, flagName) => {
28255
+ const flagSignal = useMemo(
28256
+ () =>
28257
+ computed(
28258
+ () => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
28259
+ ),
28260
+ [href, flagName],
28261
+ );
28262
+ return flagSignal.value;
28263
+ };
28264
+
28226
28265
  /*
28227
28266
  * An app can ship more than one document — an admin panel, a status page, an
28228
28267
  * entry point of its own — and a link to one of them is not a route, it is a
@@ -46728,19 +46767,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
46728
46767
 
46729
46768
  /**
46730
46769
  * Hook that reactively checks if a URL is visited.
46731
- * Re-renders when the visited URL set changes.
46770
+ * Re-renders when that answer changes — not at every url added to the visited
46771
+ * set, which grows at every new address (a search param bound to a wheel
46772
+ * writes one at every notch).
46732
46773
  *
46733
46774
  * @param {string} url - The URL to check
46734
46775
  * @returns {boolean} Whether the URL has been visited
46735
46776
  */
46736
46777
  const useIsVisited = (url) => {
46737
- return useMemo(() => {
46738
- // Access the signal to create reactive dependency
46739
- // eslint-disable-next-line no-unused-expressions
46740
- visitedUrlsSignal.value;
46741
-
46742
- return isVisited(url);
46743
- }, [url, visitedUrlsSignal.value]);
46778
+ const visitedSignal = useMemo(
46779
+ () =>
46780
+ computed(() => {
46781
+ // Both are what the answer depends on: the set, and the address a
46782
+ // relative url is resolved against (see isVisited).
46783
+ // eslint-disable-next-line no-unused-expressions
46784
+ visitedUrlsSignal.value;
46785
+ // eslint-disable-next-line no-unused-expressions
46786
+ documentUrlSignal.value;
46787
+ return isVisited(url);
46788
+ }),
46789
+ [url],
46790
+ );
46791
+ return visitedSignal.value;
46744
46792
  };
46745
46793
 
46746
46794
  /**
@@ -47400,13 +47448,14 @@ const LinkPlain = props => {
47400
47448
  const loading = basePseudoState[":-navi-loading"];
47401
47449
  const shouldDimColor = readOnly || disabled;
47402
47450
  useDimColorWhen(props.ref, shouldDimColor);
47403
- // subscribe to document url to re-render and re-compute getHrefTargetInfo
47404
- useDocumentUrl();
47451
+ // Re-rendered when one of these flips, not at every address write (see
47452
+ // useHrefTargetFlag). Whether the href leaves the site does not depend on
47453
+ // the address.
47454
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
47455
+ const isAnchor = useHrefTargetFlag(href, "isAnchor");
47405
47456
  const {
47406
- isSameSite,
47407
- isAnchor,
47408
- isCurrent
47409
- } = getHrefTargetInfo(href);
47457
+ isSameSite
47458
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
47410
47459
  // A tab that is a SLIDE is current when the container is on it — which the
47411
47460
  // <Nav> around reads off that container, so nothing here has to be told.
47412
47461
  const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
@@ -52372,10 +52421,9 @@ const ButtonHrefResolver = props => {
52372
52421
  });
52373
52422
  };
52374
52423
 
52375
- // What the href means next to the page one is on depends on the document url,
52376
- // so a button with an href re-renders at every address write. One without
52377
- // never reaches here: it has nothing to re-compute, and every button on screen
52378
- // would otherwise pay for each write.
52424
+ // Whether the href is the page one is on depends on the address, and re-renders
52425
+ // the button only when it flips (see useHrefTargetFlag). Whether it leaves the
52426
+ // site does not depend on the address.
52379
52427
  const ButtonWithHref = props => {
52380
52428
  const Next = useNextResolver();
52381
52429
  const {
@@ -52384,12 +52432,10 @@ const ButtonWithHref = props => {
52384
52432
  rel,
52385
52433
  pseudoState
52386
52434
  } = props;
52387
- // Read for the subscription: getHrefTargetInfo reads window.location.
52388
- useDocumentUrl();
52435
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
52389
52436
  const {
52390
- isSameSite,
52391
- isCurrent
52392
- } = getHrefTargetInfo(href);
52437
+ isSameSite
52438
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
52393
52439
  return jsx(Next, {
52394
52440
  ...props,
52395
52441
  target: target === undefined ? isSameSite ? undefined : "_blank" : target,
@@ -80989,7 +81035,6 @@ const useWheelInteractions = ({
80989
81035
  attemptInteraction,
80990
81036
  cancelAnim,
80991
81037
  setPos,
80992
- releaseScrub,
80993
81038
  snapPosToRow,
80994
81039
  getItemSize,
80995
81040
  viewportMain,
@@ -81397,8 +81442,6 @@ const useWheelInteractions = ({
81397
81442
  clearTimeout(settleTimer);
81398
81443
  stopClaimingGesture();
81399
81444
  cancelAnim();
81400
- // Nothing is left to settle the scrub in progress.
81401
- releaseScrub();
81402
81445
  document.removeEventListener("pointerdown", onDocumentPointerDown, {
81403
81446
  capture: true
81404
81447
  });
@@ -81564,11 +81607,11 @@ function WheelUI(props) {
81564
81607
  ...props,
81565
81608
  ref: inputRef,
81566
81609
  type: "navi_js",
81567
- // An arrow or a tap changes the value at once while the row glides into
81568
- // place, and the action must wait for the wheel to come to rest. "custom"
81569
- // stops setUIState from auto-firing the action on every value change;
81570
- // commitSelection dispatches it explicitly on settle. A caller can still
81571
- // override actionEvent.
81610
+ // uiAction fires live (every center-crossing, via setPos live reports);
81611
+ // the committed action must fire only when the wheel stabilizes. "custom"
81612
+ // stops setUIState from auto-firing the action on every value change, so
81613
+ // the live reports are uiAction-only; commitSelection then dispatches the
81614
+ // action explicitly on settle. A caller can still override actionEvent.
81572
81615
  actionEvent: props.actionEvent ?? "custom"
81573
81616
  }, {
81574
81617
  controlType: "input"
@@ -81664,11 +81707,6 @@ function WheelUI(props) {
81664
81707
  // while our own scroll-driven selection does not scroll a second time. Holds the
81665
81708
  // TARGET so rapid inputs step from it and accumulate. null = nothing pending.
81666
81709
  const centeredIndexRef = useRef(null);
81667
- // A user scrub moved the wheel onto a row that is not the value yet: set at
81668
- // the first row it crosses, cleared when it settles (commitSelection), when
81669
- // an arrow or a tap sets a value at once (glideToIndex), or when it is
81670
- // dropped (releaseScrub). See setPos.
81671
- const scrubHoldsRowRef = useRef(false);
81672
81710
  // The wheel does NOT use native scroll. `posRef` is our own virtual scroll
81673
81711
  // position (px, main axis); the list track is translated by -pos. This is the
81674
81712
  // single source of truth, wrapped modulo the real-list extent in loop mode, so
@@ -81857,14 +81895,12 @@ function WheelUI(props) {
81857
81895
  posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
81858
81896
  };
81859
81897
 
81860
- // `live` marks a user scrub (drag, momentum, wheel scroll). A scrub moves the
81861
- // drawing, not the value: the row it reaches is held by the wheel, and only
81862
- // commitSelection turns it into the value, once the motion settles. Reported
81863
- // at each crossing, every notch would be a value change for all that is
81864
- // bound to the wheel — its group, its form, a signal, the address, which
81865
- // records where a value was put down, not the path the finger took
81866
- // (navigation.md, "A value written per frame"). A programmatic move
81867
- // (centerOnIndex) omits `live`: it places the value the wheel already has.
81898
+ // `live` (user scrubs: drag, momentum, wheel scroll) reports the value as each
81899
+ // item crosses the center, so uiAction tracks the wheel in real time — like
81900
+ // every other control. The committed action still fires only once it settles
81901
+ // (commitSelection). A programmatic move (centerOnIndex on an external value
81902
+ // sync) omits it, so scrolling the wheel into place doesn't echo the value
81903
+ // back out.
81868
81904
  const setPos = (vp, pos, {
81869
81905
  live = false
81870
81906
  } = {}) => {
@@ -81875,25 +81911,18 @@ function WheelUI(props) {
81875
81911
  renderPos(vp);
81876
81912
  if (live && interactive) {
81877
81913
  const index = centeredIndex(vp);
81878
- if (index !== centeredIndexRef.current && trackedItemsRef.current[index]) {
81879
- // In step with the scrub so the next arrow or tap steps from the row
81880
- // the wheel is on (see currentTargetIndex).
81881
- centeredIndexRef.current = index;
81882
- scrubHoldsRowRef.current = true;
81914
+ if (index !== centeredIndexRef.current) {
81915
+ const item = trackedItemsRef.current[index];
81916
+ if (item) {
81917
+ // Keep centeredIndexRef in step so the every-render
81918
+ // syncCenterToSelection doesn't read this as an external change and
81919
+ // fight the in-progress motion.
81920
+ centeredIndexRef.current = index;
81921
+ requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
81922
+ }
81883
81923
  }
81884
81924
  }
81885
81925
  };
81886
- // The scrub was dropped before it settled (the wheel stopped being
81887
- // interactive, or its input bindings were torn down): the row it held never
81888
- // became the value. Forgetting it lets the next sync put the wheel back on
81889
- // the value.
81890
- const releaseScrub = () => {
81891
- if (!scrubHoldsRowRef.current) {
81892
- return;
81893
- }
81894
- scrubHoldsRowRef.current = false;
81895
- centeredIndexRef.current = null;
81896
- };
81897
81926
 
81898
81927
  // Returns whether it actually interrupted a motion in flight — lets the caller
81899
81928
  // tell a fresh gesture from one that "caught" a spinning/gliding wheel.
@@ -81929,7 +81958,6 @@ function WheelUI(props) {
81929
81958
  renderPos(vp);
81930
81959
  }
81931
81960
  centeredIndexRef.current = index;
81932
- scrubHoldsRowRef.current = false;
81933
81961
  if (!interactive || glideFromOutsideRef.current) {
81934
81962
  return;
81935
81963
  }
@@ -82127,7 +82155,6 @@ function WheelUI(props) {
82127
82155
  // inputs accumulate.
82128
82156
  const glideToIndex = (vp, index, event) => {
82129
82157
  centeredIndexRef.current = index;
82130
- scrubHoldsRowRef.current = false;
82131
82158
  requestSelectValue(trackedItemsRef.current[index].value, event);
82132
82159
  glideTo(vp, glideTargetFor(vp, index));
82133
82160
  };
@@ -82158,12 +82185,12 @@ function WheelUI(props) {
82158
82185
  // Sync the center with the current value — used on first display and whenever
82159
82186
  // the controlled value changes from outside.
82160
82187
  const syncCenterToSelection = (viewportEl, behavior) => {
82161
- // A fling in flight, or a scrub holding a row, commits on settle: the finger
82162
- // owns the wheel until then, and the row it leaves the wheel on overrules a
82163
- // value arriving meanwhile. A glide is different: its target is re-aimed
82164
- // below (glideTo never restarts the loop), so a value set from outside
82165
- // mid-glide is where the wheel ends up.
82166
- if (momentumRef.current !== null || scrubHoldsRowRef.current) {
82188
+ // A fling in flight reports its own value at every row crossing and commits
82189
+ // on settle: the finger that threw it owns the wheel until then, and a value
82190
+ // arriving meanwhile is re-stated by the next crossing anyway. A glide is
82191
+ // different: its target is re-aimed below (glideTo never restarts the
82192
+ // loop), so a value set from outside mid-glide is where the wheel ends up.
82193
+ if (momentumRef.current !== null) {
82167
82194
  return;
82168
82195
  }
82169
82196
  if (trackedItemsRef.current.length === 0) {
@@ -82246,7 +82273,6 @@ function WheelUI(props) {
82246
82273
  attemptInteraction,
82247
82274
  cancelAnim,
82248
82275
  setPos,
82249
- releaseScrub,
82250
82276
  snapPosToRow,
82251
82277
  getItemSize,
82252
82278
  viewportMain,