@jsenv/navi 0.30.8 → 0.30.10

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.
@@ -8425,17 +8425,35 @@ const route = (pattern, options = {}) => {
8425
8425
  return `route "${cleanPattern}"`;
8426
8426
  },
8427
8427
  };
8428
- // Asks ahead of the arrival for what this route's actions ask without the
8429
- // address — a page's code, a read without params (see routeAction). An
8430
- // action keyed by a param waits for the arrival: a prefetch has no address
8431
- // (preloadState is the one that has one).
8432
- route.preload = () => {
8428
+ /**
8429
+ * Asks ahead of the arrival for what this route's actions will ask: the
8430
+ * instances the arrival promotes to runs (see prerunAhead in route_action.js).
8431
+ *
8432
+ * Without params, only what asks nothing of the address: the page's code, a
8433
+ * read without params — what a link preloads, since it has no state to read
8434
+ * the params of. Given params, `{}` included, the address `buildUrl(params)`
8435
+ * leads to: every route action of this route reads its params as if the
8436
+ * route matched there (see statesAt).
8437
+ *
8438
+ * @param {object} [params]
8439
+ */
8440
+ route.preload = (params) => {
8433
8441
  const preloadSet = routePreloadMap.get(route);
8434
- if (!preloadSet) {
8442
+ if (preloadSet) {
8443
+ for (const preload of preloadSet) {
8444
+ preload();
8445
+ }
8446
+ }
8447
+ if (params === undefined) {
8435
8448
  return;
8436
8449
  }
8437
- for (const preload of preloadSet) {
8438
- preload();
8450
+ const statePreloadSet = routeStatePreloadMap.get(route);
8451
+ if (!statePreloadSet) {
8452
+ return;
8453
+ }
8454
+ const values = statesAt(route, params);
8455
+ for (const preload of statePreloadSet) {
8456
+ preload(values);
8439
8457
  }
8440
8458
  };
8441
8459
  Object.preventExtensions(route);
@@ -9034,6 +9052,42 @@ const preloadState = (signal, value) => {
9034
9052
  }
9035
9053
  }
9036
9054
  };
9055
+ // What the states of a route will hold once the address is `route.buildUrl(params)`,
9056
+ // as the routing pass will write them (see sync_routes_with_url): a param given is
9057
+ // what its state holds there; a weak one not given is back to its default, a url
9058
+ // built for another visit never carries it (readSignalForUrlBuild); any other one
9059
+ // is carried over by that url and keeps what it holds, so it is left out.
9060
+ const statesAt = (route, params) => {
9061
+ const values = new Map();
9062
+ for (const connection of getRoutePrivateProperties(route).routePattern
9063
+ .connections) {
9064
+ const { paramName, signal } = connection;
9065
+ if (!signal) {
9066
+ continue;
9067
+ }
9068
+ if (!(paramName in params)) {
9069
+ if (connection.weak) {
9070
+ values.set(signal, connection.getDefaultValue());
9071
+ }
9072
+ continue;
9073
+ }
9074
+ const value = params[paramName];
9075
+ if (value !== undefined) {
9076
+ values.set(signal, value);
9077
+ continue;
9078
+ }
9079
+ // An address silent about a persisted search param leaves it alone.
9080
+ if (
9081
+ connection.persists &&
9082
+ connection.paramType === "query" &&
9083
+ connection.history !== "push"
9084
+ ) {
9085
+ continue;
9086
+ }
9087
+ values.set(signal, connection.getDefaultValue());
9088
+ }
9089
+ return values;
9090
+ };
9037
9091
  const setupRoutes = (routes) => {
9038
9092
  if (setupRoutesCalled) {
9039
9093
  throw new Error(
@@ -9562,6 +9616,35 @@ const activeRouteActionsSignal = computed(() => {
9562
9616
  return active;
9563
9617
  });
9564
9618
 
9619
+ /**
9620
+ * Calls back once every route action asking something for the page on screen
9621
+ * has answered, none running and none about to: what is asked on behalf of
9622
+ * another page waits for it, and never competes with the page's own reads.
9623
+ *
9624
+ * @param {() => void} callback
9625
+ * @returns {() => void} cancel
9626
+ */
9627
+ const whenPageReadsAnswered = (callback) => {
9628
+ let cancelled = false;
9629
+ const stop = effect(function () {
9630
+ for (const routeAction of activeRouteActionsSignal.value) {
9631
+ const runningState = routeAction.runningStateSignal.value;
9632
+ // IDLE: asking, and run by an effect of the same batch that has not run yet.
9633
+ if (runningState === RUNNING || runningState === IDLE) {
9634
+ return;
9635
+ }
9636
+ }
9637
+ this.dispose();
9638
+ if (!cancelled) {
9639
+ untracked(callback);
9640
+ }
9641
+ });
9642
+ return () => {
9643
+ cancelled = true;
9644
+ stop();
9645
+ };
9646
+ };
9647
+
9565
9648
  /**
9566
9649
  * Whether a route action of the page on screen waits for app state to ask: its
9567
9650
  * route matches and its `when` says no. Params that say no are not waiting:
@@ -9642,7 +9725,9 @@ const activeRouteReadsSignal = computed(() => {
9642
9725
  * come from the address is not prefetched by a link — a link's prefetch has
9643
9726
  * no address. It is by the press that opens a popup bound to a state these
9644
9727
  * params read: that address is the current one with the state set to the
9645
- * popup's value (see `prefetch` on Button).
9728
+ * popup's value (see `prefetch` on Button). And by `route.preload(params)`,
9729
+ * which names the address: what a `<RouteTravel prefetch>` asks of the pages
9730
+ * on each side of the one on screen.
9646
9731
  * @param {object} [options]
9647
9732
  * @param {() => any} [options.when] - a condition on app state the action
9648
9733
  * asks under, read off signals like the params: falsy, it runs nothing.
@@ -9723,7 +9808,7 @@ const routeAction = (
9723
9808
  for (const route of routes) {
9724
9809
  if (paramsEffect) {
9725
9810
  registerRouteStatePreload(route, (values) => {
9726
- prefetchAsIf(action, readParams, values);
9811
+ prefetchAsIf(action, readParams, readParamsAsked, values);
9727
9812
  });
9728
9813
  } else {
9729
9814
  registerRoutePreload(route, () => {
@@ -9739,15 +9824,17 @@ const routeAction = (
9739
9824
  };
9740
9825
 
9741
9826
  // The params as they will read once the states hold what they are about to
9742
- // (see readStateAsIf), prerun only when that changes them: params the state
9743
- // leaves alone are the ones the action already runs on — or is meant not to,
9744
- // and a prerun would retry a FAILED one behind the screen's back.
9745
- const prefetchAsIf = (action, readParams, values) => {
9827
+ // (see readStateAsIf), prerun only when they differ from what the action asks
9828
+ // now: params the states leave alone are the ones it already runs on — or is
9829
+ // meant not to, and a prerun would retry a FAILED one behind the screen's back.
9830
+ // An action whose route does not match asks nothing: whatever its params read,
9831
+ // the arrival runs it on them.
9832
+ const prefetchAsIf = (action, readParams, readParamsAsked, values) => {
9746
9833
  const params = readStateAsIf(values, readParams);
9747
9834
  if (!params) {
9748
9835
  return;
9749
9836
  }
9750
- if (compareTwoJsValues(params, untracked(readParams))) {
9837
+ if (compareTwoJsValues(params, untracked(readParamsAsked))) {
9751
9838
  return;
9752
9839
  }
9753
9840
  prerunAhead(action.bindParams(params));
@@ -40450,9 +40537,11 @@ const readRowStretch = (scroller, pages, page, { sideways } = {}) => {
40450
40537
  /**
40451
40538
  * @param {Element} scroller
40452
40539
  * @param {object} options
40453
- * @param {(move: {finger: boolean}) => void} options.onScroll - the row moved.
40454
- * `finger`: a touch has come since the row last stopped, so a lift will say where
40455
- * it goes (`onLeftToSnap`). Without one (a trackpad, a wheel) nothing will.
40540
+ * @param {(move: {finger: boolean, lifted: boolean}) => void} options.onScroll - the
40541
+ * row moved. `finger`: a touch has come since the row last stopped, so a lift will
40542
+ * say where it goes (`onLeftToSnap`). Without one (a trackpad, a wheel) nothing
40543
+ * will. `lifted`: that finger has lifted and none has landed since: no hand moves
40544
+ * the row, the snap the lift left running does.
40456
40545
  * @param {(event: Event|null) => void} options.onLand - the row may have stopped.
40457
40546
  * Read `pageAtRest` to know whether it stopped on a page. Heard more than once
40458
40547
  * for one landing, so what it does must find nothing to do the second time.
@@ -40483,7 +40572,10 @@ const watchRowScroll = (
40483
40572
  stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
40484
40573
  };
40485
40574
  const onScrollEvent = () => {
40486
- onScroll({ finger: touchedSinceStop });
40575
+ onScroll({
40576
+ finger: touchedSinceStop,
40577
+ lifted: touchedSinceStop && !touching,
40578
+ });
40487
40579
  armSilence();
40488
40580
  if (!liftedAt) {
40489
40581
  return;
@@ -40647,6 +40739,7 @@ const ROW_READING = {
40647
40739
  * travelByDrag?: boolean,
40648
40740
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
40649
40741
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
40742
+ * prefetch?: boolean,
40650
40743
  * }>}
40651
40744
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
40652
40745
  * over something longer, in order. Read from the <Route> children by default,
@@ -40670,6 +40763,12 @@ const ROW_READING = {
40670
40763
  * `{ route, params }`, or the entry of `routes` as it was given, and is a column
40671
40764
  * the height of the row, as the page is. Its route does not match while it stands
40672
40765
  * there: what it reads is not asked for.
40766
+ * @param {boolean} [props.prefetch=false] - whether the pages on each side of the one
40767
+ * on screen have their reads asked for ahead of the hand: at rest, once the page on
40768
+ * screen has its answers and has painted them, the next page first, each with
40769
+ * `route.preload(params)`. A swipe then builds a page whose answers are already
40770
+ * there, with no wait to show. Off by default: every page one stops on costs the
40771
+ * reads of two more.
40673
40772
  *
40674
40773
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
40675
40774
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -40686,6 +40785,7 @@ const RouteTravel = ({
40686
40785
  routeTransition: "none"
40687
40786
  }),
40688
40787
  renderNeighbour,
40788
+ prefetch = false,
40689
40789
  className,
40690
40790
  children,
40691
40791
  ...rest
@@ -40695,7 +40795,8 @@ const RouteTravel = ({
40695
40795
  const liveRef = useRef();
40696
40796
  const picturesRef = useRef();
40697
40797
  const windowed = Boolean(routesProp);
40698
- const [, render] = useReducer(count => count + 1, 0);
40798
+ // Counts the movements ended: endMovement renders through it.
40799
+ const [restCount, render] = useReducer(count => count + 1, 0);
40699
40800
  const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
40700
40801
  const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
40701
40802
  // Held while the row moves: a window recentred under a hand would move the very
@@ -41067,7 +41168,8 @@ const RouteTravel = ({
41067
41168
  const row = rowRef.current;
41068
41169
  const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
41069
41170
  const onScroll = ({
41070
- finger
41171
+ finger,
41172
+ lifted
41071
41173
  }) => {
41072
41174
  const {
41073
41175
  pages,
@@ -41102,6 +41204,12 @@ const RouteTravel = ({
41102
41204
  } = readRowFrame(row, panes);
41103
41205
  followPageOnScreen(pages[panes.indexOf(fraction >= 0.5 && toward ? toward : base)], askedPageRef.current || pages[currentIndex]);
41104
41206
  }
41207
+ // The pull is read from the pane of the address. Once the finger has lifted,
41208
+ // the address is where the snap goes (onLeftToSnap), and what still lies
41209
+ // between is the snap playing out, not a hand pulling towards the neighbour.
41210
+ if (lifted) {
41211
+ return;
41212
+ }
41105
41213
  const pulled = row.scrollLeft - pane.offsetLeft;
41106
41214
  if (Math.abs(pulled) < SET_OFF_PULL) {
41107
41215
  return;
@@ -41262,6 +41370,45 @@ const RouteTravel = ({
41262
41370
  }
41263
41371
  }, [currentIndex]);
41264
41372
 
41373
+ // What a hand can bring is asked for at rest, the next page first: never before
41374
+ // the page on screen has its answers and has painted them. A row in movement is
41375
+ // not at rest; the render that ends the movement asks again. Read as pages, not as
41376
+ // entries: the pages of <Route> children are collected again at every render.
41377
+ const neighboursRef = useRef([null, null]);
41378
+ const nextPage = currentIndex === -1 ? null : pages[currentIndex + 1] || null;
41379
+ const previousPage = currentIndex === -1 ? null : pages[currentIndex - 1] || null;
41380
+ if (!samePage(neighboursRef.current[0], nextPage) || !samePage(neighboursRef.current[1], previousPage)) {
41381
+ neighboursRef.current = [nextPage, previousPage];
41382
+ }
41383
+ const neighbours = neighboursRef.current;
41384
+ useLayoutEffect(() => {
41385
+ if (!prefetch || movementRef.current) {
41386
+ return undefined;
41387
+ }
41388
+ if (!neighbours[0] && !neighbours[1]) {
41389
+ return undefined;
41390
+ }
41391
+ let cancelPaint = null;
41392
+ const cancelWait = whenPageReadsAnswered(() => {
41393
+ cancelPaint = afterPaint(() => {
41394
+ if (movementRef.current) {
41395
+ return;
41396
+ }
41397
+ for (const page of neighbours) {
41398
+ if (page) {
41399
+ page.route.preload(page.params || {});
41400
+ }
41401
+ }
41402
+ });
41403
+ });
41404
+ return () => {
41405
+ cancelWait();
41406
+ if (cancelPaint) {
41407
+ cancelPaint();
41408
+ }
41409
+ };
41410
+ }, [prefetch, neighbours, restCount]);
41411
+
41265
41412
  // A neighbour shows from the first frame the compositor moves the row, before
41266
41413
  // any scroll event: a finger landing is the last word before that.
41267
41414
  const paintsNeighbours = Boolean(renderNeighbour);