@jsenv/navi 0.29.505 → 0.29.507

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.
@@ -50092,7 +50092,7 @@ const useControlProps = (props, {
50092
50092
  }
50093
50093
  const domProps = toDomProps(newUIState);
50094
50094
  Object.assign(el, domProps);
50095
- debugUIState(e, `syncDomState: updated to ${getElementSignature(el)}`, domProps);
50095
+ debugUIState(e, `syncDomState: updated to ${getElementSignature(el)}`);
50096
50096
  };
50097
50097
  const controlInfo = createControlInfo(props, {
50098
50098
  controlType
@@ -68014,6 +68014,40 @@ const causeOfEvent = event => {
68014
68014
  // into says (see writeAreaAsked).
68015
68015
  const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
68016
68016
  const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
68017
+ const slidesOf = containerElement => Array.from(containerElement.querySelectorAll(":scope > [data-slide-track] > [data-slide]"));
68018
+
68019
+ // Who is drawing something about a box from outside it: a tab bar saying where
68020
+ // one is, a row of dots. They name the box they follow by its id, the way
68021
+ // everything else that talks to it across the document does (commandfor).
68022
+ const readFollowerElements = containerElement => {
68023
+ const {
68024
+ id
68025
+ } = containerElement;
68026
+ if (!id) {
68027
+ return [];
68028
+ }
68029
+ return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
68030
+ };
68031
+
68032
+ // The containers inside a slide bound to the same state as the slide's own
68033
+ // container (see `signal`) — the ones directly under it, a container sharing
68034
+ // with one of those is that one's business.
68035
+ const sharingContainersIn = slideElement => {
68036
+ const holderElement = slideElement.closest("[data-slide-container]");
68037
+ return Array.from(slideElement.querySelectorAll("[data-slide-shares-state]")).filter(innerElement => innerElement.parentElement.closest("[data-slide-container]") === holderElement);
68038
+ };
68039
+
68040
+ // Whether the state holding `area` is on screen when this slide is: it is the
68041
+ // slide's own area, or a slide of a container sharing the state inside it.
68042
+ const slideShows = (slideElement, area) => readArea(slideElement) === area || sharingContainersIn(slideElement).some(innerElement => slidesOf(innerElement).some(innerSlide => slideShows(innerSlide, area)));
68043
+
68044
+ // What the state holds while this slide is on screen: the area a container
68045
+ // sharing the state inside it stands on, its own area when there is none.
68046
+ const areaHeldOn = slideElement => {
68047
+ const [innerElement] = sharingContainersIn(slideElement);
68048
+ const innerCurrent = innerElement && slidesOf(innerElement).find(innerSlide => innerSlide.hasAttribute("data-current"));
68049
+ return innerCurrent ? areaHeldOn(innerCurrent) : readArea(slideElement);
68050
+ };
68017
68051
 
68018
68052
  /**
68019
68053
  * The slide shown can be driven from outside (a `signal`, or `current` +
@@ -68048,6 +68082,10 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
68048
68082
  * "create"]`. Spelled like grid-template-areas ("." is a hole, a name
68049
68083
  * repeated spans), except that a row needs no trailing hole: what is not
68050
68084
  * written is simply not there.
68085
+ * A map is read as a map: a step landing on a hole is not offered. So rows
68086
+ * of different lengths, each walked on its own and changed as a whole (a
68087
+ * column of levels, a row of titles in each), are not a map but a container
68088
+ * in each slide of a column, all bound to one signal — see `signal`.
68051
68089
  * @param {string} [props.current] - area (or id) of the slide being shown; omit
68052
68090
  * to keep it here and drive it by command. An area named from outside is a
68053
68091
  * REQUEST rather than a placement — see `signal` for what that means.
@@ -68088,6 +68126,47 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
68088
68126
  * by links built to that route. Written by replacement unless the state says
68089
68127
  * `history: "push"`, and even then a slide reached by DRAGGING replaces — see
68090
68128
  * docs/navigation.md.
68129
+ * ONE STATE FOR CONTAINERS INSIDE ONE ANOTHER: a container in a slide,
68130
+ * bound to the SAME signal as the container holding it, shares that state.
68131
+ * For screens where one place is said at two levels — a column of
68132
+ * tournament levels with a row of titles in each, the address saying which
68133
+ * title (`?cup=P100:2`): the level is not a second state, it is read from
68134
+ * the title.
68135
+ * ```jsx
68136
+ * const cupSignal = stateSignal(undefined, { id: "cup" });
68137
+ *
68138
+ * <SlideContainer layout="column" signal={cupSignal}>
68139
+ * {levels.map((level) => (
68140
+ * <Slide area={level.name}>
68141
+ * <LevelHeader level={level} />
68142
+ * <SlideContainer signal={cupSignal} defaultCurrent={level.entryKey}>
68143
+ * {level.titles.map((title) => (
68144
+ * <Slide area={title.key}>…</Slide>
68145
+ * ))}
68146
+ * </SlideContainer>
68147
+ * </Slide>
68148
+ * ))}
68149
+ * </SlideContainer>
68150
+ * ```
68151
+ * - The state holds an area of an INNER container. The outer one shows the
68152
+ * slide holding it, the inner ones that do not have it stay where they
68153
+ * are.
68154
+ * - A travel of the outer one writes the area the inner one stands on in the
68155
+ * slide it arrives at: a row left keeps its place for when one comes back,
68156
+ * and a row not seen yet opens on its `defaultCurrent`.
68157
+ * - A value written from outside (an address, a traversal) is walked to by
68158
+ * both, each on its own axis.
68159
+ * - The arrows of the inner one on screen are heard on the outer box and on
68160
+ * what follows it (`data-slide-container-follows`), so a surface follows
68161
+ * the OUTER one and all four arrows walk it.
68162
+ * - `onCurrentChange` still tells each one its own area: the level to the
68163
+ * outer one, the title to the inner one.
68164
+ * What an outer slide holds is read in the DOM: one whose content is not
68165
+ * built (`mount="near"`) holds nothing the state can name.
68166
+ * Neither `current` + `onCurrentChange` on each, ignoring `cause: "state"`
68167
+ * and remembering by hand where each row was left — that is a second copy of
68168
+ * the state (see docs/state_binding.md) — nor one map with rows of different
68169
+ * lengths (see `layout`).
68091
68170
  * @param {string} [props.defaultCurrent] - which slide to open on, for a
68092
68171
  * container that owns its position. Mount-only, like every other `default*`:
68093
68172
  * it says where one starts, not where one is — say `current` for that.
@@ -68097,7 +68176,8 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
68097
68176
  * opens is that state's own default (`stateSignal`'s first argument) rather
68098
68177
  * than this: one place says where the area starts, and it is the place a
68099
68178
  * reset goes back to. This still answers for a signal holding nothing at
68100
- * all.
68179
+ * all. And for a container sharing the state of the one holding it (see
68180
+ * `signal`), it is where it opens while the state names a place elsewhere.
68101
68181
  * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
68102
68182
  * - the slide being shown has changed. `cause` says what asked for it, which
68103
68183
  * is what tells a place browsed past from a place aimed at — a slide dragged
@@ -68217,6 +68297,10 @@ const SlideContainer = ({
68217
68297
  // The box itself: it is what takes the keyboard when what is on screen holds
68218
68298
  // nothing that can (see handOverFocus).
68219
68299
  const containerRef = useRef();
68300
+ // Bound to the state of the container holding this one: its slides are
68301
+ // places inside one of that container's slides (see `signal`).
68302
+ const holder = useContext(SlideContainerContext);
68303
+ const sharesState = Boolean(currentSignal) && holder?.signal === currentSignal;
68220
68304
  // The AREA of the slide being shown, not its rank: a rank would be wrong the
68221
68305
  // moment a slide appears before it, and there is nothing to renumber here.
68222
68306
  // This is where the container's own answer lives, and it is the only one the
@@ -68227,7 +68311,10 @@ const SlideContainer = ({
68227
68311
  // walking past every lock in exactly the case the locks are there for.
68228
68312
  // So the picture starts where the state itself would open (a stateSignal's
68229
68313
  // own default), or on `defaultCurrent`, and the walk goes from there.
68230
- const [currentAreaState, setCurrentAreaState] = useState(() => currentSignal?.options?.getDefaultValue?.() ?? defaultCurrent);
68314
+ // A container sharing its holder's state starts on `defaultCurrent`: the
68315
+ // state's default is a place in ONE of the containers sharing it, and the
68316
+ // one holding it is walked there like any other.
68317
+ const [currentAreaState, setCurrentAreaState] = useState(() => sharesState ? defaultCurrent : currentSignal?.options?.getDefaultValue?.() ?? defaultCurrent);
68231
68318
  // The slide this container is travelling to while its controller has not been
68232
68319
  // told yet (commit="rest"): for the length of that travel the container is
68233
68320
  // ahead of whoever holds `current`, and this is where it keeps its own answer
@@ -68310,6 +68397,21 @@ const SlideContainer = ({
68310
68397
  const followerElementsRef = useRef([]);
68311
68398
  const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
68312
68399
  const current = rollingArea ?? provisionalArea ?? currentAreaState;
68400
+ // The area of this box a value held outside is shown on: the slide of that
68401
+ // name, or the one holding a container that shares the state and has it.
68402
+ // Read off the last commit, so a render before the first sees it as it is.
68403
+ const ownAreaOf = value => {
68404
+ const track = trackRef.current;
68405
+ if (value === undefined || !track) {
68406
+ return value;
68407
+ }
68408
+ const slideElements = Array.from(track.children);
68409
+ if (slideElements.some(slideElement => readArea(slideElement) === value)) {
68410
+ return value;
68411
+ }
68412
+ const holding = slideElements.find(slideElement => slideShows(slideElement, value));
68413
+ return holding ? readArea(holding) : value;
68414
+ };
68313
68415
  // The last area this container knows the caller holds, whether it wrote it
68314
68416
  // there or read it from there. What makes a two-way binding stop turning: an
68315
68417
  // area held outside is an INSTRUCTION only when it changed by itself (a load,
@@ -68328,7 +68430,7 @@ const SlideContainer = ({
68328
68430
  // it is painted, the first request included — and the container's own answer
68329
68431
  // otherwise. What a slide waiting for a travel reads to have its content in
68330
68432
  // the very render that shows it (see Slide's `mount`).
68331
- const areaShownNext = requestIsNews() ? currentFromCaller : current;
68433
+ const areaShownNext = requestIsNews() ? ownAreaOf(currentFromCaller) : current;
68332
68434
  // Where the container stands, said where the caller holds it.
68333
68435
  //
68334
68436
  // Whether a slide is a place one came from is the STATE's own business (see
@@ -68345,17 +68447,31 @@ const SlideContainer = ({
68345
68447
  const writeAreaAsked = (area, {
68346
68448
  cause
68347
68449
  } = {}) => {
68348
- areaAskedSeenRef.current = area;
68349
68450
  if (!currentSignal) {
68451
+ areaAskedSeenRef.current = area;
68350
68452
  return;
68351
68453
  }
68454
+ const value = valueOfArea(area);
68455
+ areaAskedSeenRef.current = value;
68352
68456
  if (CAUSES_WRITTEN_BY_REPLACEMENT.includes(cause) && currentSignal.set) {
68353
- currentSignal.set(area, {
68457
+ currentSignal.set(value, {
68354
68458
  history: "replace"
68355
68459
  });
68356
68460
  return;
68357
68461
  }
68358
- currentSignal.value = area;
68462
+ currentSignal.value = value;
68463
+ };
68464
+ // What the state holds for one of this box's areas: the place a container
68465
+ // sharing it stands on inside that slide (see areaHeldOn). A state already
68466
+ // naming a place in that slide is kept as it is — a container in there may
68467
+ // have written it in this very commit, before its DOM says so.
68468
+ const valueOfArea = area => {
68469
+ const held = currentSignal.peek();
68470
+ if (held !== area && ownAreaOf(held) === area) {
68471
+ return held;
68472
+ }
68473
+ const slideElement = readMap().slideElements.find(slideElement => readArea(slideElement) === area);
68474
+ return slideElement ? areaHeldOn(slideElement) : area;
68359
68475
  };
68360
68476
  const vertical = layout === "column";
68361
68477
  // What the map has, and what each way of asking is allowed to use of it.
@@ -68407,7 +68523,7 @@ const SlideContainer = ({
68407
68523
  if (provisionalArea === null) {
68408
68524
  return;
68409
68525
  }
68410
- const heldOutside = currentFromCaller ?? currentAreaState;
68526
+ const heldOutside = ownAreaOf(currentFromCaller) ?? currentAreaState;
68411
68527
  if (heldOutside === provisionalArea) {
68412
68528
  setProvisionalArea(null);
68413
68529
  }
@@ -68498,20 +68614,6 @@ const SlideContainer = ({
68498
68614
  };
68499
68615
  };
68500
68616
 
68501
- // Who is drawing something about this box from outside it: a tab bar saying
68502
- // where one is, a row of dots. They name the box they follow by its id, the
68503
- // way everything else that talks to it across the document does (commandfor).
68504
- const readFollowerElements = () => {
68505
- const containerEl = containerRef.current;
68506
- const {
68507
- id
68508
- } = containerEl;
68509
- if (!id) {
68510
- return [];
68511
- }
68512
- return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
68513
- };
68514
-
68515
68617
  // Which slide is on screen, said in the DOM: it is what anything outside the
68516
68618
  // box reads to know where one is (a tab bar marking its current tab), and
68517
68619
  // there is nothing else for it to read — a slide the container holds by
@@ -68650,7 +68752,7 @@ const SlideContainer = ({
68650
68752
  // Read on every render rather than subscribed to: a follower says who it
68651
68753
  // follows in the DOM, and the render that mounted one is the render this
68652
68754
  // runs after.
68653
- followerElementsRef.current = readFollowerElements();
68755
+ followerElementsRef.current = readFollowerElements(containerRef.current);
68654
68756
  const {
68655
68757
  slideElements,
68656
68758
  placeOf
@@ -68923,7 +69025,7 @@ const SlideContainer = ({
68923
69025
  // this one, in the same commit, and the slides around a picture about to be
68924
69026
  // walked away from would be built for nothing. The neighbours only at
68925
69027
  // rest, see renderNear.
68926
- const settlingArea = requestIsNews() && reachableTowards(currentArea, currentFromCaller) || currentArea;
69028
+ const settlingArea = requestIsNews() && reachableTowards(currentArea, ownAreaOf(currentFromCaller)) || currentArea;
68927
69029
  renderNear(settlingArea, {
68928
69030
  alongside: !stageRef.current
68929
69031
  });
@@ -69173,8 +69275,11 @@ const SlideContainer = ({
69173
69275
  // content arrives a request later. Not a refusal: nothing is remembered and
69174
69276
  // nothing is written, and the render that brings the slide asks again. A
69175
69277
  // name that will never be a slide is the state's to refuse (`oneOf`), not
69176
- // the container's: from here, "later" and "never" look the same.
69177
- if (!readMap().slideElements.some(slideElement => readArea(slideElement) === currentFromCaller)) {
69278
+ // the container's: from here, "later" and "never" look the same. Nor is a
69279
+ // place shown by another container sharing the state — a row of a column
69280
+ // of rows stays where it was while the state names a place in another one.
69281
+ const areaAsked = ownAreaOf(currentFromCaller);
69282
+ if (!readMap().slideElements.some(slideElement => readArea(slideElement) === areaAsked)) {
69178
69283
  return;
69179
69284
  }
69180
69285
  areaAskedSeenRef.current = currentFromCaller;
@@ -69182,10 +69287,10 @@ const SlideContainer = ({
69182
69287
  // names a slide, and the container standing on its first one is a fact only
69183
69288
  // the map knows (see the layout effect that paints it).
69184
69289
  const areaOnScreen = containerRef.current.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
69185
- if (currentFromCaller === areaOnScreen) {
69290
+ if (areaAsked === areaOnScreen) {
69186
69291
  return;
69187
69292
  }
69188
- const reached = reachableTowards(areaOnScreen, currentFromCaller);
69293
+ const reached = reachableTowards(areaOnScreen, areaAsked);
69189
69294
  if (reached && reached !== areaOnScreen) {
69190
69295
  // Told rather than asked: the walk above has already put every lock on
69191
69296
  // the way the question goToArea would have put the first one, and this
@@ -69614,7 +69719,7 @@ const SlideContainer = ({
69614
69719
  // Read again at the start of every travel, not only at every render: a
69615
69720
  // follower appearing does not make this box render, and the travel it must
69616
69721
  // draw is the one about to start.
69617
- followerElementsRef.current = readFollowerElements();
69722
+ followerElementsRef.current = readFollowerElements(containerRef.current);
69618
69723
  cancelTravelProgressAnimation();
69619
69724
  paintTravelProgress(0);
69620
69725
  const painters = travelPainters();
@@ -69927,7 +70032,7 @@ const SlideContainer = ({
69927
70032
  trackAnimationRef.current?.cancel();
69928
70033
  trackAnimationRef.current = null;
69929
70034
  cancelTravelProgressAnimation();
69930
- followerElementsRef.current = readFollowerElements();
70035
+ followerElementsRef.current = readFollowerElements(containerRef.current);
69931
70036
  drag.axis = axis;
69932
70037
  drag.areaBack = areaBack;
69933
70038
  drag.areaOn = areaOn;
@@ -70315,6 +70420,35 @@ const SlideContainer = ({
70315
70420
  }
70316
70421
  };
70317
70422
  });
70423
+ // A container sharing the state of the one holding it is part of that one's
70424
+ // surface: the arrows it walks are heard on the holder's box and on what
70425
+ // follows it, while its slide is the one on screen — a row of a column of
70426
+ // rows walks sideways with the keyboard on the column, or on a chevron of
70427
+ // the column. A press something already travelled on is left alone (it is
70428
+ // marked, see applyKeyboardShortcuts), and so is one pressed inside this box,
70429
+ // which heard it first.
70430
+ useLayoutEffect(() => {
70431
+ const holderElement = sharesState && holder.containerRef.current;
70432
+ if (!holderElement) {
70433
+ return undefined;
70434
+ }
70435
+ const surfaceElements = [holderElement, ...readFollowerElements(holderElement)];
70436
+ const onSurfaceKeyDown = keyDownEvent => {
70437
+ const containerEl = containerRef.current;
70438
+ if (keyDownEvent.defaultPrevented || !containerEl || containerEl.contains(keyDownEvent.target) || !containerEl.closest("[data-slide]")?.hasAttribute("data-current")) {
70439
+ return;
70440
+ }
70441
+ onKeyDownShortcuts(keyDownEvent);
70442
+ };
70443
+ for (const surfaceElement of surfaceElements) {
70444
+ surfaceElement.addEventListener("keydown", onSurfaceKeyDown);
70445
+ }
70446
+ return () => {
70447
+ for (const surfaceElement of surfaceElements) {
70448
+ surfaceElement.removeEventListener("keydown", onSurfaceKeyDown);
70449
+ }
70450
+ };
70451
+ });
70318
70452
  return (
70319
70453
  // Box rather than a plain div: it is how every navi component takes the
70320
70454
  // onnavi_* handlers below — they are navi's own event names, and Box is
@@ -70324,6 +70458,10 @@ const SlideContainer = ({
70324
70458
  ref: containerRef,
70325
70459
  baseClassName: "navi_slide_container",
70326
70460
  "data-slide-container": ""
70461
+ // What the holder reads to show the slide holding the place the state
70462
+ // names, and to write the place this box stands on (see `signal`).
70463
+ ,
70464
+ "data-slide-shares-state": sharesState ? "" : undefined
70327
70465
  // Which axes a touch may travel on, said in the DOM: what the browser
70328
70466
  // does with a finger is decided by CSS (touch-action) before any of this
70329
70467
  // has seen the gesture — and it is also what a box HOLDING this one reads
@@ -70465,6 +70603,9 @@ const SlideContainer = ({
70465
70603
  areaShownNext,
70466
70604
  nearAreas,
70467
70605
  claimOpening: areaShownNext === undefined ? claimOpening : null,
70606
+ // What a container nested in a slide compares its own with, to
70607
+ // know it shares this state (see `signal`).
70608
+ signal: currentSignal,
70468
70609
  // The box itself, for what is written INSIDE it to read the same
70469
70610
  // facts what is written outside reads by id (useSlideContainer):
70470
70611
  // a way out is a way out on either side of the box.
@@ -82942,7 +83083,8 @@ const SelectableInput = SelectableInputProxy;
82942
83083
  * The `highlight` argument of `useSearchHighlight` (a row's `matchInfo.matchRanges`) can be:
82943
83084
  * - an array of [start, end] pairs — applied to all text nodes under the root element
82944
83085
  * - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
82945
- * matched by the selector (the format produced by createSearch)
83086
+ * matched by the selector (the format produced by createSearch); a selector
83087
+ * matching nothing under the root element marks nothing
82946
83088
  */
82947
83089
 
82948
83090
  // Module-level shared Highlight instance — created lazily once.
@@ -82988,10 +83130,12 @@ const applySearchHighlight = (el, highlight) => {
82988
83130
  ? highlight.length === 0
82989
83131
  ? []
82990
83132
  : [{ root: el, ranges: highlight }]
82991
- : Object.entries(highlight).map(([selector, ranges]) => ({
82992
- root: el.querySelector(selector) ?? el,
82993
- ranges,
82994
- }));
83133
+ : Object.entries(highlight).flatMap(([selector, ranges]) => {
83134
+ // The ranges are offsets into the selected element's text: an item
83135
+ // that doesn't render it has nothing to mark.
83136
+ const root = el.querySelector(selector);
83137
+ return root ? [{ root, ranges }] : [];
83138
+ });
82995
83139
 
82996
83140
  if (entries.length === 0) {
82997
83141
  return undefined;
@@ -96738,7 +96882,12 @@ const mergeRanges = (ranges) => {
96738
96882
  *
96739
96883
  * Each field config:
96740
96884
  * - getter(item): string — extracts the text to search
96741
- * - domSelector: string — CSS selector used by ListItem to find the target element
96885
+ * - domSelector?: string — CSS selector used by ListItem to find the target element.
96886
+ * Without it, the field is searched and scored but highlights
96887
+ * nothing: text the item doesn't write (an alias, a civil name
96888
+ * under a nickname, tags). One field per selector: the ranges
96889
+ * of two fields sharing one are offsets into two different
96890
+ * strings, so the last field wins.
96742
96891
  * - priority?: number — multiplier applied to the field's score (default 1)
96743
96892
  * - matchFn?: function — custom match function (searchText, fieldValue) => { match, matchScore, matchRanges }
96744
96893
  * defaults to applySearch
@@ -96758,7 +96907,9 @@ const createSearch = (fields) => {
96758
96907
  const result = matchFn(searchText, fieldValue);
96759
96908
  if (result.match && result.matchRanges.length > 0) {
96760
96909
  totalScore += result.matchScore * priority;
96761
- matchRanges[domSelector] = result.matchRanges;
96910
+ if (domSelector) {
96911
+ matchRanges[domSelector] = result.matchRanges;
96912
+ }
96762
96913
  }
96763
96914
  }
96764
96915
  if (totalScore === 0) {