@jsenv/navi 0.29.25 → 0.29.27
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.
- package/README.md +1 -1
- package/dist/jsenv_navi.js +746 -177
- package/dist/jsenv_navi.js.map +104 -55
- package/docs/AI_INSTRUCTIONS.md +7 -3
- package/docs/drag_to_travel.md +24 -2
- package/docs/navigation.md +294 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createContext, isValidElement, h, Fragment, toChildArray, render, optio
|
|
|
7
7
|
import { useContext, useLayoutEffect, useRef, useCallback, useState, useMemo, useId, useEffect, useErrorBoundary } from "preact/hooks";
|
|
8
8
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
9
9
|
import { computed, signal, effect, batch, useSignal } from "@preact/signals";
|
|
10
|
-
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
10
|
+
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
11
11
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
12
12
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
13
13
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
@@ -2775,6 +2775,32 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2775
2775
|
return false;
|
|
2776
2776
|
}
|
|
2777
2777
|
|
|
2778
|
+
// Descending into a child path param means "this URL keeps the value you
|
|
2779
|
+
// are on" — right for a param that QUALIFIES a position (a tab, a mode),
|
|
2780
|
+
// wrong for one that NAMES a page. Two things must both hold for it to be
|
|
2781
|
+
// a name:
|
|
2782
|
+
//
|
|
2783
|
+
// - literal routes are declared for its other values ("/games/me/done").
|
|
2784
|
+
// Declaring them is the developer saying these values are places; a tab
|
|
2785
|
+
// nobody named a route after stays a qualifier;
|
|
2786
|
+
// - it has a default value, which makes THIS url the url of that default:
|
|
2787
|
+
// "/games/me" IS section=a-venir. Descending would leave the default
|
|
2788
|
+
// unaddressable — two states, one url. Without a default ("/map" is not
|
|
2789
|
+
// a panel, it is the absence of one) this url means nothing yet and
|
|
2790
|
+
// stays free to remember where you were.
|
|
2791
|
+
const thisUrlAlreadyMeansAParamValue = (connection) => {
|
|
2792
|
+
if (connection.paramType !== "path") {
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2795
|
+
if (pathConnectionMap.has(connection.paramName)) {
|
|
2796
|
+
return false; // we carry that param ourselves, we are not its default
|
|
2797
|
+
}
|
|
2798
|
+
if (!connection.namedByLiteralRoutes) {
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
return connection.getDefaultValue() !== undefined;
|
|
2802
|
+
};
|
|
2803
|
+
|
|
2778
2804
|
// Check if child has active non-default signal values
|
|
2779
2805
|
let hasActiveParams = false;
|
|
2780
2806
|
const childParams = { ...compatibility.childParams };
|
|
@@ -2801,7 +2827,10 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2801
2827
|
if (signalValue !== undefined) {
|
|
2802
2828
|
// No explicit override - use signal value
|
|
2803
2829
|
childParams[paramName] = signalValue;
|
|
2804
|
-
if (
|
|
2830
|
+
if (
|
|
2831
|
+
connection.isCustomValue(signalValue) &&
|
|
2832
|
+
!thisUrlAlreadyMeansAParamValue(connection)
|
|
2833
|
+
) {
|
|
2805
2834
|
hasActiveParams = true;
|
|
2806
2835
|
}
|
|
2807
2836
|
}
|
|
@@ -5323,6 +5352,57 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
5323
5352
|
collectDescendantPathSignals(routePattern);
|
|
5324
5353
|
routePattern.descendantPathSignals = descendantPathSignalsByIndex;
|
|
5325
5354
|
}
|
|
5355
|
+
// Phase 5b: Flag path params whose values are ALSO declared as literal routes
|
|
5356
|
+
// ("/games/me/done" next to "/games/me/:section"). That declaration is the
|
|
5357
|
+
// only reliable statement that the param names pages rather than qualifying
|
|
5358
|
+
// one — read by shouldUseChildRoute to decide whether an ancestor url may
|
|
5359
|
+
// descend into it.
|
|
5360
|
+
for (const routePattern of routePatternSet) {
|
|
5361
|
+
for (const connection of routePattern.connections) {
|
|
5362
|
+
if (connection.paramType !== "path") {
|
|
5363
|
+
continue;
|
|
5364
|
+
}
|
|
5365
|
+
const paramSegment = routePattern.pattern.segments.find(
|
|
5366
|
+
(seg) => seg.type === "param" && seg.name === connection.paramName,
|
|
5367
|
+
);
|
|
5368
|
+
if (!paramSegment) {
|
|
5369
|
+
continue;
|
|
5370
|
+
}
|
|
5371
|
+
const { index } = paramSegment;
|
|
5372
|
+
const sharesPathUpTo = (otherSegments) => {
|
|
5373
|
+
for (let i = 0; i < index; i++) {
|
|
5374
|
+
const seg = routePattern.pattern.segments[i];
|
|
5375
|
+
const otherSeg = otherSegments[i];
|
|
5376
|
+
if (!otherSeg) {
|
|
5377
|
+
return false;
|
|
5378
|
+
}
|
|
5379
|
+
if (seg.type === "literal" && otherSeg.type === "literal") {
|
|
5380
|
+
if (seg.value !== otherSeg.value) {
|
|
5381
|
+
return false;
|
|
5382
|
+
}
|
|
5383
|
+
} else if (seg.type !== otherSeg.type) {
|
|
5384
|
+
return false;
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
return true;
|
|
5388
|
+
};
|
|
5389
|
+
for (const otherPattern of routePatternSet) {
|
|
5390
|
+
if (otherPattern === routePattern) {
|
|
5391
|
+
continue;
|
|
5392
|
+
}
|
|
5393
|
+
const otherSegments = otherPattern.pattern.segments;
|
|
5394
|
+
const otherSegment = otherSegments[index];
|
|
5395
|
+
if (!otherSegment || otherSegment.type !== "literal") {
|
|
5396
|
+
continue;
|
|
5397
|
+
}
|
|
5398
|
+
if (!sharesPathUpTo(otherSegments)) {
|
|
5399
|
+
continue;
|
|
5400
|
+
}
|
|
5401
|
+
connection.namedByLiteralRoutes = true;
|
|
5402
|
+
break;
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5326
5406
|
// Phase 6: Calculate depths for all patterns
|
|
5327
5407
|
for (const routePattern of routePatternSet) {
|
|
5328
5408
|
calculatePatternDepth(routePattern);
|
|
@@ -34654,17 +34734,25 @@ const Route = props => {
|
|
|
34654
34734
|
});
|
|
34655
34735
|
};
|
|
34656
34736
|
/**
|
|
34657
|
-
* The
|
|
34737
|
+
* The pages a tree of <Route> children is made of, in the order they are
|
|
34658
34738
|
* written. Reading them is what turns a router into a row one can walk: "one
|
|
34659
34739
|
* step that way" is a fact about the order the branches were declared in, and
|
|
34660
34740
|
* nothing in a URL says it.
|
|
34661
34741
|
*
|
|
34742
|
+
* A page is `{ route, params }`, never the route alone: a section of a page is
|
|
34743
|
+
* as often a PARAM as it is a route of its own — `<Route route={PAGE}
|
|
34744
|
+
* routeParams={{ section: "done" }}>` is how this very file selects a branch on
|
|
34745
|
+
* one — and three branches of the same route are then the same object three
|
|
34746
|
+
* times. Told apart by their params, they are three pages one walks between;
|
|
34747
|
+
* told apart by identity, they are one page and there is nowhere to walk.
|
|
34748
|
+
* `params` is undefined for a branch that is a route on its own.
|
|
34749
|
+
*
|
|
34662
34750
|
* The same walk the container does to find the active branch (collectBranches),
|
|
34663
34751
|
* except that it keeps every leaf rather than the one that matches — and reads
|
|
34664
34752
|
* no signal, so asking does not subscribe the asker to anything.
|
|
34665
34753
|
*/
|
|
34666
|
-
const
|
|
34667
|
-
const
|
|
34754
|
+
const collectRoutePages = children => {
|
|
34755
|
+
const pages = [];
|
|
34668
34756
|
const visit = child => {
|
|
34669
34757
|
if (!child || child === true || child === false) {
|
|
34670
34758
|
return;
|
|
@@ -34680,18 +34768,22 @@ const collectRoutes = children => {
|
|
|
34680
34768
|
}
|
|
34681
34769
|
const {
|
|
34682
34770
|
children: nodeChildren,
|
|
34683
|
-
route
|
|
34771
|
+
route,
|
|
34772
|
+
routeParams
|
|
34684
34773
|
} = child.props;
|
|
34685
34774
|
if (nodeChildren) {
|
|
34686
34775
|
visit(nodeChildren);
|
|
34687
34776
|
return;
|
|
34688
34777
|
}
|
|
34689
34778
|
if (route) {
|
|
34690
|
-
|
|
34779
|
+
pages.push({
|
|
34780
|
+
route,
|
|
34781
|
+
params: routeParams
|
|
34782
|
+
});
|
|
34691
34783
|
}
|
|
34692
34784
|
};
|
|
34693
34785
|
visit(children);
|
|
34694
|
-
return
|
|
34786
|
+
return pages;
|
|
34695
34787
|
};
|
|
34696
34788
|
|
|
34697
34789
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
@@ -34897,14 +34989,19 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
34897
34989
|
// transition carries was measured once, at the start, against a destination
|
|
34898
34990
|
// this travel is no longer going to.
|
|
34899
34991
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
34992
|
+
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
34993
|
+
const TRAVEL_NAME = "navi-route-travel";
|
|
34900
34994
|
const css$R = /* css */`
|
|
34995
|
+
/* The name that makes the page inside this box a picture of its own during a
|
|
34996
|
+
transition — rather than part of the one big picture the document takes, so
|
|
34997
|
+
the two pages can move past each other while everything else stays where it
|
|
34998
|
+
is — is not written here: it is worn only for the length of a travel (see
|
|
34999
|
+
nameForTravel). A name belongs to ONE element at a time, and a page can hold
|
|
35000
|
+
several of these boxes at once — a section of the url and a search param of
|
|
35001
|
+
the root route are two rows of tabs, both live, and only one of them is ever
|
|
35002
|
+
travelling. */
|
|
34901
35003
|
.navi_route_travel {
|
|
34902
35004
|
position: relative;
|
|
34903
|
-
/* Named, so the page inside this box is a picture of its own during a
|
|
34904
|
-
transition rather than part of the one big picture the document takes:
|
|
34905
|
-
the two pages can then move past each other while everything else stays
|
|
34906
|
-
where it is. */
|
|
34907
|
-
view-transition-name: navi-route-travel;
|
|
34908
35005
|
/* The gesture takes the axis the pages travel on and leaves the other one
|
|
34909
35006
|
to the page, so a list still scrolls under the same finger. */
|
|
34910
35007
|
touch-action: pan-y;
|
|
@@ -35102,21 +35199,30 @@ const css$R = /* css */`
|
|
|
35102
35199
|
|
|
35103
35200
|
/**
|
|
35104
35201
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
35105
|
-
* routes?: Array<object>,
|
|
35202
|
+
* routes?: Array<object|{route: object, params?: object}>,
|
|
35106
35203
|
* axis?: "x"|"y",
|
|
35107
35204
|
* travelByDrag?: boolean,
|
|
35108
|
-
* onTravel?: (detail: {route: object, cause: string}) => void|Promise<void>,
|
|
35205
|
+
* onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
|
|
35109
35206
|
* }>}
|
|
35110
|
-
* @param {Array<object>} [props.routes] - the
|
|
35111
|
-
* Read from the <Route> children by
|
|
35112
|
-
* the router already holds that list,
|
|
35113
|
-
* asking
|
|
35114
|
-
* pages are not children of this box
|
|
35207
|
+
* @param {Array<object|{route: object, params?: object}>} [props.routes] - the
|
|
35208
|
+
* tabs, in the order they are shown. Read from the <Route> children by
|
|
35209
|
+
* default, in the order they are written: the router already holds that list,
|
|
35210
|
+
* and asking a caller to write it twice is asking for the two to disagree.
|
|
35211
|
+
* Pass it to say another order, when the pages are not children of this box,
|
|
35212
|
+
* or to name a tab the children cannot — the section a <Route fallback> shows
|
|
35213
|
+
* is a tab like the others, and only its params say which one.
|
|
35214
|
+
*
|
|
35215
|
+
* An entry is a route, or `{ route, params }` when the tabs of the row are a
|
|
35216
|
+
* PARAM of one route rather than routes of their own (the form
|
|
35217
|
+
* `<Route routeParams>` selects a branch on, and the form that lets a link
|
|
35218
|
+
* with no params reopen the section one was looking at). Written as bare
|
|
35219
|
+
* routes, three tabs of one route are the same object three times: there is
|
|
35220
|
+
* then one tab, and nowhere to travel.
|
|
35115
35221
|
* @param {"x"|"y"} [props.axis="x"] - which way the pages are laid out.
|
|
35116
35222
|
* @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
|
|
35117
35223
|
* page travels. Off where the gesture belongs to the content.
|
|
35118
|
-
* @param {(detail: {route: object, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
35119
|
-
* - how to go to a
|
|
35224
|
+
* @param {(detail: {route: object, params: object|undefined, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
35225
|
+
* - how to go to a tab. The default REPLACES the current history entry
|
|
35120
35226
|
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
35121
35227
|
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
35122
35228
|
* page under six entries. A tab pressed is the other case and pushes, which
|
|
@@ -35139,8 +35245,9 @@ const RouteTravel = ({
|
|
|
35139
35245
|
axis = "x",
|
|
35140
35246
|
travelByDrag = true,
|
|
35141
35247
|
onTravel = ({
|
|
35142
|
-
route
|
|
35143
|
-
|
|
35248
|
+
route,
|
|
35249
|
+
params
|
|
35250
|
+
}) => route.redirectTo(params),
|
|
35144
35251
|
className,
|
|
35145
35252
|
children,
|
|
35146
35253
|
...rest
|
|
@@ -35152,51 +35259,58 @@ const RouteTravel = ({
|
|
|
35152
35259
|
// left, the animations the finger drives, and what to do with them once the
|
|
35153
35260
|
// browser has them ready. Null when no page is on its way anywhere.
|
|
35154
35261
|
const travelRef = useRef(null);
|
|
35155
|
-
// The
|
|
35262
|
+
// The page this box has ASKED for and is still waiting to see arrive.
|
|
35156
35263
|
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
35157
35264
|
// travel decided anything about it — sometimes after the travel was undone —
|
|
35158
|
-
// and read back as "the
|
|
35265
|
+
// and read back as "the page changed" it would start a second travel nobody
|
|
35159
35266
|
// asked for, over pictures that are already showing something else.
|
|
35160
|
-
const
|
|
35267
|
+
const pageAskedForRef = useRef(null);
|
|
35161
35268
|
// What a press stopped in flight, until the gesture says what it is about.
|
|
35162
35269
|
const caughtAtPressRef = useRef(null);
|
|
35163
35270
|
// The latest way to answer a gesture, for a watcher that outlives every
|
|
35164
35271
|
// render (see the wheel effect below).
|
|
35165
35272
|
const travelHandlersRef = useRef(null);
|
|
35166
35273
|
const pointerDownRef = useRef(null);
|
|
35167
|
-
const
|
|
35168
|
-
const
|
|
35169
|
-
|
|
35170
|
-
|
|
35171
|
-
// is
|
|
35172
|
-
|
|
35173
|
-
for
|
|
35174
|
-
|
|
35175
|
-
|
|
35176
|
-
}
|
|
35177
|
-
}
|
|
35274
|
+
const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
|
|
35275
|
+
const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
|
|
35276
|
+
const pages = pagesFromProp || pagesFromChildren;
|
|
35277
|
+
|
|
35278
|
+
// Which page is on screen, read from the pages themselves: every one of them
|
|
35279
|
+
// is read, so this re-renders when any of them starts or stops matching — and
|
|
35280
|
+
// for a row whose tabs are params of one route, when the params move from one
|
|
35281
|
+
// tab to the next (see pageIsCurrent).
|
|
35282
|
+
const currentIndex = currentPageIndex(pages);
|
|
35178
35283
|
// The page that was on screen when the change now happening was asked for:
|
|
35179
35284
|
// a travel is between two of them, and by the time anything renders the first
|
|
35180
35285
|
// one is already gone. Written after each render (below), so a subscriber
|
|
35181
35286
|
// reading it — they all run before Preact flushes — reads the one being left.
|
|
35182
35287
|
const currentIndexRef = useRef(currentIndex);
|
|
35183
35288
|
|
|
35289
|
+
// Where a page is asked for, whoever asks: a page is a route AND the params
|
|
35290
|
+
// that say which of its tabs, and a caller told only the route would send the
|
|
35291
|
+
// row back to whichever tab the URL already says (see redirectTo).
|
|
35292
|
+
const travelTo = (page, cause) => onTravel({
|
|
35293
|
+
route: page.route,
|
|
35294
|
+
params: page.params,
|
|
35295
|
+
cause
|
|
35296
|
+
});
|
|
35297
|
+
|
|
35184
35298
|
// One travel, whoever asked for it: a finger, a tab pressed, the browser's
|
|
35185
35299
|
// own back button. What differs is only who moves it — the finger drives it
|
|
35186
35300
|
// frame by frame (`scrub`), everything else lets it play.
|
|
35187
35301
|
const beginTravel = ({
|
|
35188
|
-
|
|
35189
|
-
|
|
35302
|
+
page,
|
|
35303
|
+
fromPage,
|
|
35190
35304
|
direction,
|
|
35191
35305
|
scrub,
|
|
35192
35306
|
change
|
|
35193
35307
|
}) => {
|
|
35194
35308
|
const travel = {
|
|
35195
|
-
|
|
35309
|
+
page,
|
|
35196
35310
|
// The page this set off from, kept rather than looked up again: the URL
|
|
35197
35311
|
// changes at the first pixel, so a moment later nothing on screen
|
|
35198
35312
|
// remembers where it started.
|
|
35199
|
-
|
|
35313
|
+
fromPage,
|
|
35200
35314
|
direction,
|
|
35201
35315
|
scrub,
|
|
35202
35316
|
ratio: 0,
|
|
@@ -35207,12 +35321,16 @@ const RouteTravel = ({
|
|
|
35207
35321
|
ended: false
|
|
35208
35322
|
};
|
|
35209
35323
|
travelRef.current = travel;
|
|
35324
|
+
// Taken before the picture is: the browser reads the name off the DOM as it
|
|
35325
|
+
// stands when the transition starts, and this box is only a picture of its
|
|
35326
|
+
// own for as long as it is the one travelling.
|
|
35327
|
+
nameForTravel(elementRef.current);
|
|
35210
35328
|
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
35211
35329
|
if (scrub) {
|
|
35212
35330
|
holdPictures(travel);
|
|
35213
35331
|
document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
|
|
35214
35332
|
}
|
|
35215
|
-
|
|
35333
|
+
pageAskedForRef.current = page;
|
|
35216
35334
|
// The box as it stands before anything moves: rendering is held, so this is
|
|
35217
35335
|
// still the page being left (see holdTravelHeight).
|
|
35218
35336
|
const heightBefore = elementRef.current.getBoundingClientRect().height;
|
|
@@ -35223,7 +35341,7 @@ const RouteTravel = ({
|
|
|
35223
35341
|
// The picture the browser is about to take must be of the page that was
|
|
35224
35342
|
// asked for, and a route matching is not yet a page rendered.
|
|
35225
35343
|
const viewTransition = startViewTransition(async () => {
|
|
35226
|
-
await
|
|
35344
|
+
await whilePageRenders(page, async () => {
|
|
35227
35345
|
releaseRendering();
|
|
35228
35346
|
if (change) {
|
|
35229
35347
|
await change();
|
|
@@ -35276,21 +35394,41 @@ const RouteTravel = ({
|
|
|
35276
35394
|
};
|
|
35277
35395
|
|
|
35278
35396
|
// A page change nobody here asked for: a tab pressed, a key, the back button.
|
|
35279
|
-
// The transition is started from the
|
|
35280
|
-
//
|
|
35281
|
-
//
|
|
35397
|
+
// The transition is started from what the router SAYS rather than from a
|
|
35398
|
+
// render, because a render is one flush too late — by then the DOM holds the
|
|
35399
|
+
// new page and the picture of the old one cannot be taken anymore.
|
|
35400
|
+
//
|
|
35401
|
+
// Watched as a position in the row rather than route by route. A route
|
|
35402
|
+
// announces its own status, and the row's tabs can all be one route: the
|
|
35403
|
+
// announcement then says a section changed without saying which is on screen,
|
|
35404
|
+
// and it says it about things this row does not move for (a route that has
|
|
35405
|
+
// now been visited, a param of its own that is not a tab of this row). Worse,
|
|
35406
|
+
// a status is published from inside the routing and the params it carries are
|
|
35407
|
+
// the ones known at that instant — a section that lands as a signal settles
|
|
35408
|
+
// is announced late, or not at all. The signals ARE the position, so the
|
|
35409
|
+
// position is read from them: one computed over the whole row, notified once
|
|
35410
|
+
// per move, whichever route moved and whether by matching or by params.
|
|
35282
35411
|
useLayoutEffect(() => {
|
|
35283
|
-
const
|
|
35284
|
-
|
|
35285
|
-
|
|
35286
|
-
if (!matching) {
|
|
35412
|
+
const currentIndexSignal = computed(() => currentPageIndex(pages));
|
|
35413
|
+
const onRowMove = index => {
|
|
35414
|
+
if (index === -1) {
|
|
35287
35415
|
return;
|
|
35288
35416
|
}
|
|
35417
|
+
if (index === currentIndexRef.current) {
|
|
35418
|
+
// Where the row already was — the first reading of all, and any move
|
|
35419
|
+
// this box has already taken note of (a render writes it too). What it
|
|
35420
|
+
// was still waiting for is here nonetheless, so the wait is called off.
|
|
35421
|
+
if (samePage(pageAskedForRef.current, pages[index])) {
|
|
35422
|
+
pageAskedForRef.current = null;
|
|
35423
|
+
}
|
|
35424
|
+
return;
|
|
35425
|
+
}
|
|
35426
|
+
const page = pages[index];
|
|
35289
35427
|
// A page this box asked for itself — a travel's own navigation, or one
|
|
35290
35428
|
// it had given up waiting on: what arrives here is the answer to a
|
|
35291
35429
|
// question already answered, not somebody going somewhere.
|
|
35292
|
-
if (
|
|
35293
|
-
|
|
35430
|
+
if (samePage(pageAskedForRef.current, page)) {
|
|
35431
|
+
pageAskedForRef.current = null;
|
|
35294
35432
|
currentIndexRef.current = index;
|
|
35295
35433
|
return;
|
|
35296
35434
|
}
|
|
@@ -35298,13 +35436,13 @@ const RouteTravel = ({
|
|
|
35298
35436
|
// is not coming, or no longer means anything. Forgotten here rather
|
|
35299
35437
|
// than kept, or the next press on that very tab would be taken for the
|
|
35300
35438
|
// late answer to a question nobody remembers asking.
|
|
35301
|
-
|
|
35439
|
+
pageAskedForRef.current = null;
|
|
35302
35440
|
// Asked for a page while one was already on its way: the travel in
|
|
35303
35441
|
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
35304
35442
|
// top would leave this one's pictures to be dropped mid-slide.
|
|
35305
35443
|
if (travelRef.current) {
|
|
35306
35444
|
currentIndexRef.current = index;
|
|
35307
|
-
retargetTravel(travelRef.current,
|
|
35445
|
+
retargetTravel(travelRef.current, page);
|
|
35308
35446
|
return;
|
|
35309
35447
|
}
|
|
35310
35448
|
const fromIndex = currentIndexRef.current;
|
|
@@ -35315,18 +35453,18 @@ const RouteTravel = ({
|
|
|
35315
35453
|
return;
|
|
35316
35454
|
}
|
|
35317
35455
|
beginTravel({
|
|
35318
|
-
|
|
35319
|
-
|
|
35456
|
+
page,
|
|
35457
|
+
fromPage: pages[fromIndex],
|
|
35320
35458
|
direction: index > fromIndex ? "forward" : "back",
|
|
35321
35459
|
scrub: false
|
|
35322
35460
|
});
|
|
35323
|
-
}));
|
|
35324
|
-
return () => {
|
|
35325
|
-
for (const unsubscribe of unsubscribes) {
|
|
35326
|
-
unsubscribe();
|
|
35327
|
-
}
|
|
35328
35461
|
};
|
|
35329
|
-
|
|
35462
|
+
// `subscribe` rather than `effect`: it hands the value to a callback that
|
|
35463
|
+
// is NOT being tracked, and what this one does — navigate, ask the router
|
|
35464
|
+
// for another page — reads and writes the very signals the row is watched
|
|
35465
|
+
// through.
|
|
35466
|
+
return currentIndexSignal.subscribe(onRowMove);
|
|
35467
|
+
}, [pages]);
|
|
35330
35468
|
|
|
35331
35469
|
// Rendering is held for the length of a navigation, so that whatever picture
|
|
35332
35470
|
// this box is about to take is of the page being LEFT (see holdRendering).
|
|
@@ -35363,6 +35501,13 @@ const RouteTravel = ({
|
|
|
35363
35501
|
// Let go of far enough: the movement carries on from under the finger, at its
|
|
35364
35502
|
// own pace, to the end.
|
|
35365
35503
|
const finishTravel = travel => {
|
|
35504
|
+
// Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
|
|
35505
|
+
// set them off: left standing after the release, this travel would go on
|
|
35506
|
+
// claiming a hand that is no longer there — and everything that asks
|
|
35507
|
+
// "is somebody holding this?" before touching it (a press on the tab one
|
|
35508
|
+
// came from, a wheel push) would be answered yes and do nothing, while the
|
|
35509
|
+
// pages carry on to a page the router has already left.
|
|
35510
|
+
travel.scrub = false;
|
|
35366
35511
|
releaseHold();
|
|
35367
35512
|
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
35368
35513
|
};
|
|
@@ -35415,19 +35560,20 @@ const RouteTravel = ({
|
|
|
35415
35560
|
Promise.resolve();
|
|
35416
35561
|
backAtTheStart.then(async () => {
|
|
35417
35562
|
try {
|
|
35418
|
-
|
|
35563
|
+
pageAskedForRef.current = travel.fromPage;
|
|
35419
35564
|
// The page that was left is put back UNDER the picture before the
|
|
35420
35565
|
// picture is dropped, so the two are the same thing at the moment they
|
|
35421
35566
|
// are swapped: that only holds once the page is really back.
|
|
35422
|
-
if (travel.
|
|
35567
|
+
if (pageIsCurrent(travel.fromPage)) {
|
|
35423
35568
|
// It never left: the press that set this revert off put it back
|
|
35424
35569
|
// there, and the pages have been held where they were until now.
|
|
35425
35570
|
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
35426
35571
|
// render that never comes and a page frozen under its own pictures.
|
|
35427
35572
|
releaseRendering();
|
|
35428
35573
|
} else {
|
|
35429
|
-
await
|
|
35430
|
-
route: travel.
|
|
35574
|
+
await whilePageRenders(travel.fromPage, () => onTravel({
|
|
35575
|
+
route: travel.fromPage.route,
|
|
35576
|
+
params: travel.fromPage.params,
|
|
35431
35577
|
cause: "revert"
|
|
35432
35578
|
}));
|
|
35433
35579
|
}
|
|
@@ -35466,8 +35612,8 @@ const RouteTravel = ({
|
|
|
35466
35612
|
// change — only what is being brought in against it, and that one is LIVE:
|
|
35467
35613
|
// pointing the router elsewhere is all it takes for the picture to show that
|
|
35468
35614
|
// page instead.
|
|
35469
|
-
const redirectTravel = (travel,
|
|
35470
|
-
travel.
|
|
35615
|
+
const redirectTravel = (travel, page, direction) => {
|
|
35616
|
+
travel.page = page;
|
|
35471
35617
|
// Everything the transition carries that is NOT the pages was measured
|
|
35472
35618
|
// against a destination this travel is no longer going to (see the CSS).
|
|
35473
35619
|
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
@@ -35488,28 +35634,28 @@ const RouteTravel = ({
|
|
|
35488
35634
|
|
|
35489
35635
|
// Somebody asked for a page while one was on its way. Where they asked for
|
|
35490
35636
|
// decides what that means.
|
|
35491
|
-
const retargetTravel = (travel,
|
|
35637
|
+
const retargetTravel = (travel, page) => {
|
|
35492
35638
|
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
35493
35639
|
// A hand is holding the pages, or they are already on their way back:
|
|
35494
35640
|
// either way this travel's end is decided by somebody else.
|
|
35495
35641
|
return;
|
|
35496
35642
|
}
|
|
35497
|
-
if (
|
|
35643
|
+
if (samePage(page, travel.page)) {
|
|
35498
35644
|
// Already on its way there.
|
|
35499
35645
|
return;
|
|
35500
35646
|
}
|
|
35501
|
-
if (
|
|
35647
|
+
if (samePage(page, travel.fromPage)) {
|
|
35502
35648
|
// Back where it set off from: that is not another travel, it is this one
|
|
35503
35649
|
// undone — the same pictures, run backwards.
|
|
35504
35650
|
revertTravel(travel);
|
|
35505
35651
|
return;
|
|
35506
35652
|
}
|
|
35507
|
-
const fromIndex =
|
|
35508
|
-
const toIndex =
|
|
35653
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35654
|
+
const toIndex = pageIndexOf(pages, page);
|
|
35509
35655
|
if (fromIndex === -1 || toIndex === -1) {
|
|
35510
35656
|
return;
|
|
35511
35657
|
}
|
|
35512
|
-
redirectTravel(travel,
|
|
35658
|
+
redirectTravel(travel, page, toIndex > fromIndex ? "forward" : "back");
|
|
35513
35659
|
};
|
|
35514
35660
|
const endTravel = travel => {
|
|
35515
35661
|
if (travel.ended) {
|
|
@@ -35523,6 +35669,10 @@ const RouteTravel = ({
|
|
|
35523
35669
|
releaseHold(travel);
|
|
35524
35670
|
if (travelRef.current === travel) {
|
|
35525
35671
|
travelRef.current = null;
|
|
35672
|
+
// Given back, so another box may wear it: kept, two of them on a page
|
|
35673
|
+
// would both answer to it and the browser refuses the whole transition
|
|
35674
|
+
// rather than pick.
|
|
35675
|
+
unnameAfterTravel(elementRef.current);
|
|
35526
35676
|
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
35527
35677
|
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
35528
35678
|
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
@@ -35587,24 +35737,21 @@ const RouteTravel = ({
|
|
|
35587
35737
|
}
|
|
35588
35738
|
// Dragging the page towards the end of the axis brings in what is
|
|
35589
35739
|
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
35590
|
-
const
|
|
35591
|
-
if (!
|
|
35740
|
+
const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
35741
|
+
if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
35592
35742
|
return false;
|
|
35593
35743
|
}
|
|
35594
35744
|
if (CAN_KEEP_PICTURE) {
|
|
35595
35745
|
beginTravel({
|
|
35596
|
-
|
|
35597
|
-
|
|
35746
|
+
page,
|
|
35747
|
+
fromPage: pages[currentIndex],
|
|
35598
35748
|
direction: sign > 0 ? "back" : "forward",
|
|
35599
35749
|
scrub: true,
|
|
35600
|
-
change: () =>
|
|
35601
|
-
route,
|
|
35602
|
-
cause: "drag"
|
|
35603
|
-
})
|
|
35750
|
+
change: () => travelTo(page, "drag")
|
|
35604
35751
|
});
|
|
35605
35752
|
} else {
|
|
35606
35753
|
travelRef.current = {
|
|
35607
|
-
|
|
35754
|
+
page,
|
|
35608
35755
|
noPicture: true,
|
|
35609
35756
|
ended: false
|
|
35610
35757
|
};
|
|
@@ -35656,9 +35803,9 @@ const RouteTravel = ({
|
|
|
35656
35803
|
// Where we are is what THIS travel was bringing in — the URL changed at
|
|
35657
35804
|
// the first pixel, so `currentIndex` belongs to a render this gesture
|
|
35658
35805
|
// is older than.
|
|
35659
|
-
const fromIndex =
|
|
35660
|
-
const
|
|
35661
|
-
if (fromIndex === -1 || !
|
|
35806
|
+
const fromIndex = pageIndexOf(pages, travel.page);
|
|
35807
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35808
|
+
if (fromIndex === -1 || !page) {
|
|
35662
35809
|
return false;
|
|
35663
35810
|
}
|
|
35664
35811
|
// At their very end before they are let go of: what ends the travel in
|
|
@@ -35668,14 +35815,11 @@ const RouteTravel = ({
|
|
|
35668
35815
|
scrubTravel(travel, 1);
|
|
35669
35816
|
travel.ratio = 1;
|
|
35670
35817
|
beginTravel({
|
|
35671
|
-
|
|
35672
|
-
|
|
35818
|
+
page,
|
|
35819
|
+
fromPage: travel.page,
|
|
35673
35820
|
direction,
|
|
35674
35821
|
scrub: true,
|
|
35675
|
-
change: () =>
|
|
35676
|
-
route,
|
|
35677
|
-
cause: "drag"
|
|
35678
|
-
})
|
|
35822
|
+
change: () => travelTo(page, "drag")
|
|
35679
35823
|
});
|
|
35680
35824
|
return {
|
|
35681
35825
|
size: boxSizeOnAxis(),
|
|
@@ -35690,17 +35834,14 @@ const RouteTravel = ({
|
|
|
35690
35834
|
// other neighbour is enough for it to show that one instead. The travel
|
|
35691
35835
|
// turns around where it stands, on the same transition and under the same
|
|
35692
35836
|
// hand, and there is no gap at all.
|
|
35693
|
-
const fromIndex =
|
|
35694
|
-
const
|
|
35695
|
-
if (fromIndex === -1 || !
|
|
35837
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35838
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35839
|
+
if (fromIndex === -1 || !page) {
|
|
35696
35840
|
return false;
|
|
35697
35841
|
}
|
|
35698
|
-
redirectTravel(travel,
|
|
35699
|
-
|
|
35700
|
-
|
|
35701
|
-
route,
|
|
35702
|
-
cause: "drag"
|
|
35703
|
-
});
|
|
35842
|
+
redirectTravel(travel, page, direction);
|
|
35843
|
+
pageAskedForRef.current = page;
|
|
35844
|
+
travelTo(page, "drag");
|
|
35704
35845
|
return {
|
|
35705
35846
|
size: boxSizeOnAxis(),
|
|
35706
35847
|
travelBack: sign > 0,
|
|
@@ -35723,10 +35864,7 @@ const RouteTravel = ({
|
|
|
35723
35864
|
if (travel.noPicture) {
|
|
35724
35865
|
travelRef.current = null;
|
|
35725
35866
|
if (travels) {
|
|
35726
|
-
|
|
35727
|
-
route: travel.route,
|
|
35728
|
-
cause: "drag"
|
|
35729
|
-
});
|
|
35867
|
+
travelTo(travel.page, "drag");
|
|
35730
35868
|
}
|
|
35731
35869
|
return;
|
|
35732
35870
|
}
|
|
@@ -35758,6 +35896,16 @@ const RouteTravel = ({
|
|
|
35758
35896
|
if (currentIndex === -1) {
|
|
35759
35897
|
return;
|
|
35760
35898
|
}
|
|
35899
|
+
// A press that never became a gesture: whatever it stopped goes on its way,
|
|
35900
|
+
// from where the finger caught it.
|
|
35901
|
+
const giveUp = () => {
|
|
35902
|
+
gestureRef.current = null;
|
|
35903
|
+
const caught = caughtAtPressRef.current;
|
|
35904
|
+
caughtAtPressRef.current = null;
|
|
35905
|
+
if (caught && !caught.ended) {
|
|
35906
|
+
releaseHold(caught);
|
|
35907
|
+
}
|
|
35908
|
+
};
|
|
35761
35909
|
const gesture = startDragToTravel(pointerDownEvent, {
|
|
35762
35910
|
element: elementRef.current,
|
|
35763
35911
|
axes: axis,
|
|
@@ -35765,17 +35913,14 @@ const RouteTravel = ({
|
|
|
35765
35913
|
// is answered from its first pixel, on the axis the pages travel.
|
|
35766
35914
|
immediate: caughtAtPressRef.current ? axis : false,
|
|
35767
35915
|
...travelHandlers,
|
|
35768
|
-
onGiveUp:
|
|
35769
|
-
gestureRef.current = null;
|
|
35770
|
-
// A press that never became a gesture: whatever it stopped goes on its
|
|
35771
|
-
// way, from where the finger caught it.
|
|
35772
|
-
const caught = caughtAtPressRef.current;
|
|
35773
|
-
caughtAtPressRef.current = null;
|
|
35774
|
-
if (caught && !caught.ended) {
|
|
35775
|
-
releaseHold(caught);
|
|
35776
|
-
}
|
|
35777
|
-
}
|
|
35916
|
+
onGiveUp: giveUp
|
|
35778
35917
|
});
|
|
35918
|
+
if (!gesture) {
|
|
35919
|
+
// Not a press this box can be about — something that reads the pointer
|
|
35920
|
+
// itself, a box below it that travels the same way.
|
|
35921
|
+
giveUp();
|
|
35922
|
+
return;
|
|
35923
|
+
}
|
|
35779
35924
|
gestureRef.current = gesture;
|
|
35780
35925
|
};
|
|
35781
35926
|
|
|
@@ -35827,13 +35972,13 @@ const RouteTravel = ({
|
|
|
35827
35972
|
}
|
|
35828
35973
|
// Where the box is going, which is not where it is: a step asked for while
|
|
35829
35974
|
// a travel plays is the page after the one on its way.
|
|
35830
|
-
const
|
|
35831
|
-
const fromIndex =
|
|
35975
|
+
const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
|
|
35976
|
+
const fromIndex = pageIndexOf(pages, fromPage);
|
|
35832
35977
|
if (fromIndex === -1) {
|
|
35833
35978
|
return;
|
|
35834
35979
|
}
|
|
35835
|
-
const
|
|
35836
|
-
if (!
|
|
35980
|
+
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35981
|
+
if (!page) {
|
|
35837
35982
|
return;
|
|
35838
35983
|
}
|
|
35839
35984
|
if (travelInFlight) {
|
|
@@ -35844,14 +35989,11 @@ const RouteTravel = ({
|
|
|
35844
35989
|
travelInFlight.ratio = 1;
|
|
35845
35990
|
}
|
|
35846
35991
|
beginTravel({
|
|
35847
|
-
|
|
35848
|
-
|
|
35992
|
+
page,
|
|
35993
|
+
fromPage,
|
|
35849
35994
|
direction: sign > 0 ? "back" : "forward",
|
|
35850
35995
|
scrub: false,
|
|
35851
|
-
change: () =>
|
|
35852
|
-
route,
|
|
35853
|
-
cause: "wheel"
|
|
35854
|
-
})
|
|
35996
|
+
change: () => travelTo(page, "wheel")
|
|
35855
35997
|
});
|
|
35856
35998
|
};
|
|
35857
35999
|
|
|
@@ -35882,7 +36024,15 @@ const RouteTravel = ({
|
|
|
35882
36024
|
// page (see drag_to_travel.js).
|
|
35883
36025
|
,
|
|
35884
36026
|
|
|
35885
|
-
"data-drag-travel": travelByDrag ? axis : undefined
|
|
36027
|
+
"data-drag-travel": travelByDrag ? axis : undefined
|
|
36028
|
+
// The same fact said once per gesture, and for the other question the
|
|
36029
|
+
// DOM answers: a box that travels INSIDE this one — a row of slides in a
|
|
36030
|
+
// page — takes the axis it walks, and these are what it reads to know
|
|
36031
|
+
// this box walks it too.
|
|
36032
|
+
,
|
|
36033
|
+
|
|
36034
|
+
"data-travel-by-drag": travelByDrag ? axis : undefined,
|
|
36035
|
+
"data-travel-by-wheel": travelByDrag ? axis : undefined,
|
|
35886
36036
|
onPointerDown: onPointerDown,
|
|
35887
36037
|
children: children
|
|
35888
36038
|
});
|
|
@@ -36076,7 +36226,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
36076
36226
|
}
|
|
36077
36227
|
};
|
|
36078
36228
|
|
|
36079
|
-
// A
|
|
36229
|
+
// A page change, carried out and then waited for until the page it selects is
|
|
36080
36230
|
// really on screen. The container doing the swapping is the only one who knows
|
|
36081
36231
|
// when that is (observeRouteRender): a route matching is a signal changing, and
|
|
36082
36232
|
// how many passes Preact takes to answer it is its own business.
|
|
@@ -36086,7 +36236,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
36086
36236
|
// inside the callback of a view transition: the browser has stopped rendering
|
|
36087
36237
|
// and is waiting on this very promise to take its picture, so a wait that never
|
|
36088
36238
|
// ends is a page frozen under a transition that never became ready.
|
|
36089
|
-
const
|
|
36239
|
+
const whilePageRenders = async (page, change) => {
|
|
36090
36240
|
let stopListening;
|
|
36091
36241
|
const rendered = new Promise(resolve => {
|
|
36092
36242
|
// Listened for before the change, or a render landing while the change is
|
|
@@ -36095,7 +36245,7 @@ const whileRouteRenders = async (route, change) => {
|
|
|
36095
36245
|
});
|
|
36096
36246
|
try {
|
|
36097
36247
|
await change();
|
|
36098
|
-
if (
|
|
36248
|
+
if (pageIsCurrent(page)) {
|
|
36099
36249
|
await rendered;
|
|
36100
36250
|
}
|
|
36101
36251
|
} finally {
|
|
@@ -36103,9 +36253,76 @@ const whileRouteRenders = async (route, change) => {
|
|
|
36103
36253
|
}
|
|
36104
36254
|
};
|
|
36105
36255
|
|
|
36256
|
+
// A page of the row: a route, and the params that say which of its tabs when
|
|
36257
|
+
// several of them share it. Written as a bare route by a caller whose tabs are
|
|
36258
|
+
// routes of their own — which is the same page with nothing to tell apart.
|
|
36259
|
+
const normalizePage = page => page.isRoute ? {
|
|
36260
|
+
route: page,
|
|
36261
|
+
params: undefined
|
|
36262
|
+
} : page;
|
|
36263
|
+
|
|
36264
|
+
// Two pages are the same page when they select the same thing, not when they
|
|
36265
|
+
// were written by the same hand: the params of a tab are a literal in JSX, so
|
|
36266
|
+
// every render builds another object for what is plainly the same tab.
|
|
36267
|
+
const samePage = (a, b) => {
|
|
36268
|
+
if (a === b) {
|
|
36269
|
+
return true;
|
|
36270
|
+
}
|
|
36271
|
+
if (!a || !b) {
|
|
36272
|
+
return false;
|
|
36273
|
+
}
|
|
36274
|
+
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
36275
|
+
};
|
|
36276
|
+
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
|
|
36277
|
+
|
|
36278
|
+
// Whether this page is the one on screen. `matchesParams` reads paramsSignal,
|
|
36279
|
+
// so a caller reading this during a render is subscribed to the param changes
|
|
36280
|
+
// that walk from one tab to the next — matchingSignal alone never moves there,
|
|
36281
|
+
// and a row whose tabs are params of one route would never re-render.
|
|
36282
|
+
//
|
|
36283
|
+
// The params are read only for a route that matches, and that is not a signal
|
|
36284
|
+
// left unread: a reader wakes on anything it read last time, so what matters is
|
|
36285
|
+
// that everything able to make this answer change is among them.
|
|
36286
|
+
// matchingSignal is read whatever happens, and it is a NECESSARY condition —
|
|
36287
|
+
// while it is false no param of that route can put this page on screen, and the
|
|
36288
|
+
// day one could, matchingSignal itself has to turn true to say so, which is the
|
|
36289
|
+
// read that brings the params back in. (Asking anyway would be worse than
|
|
36290
|
+
// useless: the params of a route that does not match are not params.)
|
|
36291
|
+
const pageIsCurrent = ({
|
|
36292
|
+
route,
|
|
36293
|
+
params
|
|
36294
|
+
}) => {
|
|
36295
|
+
if (!route.matchingSignal.value) {
|
|
36296
|
+
return false;
|
|
36297
|
+
}
|
|
36298
|
+
return params ? route.matchesParams(params) : true;
|
|
36299
|
+
};
|
|
36300
|
+
// Every page is read, never only up to the one that answers yes: a page that is
|
|
36301
|
+
// not the current one today is the one that must wake the reader tomorrow.
|
|
36302
|
+
const currentPageIndex = pages => {
|
|
36303
|
+
let currentIndex = -1;
|
|
36304
|
+
for (let i = 0; i < pages.length; i++) {
|
|
36305
|
+
if (pageIsCurrent(pages[i])) {
|
|
36306
|
+
currentIndex = i;
|
|
36307
|
+
}
|
|
36308
|
+
}
|
|
36309
|
+
return currentIndex;
|
|
36310
|
+
};
|
|
36311
|
+
|
|
36106
36312
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
36107
36313
|
const ignoreSkipped = () => {};
|
|
36108
36314
|
|
|
36315
|
+
// The name is lent to the box that is travelling and taken back afterwards.
|
|
36316
|
+
// There is one transition in a document at a time, so one box wears it at a
|
|
36317
|
+
// time — and the others, unnamed, are simply not captured: they stay live
|
|
36318
|
+
// under the pictures rather than being frozen with the page.
|
|
36319
|
+
const nameForTravel = element => {
|
|
36320
|
+
element.style.viewTransitionName = TRAVEL_NAME;
|
|
36321
|
+
};
|
|
36322
|
+
const unnameAfterTravel = element => {
|
|
36323
|
+
element.style.viewTransitionName = "";
|
|
36324
|
+
};
|
|
36325
|
+
|
|
36109
36326
|
const routeAction = (
|
|
36110
36327
|
routeOrRoutes,
|
|
36111
36328
|
action,
|
|
@@ -38068,8 +38285,10 @@ const BinderItemContext = createContext(null);
|
|
|
38068
38285
|
|
|
38069
38286
|
/**
|
|
38070
38287
|
* What a <Link> learns from the <Nav> around it: where to draw the bar that
|
|
38071
|
-
* says "you are here",
|
|
38072
|
-
*
|
|
38288
|
+
* says "you are here", the name under which the browser is to recognise that
|
|
38289
|
+
* bar from one page to the next, and — for a row of tabs that are slides — which
|
|
38290
|
+
* <SlideContainer> they are about and which of its slides is on screen (see
|
|
38291
|
+
* nav.jsx).
|
|
38073
38292
|
*/
|
|
38074
38293
|
const NavContext = createContext(null);
|
|
38075
38294
|
|
|
@@ -38502,6 +38721,12 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
38502
38721
|
* instead of a raw `href`: the URL is built from the route (see
|
|
38503
38722
|
* `routeParams`) and "current" is derived from whether the route matches.
|
|
38504
38723
|
* @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
|
|
38724
|
+
* @param {string} [props.slide] - Makes this a tab for a slide rather than for
|
|
38725
|
+
* a URL: the area of a `<SlideContainer>` it goes to. The container is the one
|
|
38726
|
+
* the surrounding `<Nav slideContainer={id}>` names, and it is also what says
|
|
38727
|
+
* whether this tab is the current one. Nothing is written to the URL — these
|
|
38728
|
+
* are places within one screen, not pages of their own — so there is no href
|
|
38729
|
+
* and the tab behaves like a button.
|
|
38505
38730
|
* @param {string} [props.target] - Native anchor target; defaults from
|
|
38506
38731
|
* internal/external detection when omitted.
|
|
38507
38732
|
* @param {string} [props.rel] - Native anchor rel; defaults to
|
|
@@ -38591,6 +38816,7 @@ const LinkPlain = props => {
|
|
|
38591
38816
|
target,
|
|
38592
38817
|
rel,
|
|
38593
38818
|
anchor,
|
|
38819
|
+
slide,
|
|
38594
38820
|
value = href,
|
|
38595
38821
|
// visual
|
|
38596
38822
|
variant,
|
|
@@ -38641,7 +38867,9 @@ const LinkPlain = props => {
|
|
|
38641
38867
|
isAnchor,
|
|
38642
38868
|
isCurrent
|
|
38643
38869
|
} = getHrefTargetInfo(href);
|
|
38644
|
-
|
|
38870
|
+
// A tab that is a SLIDE is current when the container is on it — which the
|
|
38871
|
+
// <Nav> around reads off that container, so nothing here has to be told.
|
|
38872
|
+
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
38645
38873
|
useReportCurrentToBinderItem(innerCurrent);
|
|
38646
38874
|
controlHostProps.basePseudoState = {
|
|
38647
38875
|
...basePseudoState,
|
|
@@ -38728,6 +38956,12 @@ const LinkPlain = props => {
|
|
|
38728
38956
|
onClick,
|
|
38729
38957
|
preventDefault
|
|
38730
38958
|
} = props;
|
|
38959
|
+
// Travelling there is the container's business, said as the command anything
|
|
38960
|
+
// else in the page would say it with: the tab knows the name of a slide and
|
|
38961
|
+
// the id of the box, and nothing more about either.
|
|
38962
|
+
const goToSlide = (element, event) => {
|
|
38963
|
+
triggerNaviCommand(element, `--navi-go-to-slide:${slide}`, event);
|
|
38964
|
+
};
|
|
38731
38965
|
return jsxs(Text, {
|
|
38732
38966
|
as: "a",
|
|
38733
38967
|
color: anchor && !innerChildren ? "inherit" : undefined,
|
|
@@ -38740,6 +38974,7 @@ const LinkPlain = props => {
|
|
|
38740
38974
|
// was handed.
|
|
38741
38975
|
preventDefault: undefined,
|
|
38742
38976
|
anchor: undefined,
|
|
38977
|
+
slide: undefined,
|
|
38743
38978
|
revealOnInteraction: undefined,
|
|
38744
38979
|
variant: undefined,
|
|
38745
38980
|
current: undefined,
|
|
@@ -38753,15 +38988,43 @@ const LinkPlain = props => {
|
|
|
38753
38988
|
hrefFallback: undefined,
|
|
38754
38989
|
onClick: e => {
|
|
38755
38990
|
onClick?.(e);
|
|
38991
|
+
if (slide) {
|
|
38992
|
+
goToSlide(e.currentTarget, e);
|
|
38993
|
+
}
|
|
38756
38994
|
if (preventDefault) {
|
|
38757
38995
|
e.preventDefault();
|
|
38758
38996
|
}
|
|
38997
|
+
}
|
|
38998
|
+
// A tab with no href is not a link the browser knows how to press: it is
|
|
38999
|
+
// focusable because it says so (tabIndex below) and it answers the two
|
|
39000
|
+
// keys a button answers, since that is what it behaves like.
|
|
39001
|
+
,
|
|
39002
|
+
|
|
39003
|
+
onKeyDown: e => {
|
|
39004
|
+
props.onKeyDown?.(e);
|
|
39005
|
+
if (!slide || e.defaultPrevented) {
|
|
39006
|
+
return;
|
|
39007
|
+
}
|
|
39008
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
39009
|
+
e.preventDefault();
|
|
39010
|
+
goToSlide(e.currentTarget, e);
|
|
39011
|
+
}
|
|
38759
39012
|
},
|
|
38760
39013
|
href: href,
|
|
38761
39014
|
rel: innerRel,
|
|
38762
|
-
target: innerTarget === "_self" ? undefined : target
|
|
39015
|
+
target: innerTarget === "_self" ? undefined : target
|
|
39016
|
+
// Which slide this tab is, and which box to say it to: read by the <Nav>
|
|
39017
|
+
// around it to place the row's own bar, and by the command above to find
|
|
39018
|
+
// the container across the document.
|
|
39019
|
+
,
|
|
39020
|
+
|
|
39021
|
+
"data-slide-target": slide,
|
|
39022
|
+
commandfor: slide ? nav?.slideContainer : undefined,
|
|
39023
|
+
"aria-controls": slide ? nav?.slideContainer : undefined,
|
|
39024
|
+
tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
|
|
39025
|
+
role: slide ? "tab" : props.role,
|
|
38763
39026
|
"aria-current": isCurrent ? "page" : undefined,
|
|
38764
|
-
"aria-selected": selectionContext ? selected : undefined,
|
|
39027
|
+
"aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
|
|
38765
39028
|
"data-value-event": "navi_value",
|
|
38766
39029
|
onnavi_value: e => {
|
|
38767
39030
|
e.detail.setValue(value);
|
|
@@ -38824,6 +39087,75 @@ const css$N = /* css */`
|
|
|
38824
39087
|
--nav-padding: 0px;
|
|
38825
39088
|
--nav-border-radius: 0px;
|
|
38826
39089
|
--nav-background: transparent;
|
|
39090
|
+
--nav-current-indicator-size: 2px;
|
|
39091
|
+
--nav-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
39092
|
+
}
|
|
39093
|
+
}
|
|
39094
|
+
|
|
39095
|
+
/* The bar of a nav whose tabs are SLIDES: one element for the whole row,
|
|
39096
|
+
placed over the current tab and interpolated towards the one the picture
|
|
39097
|
+
leans on (see paintIndicatorGeometry). The two ends are written in pixels
|
|
39098
|
+
as plain numbers, so the whole of the movement is a calc() the browser
|
|
39099
|
+
runs itself — the trait then follows a finger dragging the slides without a
|
|
39100
|
+
render per frame, and rides the same animation as the track when the travel
|
|
39101
|
+
was asked for rather than dragged.
|
|
39102
|
+
No named view transition here, unlike the bar of a nav made of routes:
|
|
39103
|
+
there is no transition to be part of — the slides travel under an animation
|
|
39104
|
+
of their own, which a finger can hold. */
|
|
39105
|
+
.navi_nav[data-nav-indicator] {
|
|
39106
|
+
position: relative;
|
|
39107
|
+
|
|
39108
|
+
> .navi_nav_indicator {
|
|
39109
|
+
--x-nav-indicator-position: calc(
|
|
39110
|
+
var(--nav-indicator-position) + var(--slide-travel-progress) *
|
|
39111
|
+
var(--nav-indicator-position-delta)
|
|
39112
|
+
);
|
|
39113
|
+
--x-nav-indicator-length: calc(
|
|
39114
|
+
var(--nav-indicator-length) + var(--slide-travel-progress) *
|
|
39115
|
+
var(--nav-indicator-length-delta)
|
|
39116
|
+
);
|
|
39117
|
+
|
|
39118
|
+
position: absolute;
|
|
39119
|
+
z-index: 1;
|
|
39120
|
+
background: var(--nav-current-indicator-color);
|
|
39121
|
+
border-radius: 0.1px;
|
|
39122
|
+
pointer-events: none;
|
|
39123
|
+
}
|
|
39124
|
+
/* Nothing to draw until the row has been measured: a tab bar whose current
|
|
39125
|
+
tab is not among its links (a container on a slide no tab names) has no
|
|
39126
|
+
place to put the trait. */
|
|
39127
|
+
&:not([data-nav-indicator-measured]) > .navi_nav_indicator {
|
|
39128
|
+
display: none;
|
|
39129
|
+
}
|
|
39130
|
+
|
|
39131
|
+
&[data-nav-indicator="top"],
|
|
39132
|
+
&[data-nav-indicator="bottom"] {
|
|
39133
|
+
> .navi_nav_indicator {
|
|
39134
|
+
left: calc(var(--x-nav-indicator-position) * 1px);
|
|
39135
|
+
width: calc(var(--x-nav-indicator-length) * 1px);
|
|
39136
|
+
height: var(--nav-current-indicator-size);
|
|
39137
|
+
}
|
|
39138
|
+
}
|
|
39139
|
+
&[data-nav-indicator="top"] > .navi_nav_indicator {
|
|
39140
|
+
top: 0;
|
|
39141
|
+
}
|
|
39142
|
+
&[data-nav-indicator="bottom"] > .navi_nav_indicator {
|
|
39143
|
+
bottom: 0;
|
|
39144
|
+
}
|
|
39145
|
+
|
|
39146
|
+
&[data-nav-indicator="left"],
|
|
39147
|
+
&[data-nav-indicator="right"] {
|
|
39148
|
+
> .navi_nav_indicator {
|
|
39149
|
+
top: calc(var(--x-nav-indicator-position) * 1px);
|
|
39150
|
+
width: var(--nav-current-indicator-size);
|
|
39151
|
+
height: calc(var(--x-nav-indicator-length) * 1px);
|
|
39152
|
+
}
|
|
39153
|
+
}
|
|
39154
|
+
&[data-nav-indicator="left"] > .navi_nav_indicator {
|
|
39155
|
+
left: 0;
|
|
39156
|
+
}
|
|
39157
|
+
&[data-nav-indicator="right"] > .navi_nav_indicator {
|
|
39158
|
+
right: 0;
|
|
38827
39159
|
}
|
|
38828
39160
|
}
|
|
38829
39161
|
|
|
@@ -38979,23 +39311,43 @@ const NavStyleCSSVars = {
|
|
|
38979
39311
|
paddingRight: "--nav-padding-right",
|
|
38980
39312
|
paddingBottom: "--nav-padding-bottom",
|
|
38981
39313
|
paddingLeft: "--nav-padding-left",
|
|
38982
|
-
background: "--nav-background"
|
|
39314
|
+
background: "--nav-background",
|
|
39315
|
+
currentIndicatorColor: "--nav-current-indicator-color",
|
|
39316
|
+
currentIndicatorSize: "--nav-current-indicator-size"
|
|
39317
|
+
};
|
|
39318
|
+
const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
39319
|
+
if (currentIndicator === true) {
|
|
39320
|
+
return vertical ? "left" : "bottom";
|
|
39321
|
+
}
|
|
39322
|
+
if (currentIndicator === "top" || currentIndicator === "bottom" || currentIndicator === "left" || currentIndicator === "right") {
|
|
39323
|
+
return currentIndicator;
|
|
39324
|
+
}
|
|
39325
|
+
return null;
|
|
38983
39326
|
};
|
|
39327
|
+
|
|
38984
39328
|
/**
|
|
38985
39329
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
38986
39330
|
* currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
|
|
38987
39331
|
* currentIndicatorSlides?: boolean,
|
|
39332
|
+
* slideContainer?: string,
|
|
38988
39333
|
* }>}
|
|
38989
39334
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
|
|
38990
39335
|
* bar that says which tab one is on, said once here rather than on every
|
|
38991
39336
|
* `<Link>`. A link may still say otherwise for itself.
|
|
38992
39337
|
* @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
|
|
38993
39338
|
* travels from the tab it was under to the tab it is under now, instead of
|
|
38994
|
-
* going out on one and coming back on the other.
|
|
38995
|
-
* which is all the browser needs: any change played as
|
|
38996
|
-
* animates it on the same clock as everything else in that
|
|
38997
|
-
* a `RouteTravel` that means it follows the pages, and the
|
|
38998
|
-
* them, without either of them being told about the other.
|
|
39339
|
+
* going out on one and coming back on the other. For a nav made of routes it
|
|
39340
|
+
* does so by being NAMED, which is all the browser needs: any change played as
|
|
39341
|
+
* a view transition animates it on the same clock as everything else in that
|
|
39342
|
+
* transition. Inside a `RouteTravel` that means it follows the pages, and the
|
|
39343
|
+
* thumb dragging them, without either of them being told about the other. For
|
|
39344
|
+
* a nav made of slides (`slideContainer`) the bar is one element for the whole
|
|
39345
|
+
* row, and it reads the travel the container publishes.
|
|
39346
|
+
* @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
|
|
39347
|
+
* tabs are about: each one says which slide it is (`<Link slide="…">`), the
|
|
39348
|
+
* container says which one is on screen, and pressing a tab travels there.
|
|
39349
|
+
* Tabs that are places in the same screen rather than pages of their own —
|
|
39350
|
+
* nothing is written to the URL and nothing is a link.
|
|
38999
39351
|
*/
|
|
39000
39352
|
const Nav = ({
|
|
39001
39353
|
children,
|
|
@@ -39008,21 +39360,132 @@ const Nav = ({
|
|
|
39008
39360
|
currentIndicatorSlides = true,
|
|
39009
39361
|
panelPosition,
|
|
39010
39362
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
39363
|
+
slideContainer,
|
|
39011
39364
|
...props
|
|
39012
39365
|
}) => {
|
|
39013
39366
|
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
39367
|
+
const defaultRef = useRef();
|
|
39368
|
+
props.ref = props.ref || defaultRef;
|
|
39369
|
+
const navRef = props.ref;
|
|
39014
39370
|
const indicatorNameRef = useRef(null);
|
|
39015
39371
|
if (indicatorNameRef.current === null) {
|
|
39016
39372
|
indicatorNameRef.current = `navi-nav-indicator-${++navCount}`;
|
|
39017
39373
|
}
|
|
39374
|
+
const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
|
|
39375
|
+
const slideContainerElementRef = useRef(null);
|
|
39376
|
+
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
39377
|
+
|
|
39378
|
+
// Where the trait is and where it is headed, as four numbers of pixels the
|
|
39379
|
+
// CSS above interpolates between (see the .navi_nav_indicator rules). Written
|
|
39380
|
+
// by hand rather than rendered: it is read off the row as it stands, and the
|
|
39381
|
+
// travel it must agree with starts in the same frame the container publishes
|
|
39382
|
+
// it — a render would land after the movement had begun.
|
|
39383
|
+
const paintIndicatorGeometry = () => {
|
|
39384
|
+
const navElement = navRef.current;
|
|
39385
|
+
const containerElement = slideContainerElementRef.current;
|
|
39386
|
+
if (!navElement || !containerElement || !indicatorPosition) {
|
|
39387
|
+
return;
|
|
39388
|
+
}
|
|
39389
|
+
const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
|
|
39390
|
+
const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
|
|
39391
|
+
const currentArea = containerElement.getAttribute("data-slide-current");
|
|
39392
|
+
const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
|
|
39393
|
+
if (currentIndex === -1) {
|
|
39394
|
+
// On a slide no tab in this row names: there is no tab to sit under.
|
|
39395
|
+
navElement.removeAttribute("data-nav-indicator-measured");
|
|
39396
|
+
return;
|
|
39397
|
+
}
|
|
39398
|
+
const measure = tabElement => vertical ? {
|
|
39399
|
+
position: tabElement.offsetTop,
|
|
39400
|
+
length: tabElement.offsetHeight
|
|
39401
|
+
} : {
|
|
39402
|
+
position: tabElement.offsetLeft,
|
|
39403
|
+
length: tabElement.offsetWidth
|
|
39404
|
+
};
|
|
39405
|
+
const currentMeasure = measure(tabElements[currentIndex]);
|
|
39406
|
+
const towardArea = containerElement.getAttribute("data-slide-travel-toward");
|
|
39407
|
+
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
39408
|
+
let positionDelta = 0;
|
|
39409
|
+
let lengthDelta = 0;
|
|
39410
|
+
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
39411
|
+
const towardMeasure = measure(tabElements[towardIndex]);
|
|
39412
|
+
// What one box of travel is worth in pixels of this row, signed so that
|
|
39413
|
+
// the trait is exactly on the other tab when the progress is at its own
|
|
39414
|
+
// end: the container counts +1 when the picture leans on a slide sitting
|
|
39415
|
+
// BEFORE the current one and -1 when it sits after.
|
|
39416
|
+
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
39417
|
+
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
39418
|
+
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
39419
|
+
}
|
|
39420
|
+
const {
|
|
39421
|
+
style
|
|
39422
|
+
} = navElement;
|
|
39423
|
+
style.setProperty("--nav-indicator-position", currentMeasure.position);
|
|
39424
|
+
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
39425
|
+
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
39426
|
+
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
39427
|
+
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
39428
|
+
};
|
|
39429
|
+
// Reached through a ref by everything watching the DOM below: those watchers
|
|
39430
|
+
// outlive a render, and what they must run is the version of this that knows
|
|
39431
|
+
// about the row as it is now.
|
|
39432
|
+
const paintIndicatorGeometryRef = useRef(null);
|
|
39433
|
+
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
39434
|
+
useLayoutEffect(() => {
|
|
39435
|
+
if (!slideContainer) {
|
|
39436
|
+
return undefined;
|
|
39437
|
+
}
|
|
39438
|
+
const containerElement = document.getElementById(slideContainer);
|
|
39439
|
+
if (!containerElement) {
|
|
39440
|
+
console.warn(`<Nav slideContainer="${slideContainer}"> but no element with that id found`);
|
|
39441
|
+
return undefined;
|
|
39442
|
+
}
|
|
39443
|
+
slideContainerElementRef.current = containerElement;
|
|
39444
|
+
const readContainer = () => {
|
|
39445
|
+
setCurrentSlideArea(containerElement.getAttribute("data-slide-current") ?? undefined);
|
|
39446
|
+
paintIndicatorGeometryRef.current();
|
|
39447
|
+
};
|
|
39448
|
+
readContainer();
|
|
39449
|
+
// The container says where one is and what the picture leans on, and says
|
|
39450
|
+
// it in the DOM: nothing here is told, everything is read — which is what
|
|
39451
|
+
// lets this row sit anywhere on the page (above the box, in a fixed bar)
|
|
39452
|
+
// rather than inside it.
|
|
39453
|
+
const attributeObserver = new MutationObserver(readContainer);
|
|
39454
|
+
attributeObserver.observe(containerElement, {
|
|
39455
|
+
attributes: true,
|
|
39456
|
+
attributeFilter: ["data-slide-current", "data-slide-travel-toward"]
|
|
39457
|
+
});
|
|
39458
|
+
// A row whose tabs changed width — a badge count, a font that just
|
|
39459
|
+
// arrived, a window resized — is measured again: what was written is
|
|
39460
|
+
// pixels, and pixels go stale.
|
|
39461
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
39462
|
+
paintIndicatorGeometryRef.current();
|
|
39463
|
+
});
|
|
39464
|
+
sizeObserver.observe(navRef.current);
|
|
39465
|
+
return () => {
|
|
39466
|
+
attributeObserver.disconnect();
|
|
39467
|
+
sizeObserver.disconnect();
|
|
39468
|
+
slideContainerElementRef.current = null;
|
|
39469
|
+
};
|
|
39470
|
+
}, [slideContainer]);
|
|
39471
|
+
|
|
39472
|
+
// Said after every commit: a tab added, removed or renamed moves the trait,
|
|
39473
|
+
// and no observer above watches this row's own children.
|
|
39474
|
+
useLayoutEffect(() => {
|
|
39475
|
+
paintIndicatorGeometry();
|
|
39476
|
+
});
|
|
39018
39477
|
const navContextValue = useMemo(() => ({
|
|
39019
|
-
|
|
39478
|
+
// The bar belongs to the row itself when the tabs are slides, so the
|
|
39479
|
+
// links draw none of their own.
|
|
39480
|
+
currentIndicator: slideContainer ? undefined : currentIndicator,
|
|
39020
39481
|
// Read by the link that is current, and by it alone: a name belongs to
|
|
39021
39482
|
// one element at a time, and the bar exists in every tab.
|
|
39022
|
-
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null
|
|
39023
|
-
|
|
39483
|
+
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null,
|
|
39484
|
+
slideContainer,
|
|
39485
|
+
currentSlideArea
|
|
39486
|
+
}), [currentIndicator, currentIndicatorSlides, slideContainer, currentSlideArea]);
|
|
39024
39487
|
children = toChildArray(children);
|
|
39025
|
-
return
|
|
39488
|
+
return jsxs(Box, {
|
|
39026
39489
|
as: "nav",
|
|
39027
39490
|
row: vertical,
|
|
39028
39491
|
column: !vertical,
|
|
@@ -39031,15 +39494,30 @@ const Nav = ({
|
|
|
39031
39494
|
"data-expand": expand || expandX ? "" : undefined,
|
|
39032
39495
|
"data-vertical": vertical ? "" : undefined,
|
|
39033
39496
|
"data-panel-position": panelPosition,
|
|
39497
|
+
"data-nav-indicator": indicatorPosition ?? undefined
|
|
39498
|
+
// "write your travel here too": a custom property cannot be read across
|
|
39499
|
+
// the DOM, so the container paints its progress onto this element and the
|
|
39500
|
+
// trait follows in CSS alone (see SlideContainer's followerElements).
|
|
39501
|
+
,
|
|
39502
|
+
|
|
39503
|
+
"data-slide-container-follows": slideContainer
|
|
39504
|
+
// Tabs over one screen, not links to pages: a screen reader is told so,
|
|
39505
|
+
// and told which way the row runs.
|
|
39506
|
+
,
|
|
39507
|
+
|
|
39508
|
+
role: slideContainer ? "tablist" : undefined,
|
|
39509
|
+
"aria-orientation": slideContainer && vertical ? "vertical" : undefined,
|
|
39034
39510
|
expand: expand,
|
|
39035
39511
|
expandX: expandX,
|
|
39036
39512
|
spacing: spacing,
|
|
39037
39513
|
...props,
|
|
39038
39514
|
styleCSSVars: NavStyleCSSVars,
|
|
39039
|
-
children: jsx(
|
|
39515
|
+
children: [indicatorPosition && jsx("span", {
|
|
39516
|
+
className: "navi_nav_indicator"
|
|
39517
|
+
}), jsx(NavContext.Provider, {
|
|
39040
39518
|
value: navContextValue,
|
|
39041
39519
|
children: children
|
|
39042
|
-
})
|
|
39520
|
+
})]
|
|
39043
39521
|
});
|
|
39044
39522
|
};
|
|
39045
39523
|
|
|
@@ -45922,7 +46400,16 @@ const SlideContainer = ({
|
|
|
45922
46400
|
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
45923
46401
|
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
45924
46402
|
const travelProgressFromRef = useRef(null);
|
|
45925
|
-
|
|
46403
|
+
// One per element painting the progress: the box, plus everything following
|
|
46404
|
+
// it (see followerElementsRef). All started together and with the same
|
|
46405
|
+
// options, so they are one movement said in several places.
|
|
46406
|
+
const progressAnimationsRef = useRef([]);
|
|
46407
|
+
// Elements outside the box that draw something about this travel — a tab bar
|
|
46408
|
+
// above it, most of all. A custom property cannot be read across the DOM, so
|
|
46409
|
+
// the progress is WRITTEN on each of them: they then interpolate whatever they
|
|
46410
|
+
// draw in CSS alone, at the pace of the travel and under the finger, with
|
|
46411
|
+
// nothing measured per frame.
|
|
46412
|
+
const followerElementsRef = useRef([]);
|
|
45926
46413
|
const current = rollingArea ?? provisionalArea ?? currentProp ?? currentAreaState;
|
|
45927
46414
|
const vertical = layout === "column";
|
|
45928
46415
|
// What the map has, and what each way of asking is allowed to use of it.
|
|
@@ -46048,6 +46535,28 @@ const SlideContainer = ({
|
|
|
46048
46535
|
...map
|
|
46049
46536
|
};
|
|
46050
46537
|
};
|
|
46538
|
+
|
|
46539
|
+
// Who is drawing something about this box from outside it: a tab bar saying
|
|
46540
|
+
// where one is, a row of dots. They name the box they follow by its id, the
|
|
46541
|
+
// way everything else that talks to it across the document does (commandfor).
|
|
46542
|
+
const readFollowerElements = () => {
|
|
46543
|
+
const containerEl = containerRef.current;
|
|
46544
|
+
const {
|
|
46545
|
+
id
|
|
46546
|
+
} = containerEl;
|
|
46547
|
+
if (!id) {
|
|
46548
|
+
return [];
|
|
46549
|
+
}
|
|
46550
|
+
return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
|
|
46551
|
+
};
|
|
46552
|
+
|
|
46553
|
+
// Which slide is on screen, said in the DOM: it is what anything outside the
|
|
46554
|
+
// box reads to know where one is (a tab bar marking its current tab), and
|
|
46555
|
+
// there is nothing else for it to read — a slide the container holds by
|
|
46556
|
+
// itself is known to no one else.
|
|
46557
|
+
const paintCurrentArea = area => {
|
|
46558
|
+
containerRef.current?.setAttribute("data-slide-current", area);
|
|
46559
|
+
};
|
|
46051
46560
|
const markAnswered = area => {
|
|
46052
46561
|
const order = readMap().slideElements.map(readArea);
|
|
46053
46562
|
const rank = order.indexOf(area);
|
|
@@ -46130,6 +46639,10 @@ const SlideContainer = ({
|
|
|
46130
46639
|
// the children free — their shape says nothing about the arrangement, the map
|
|
46131
46640
|
// does — and it is also the only place that has to agree with itself.
|
|
46132
46641
|
useLayoutEffect(() => {
|
|
46642
|
+
// Read on every render rather than subscribed to: a follower says who it
|
|
46643
|
+
// follows in the DOM, and the render that mounted one is the render this
|
|
46644
|
+
// runs after.
|
|
46645
|
+
followerElementsRef.current = readFollowerElements();
|
|
46133
46646
|
const {
|
|
46134
46647
|
slideElements,
|
|
46135
46648
|
placeOf
|
|
@@ -46142,6 +46655,7 @@ const SlideContainer = ({
|
|
|
46142
46655
|
// shown, the way a stack of pages opens on its first page.
|
|
46143
46656
|
slideElements[0];
|
|
46144
46657
|
const currentArea = readArea(currentElement);
|
|
46658
|
+
paintCurrentArea(currentArea);
|
|
46145
46659
|
const realPlaceOf = area => placeOf.get(area) || {
|
|
46146
46660
|
x: 0,
|
|
46147
46661
|
y: 0
|
|
@@ -46287,7 +46801,9 @@ const SlideContainer = ({
|
|
|
46287
46801
|
// there was one, from a whole box away when the travel was asked for.
|
|
46288
46802
|
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
46289
46803
|
travelProgressFromRef.current = null;
|
|
46290
|
-
|
|
46804
|
+
// The slide the picture leans on for the length of it is the one being
|
|
46805
|
+
// LEFT: it is the second one in the frame until the travel is over.
|
|
46806
|
+
animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
|
|
46291
46807
|
// Presses still waiting behind this one: it is already late, so it is
|
|
46292
46808
|
// sent home at once rather than played out at the pace of someone who
|
|
46293
46809
|
// has stopped pressing. Someone pressing → four times is asking to be
|
|
@@ -46707,6 +47223,30 @@ const SlideContainer = ({
|
|
|
46707
47223
|
paintTravelProgress(drag.progress, drag.areaPulled);
|
|
46708
47224
|
};
|
|
46709
47225
|
|
|
47226
|
+
// Everything that draws this travel: the box itself and whoever follows it.
|
|
47227
|
+
const travelPainters = () => {
|
|
47228
|
+
const containerEl = containerRef.current;
|
|
47229
|
+
if (!containerEl) {
|
|
47230
|
+
return [];
|
|
47231
|
+
}
|
|
47232
|
+
return [containerEl, ...followerElementsRef.current];
|
|
47233
|
+
};
|
|
47234
|
+
|
|
47235
|
+
// Which OTHER slide the picture leans on while it is not on the current one:
|
|
47236
|
+
// the slide being pulled in under a finger, the slide being left during a
|
|
47237
|
+
// travel. Two slides are in the frame and the number below says how far
|
|
47238
|
+
// between them one is — this says which the second one is, so a trait can be
|
|
47239
|
+
// drawn between two places rather than merely offset from one.
|
|
47240
|
+
const paintTravelToward = area => {
|
|
47241
|
+
for (const element of travelPainters()) {
|
|
47242
|
+
if (area) {
|
|
47243
|
+
element.setAttribute("data-slide-travel-toward", area);
|
|
47244
|
+
} else {
|
|
47245
|
+
element.removeAttribute("data-slide-travel-toward");
|
|
47246
|
+
}
|
|
47247
|
+
}
|
|
47248
|
+
};
|
|
47249
|
+
|
|
46710
47250
|
// Where the picture stands relative to the slide that is CURRENT, in boxes:
|
|
46711
47251
|
// 0 on it, +1 one whole box before it, -1 one box after. Written on the
|
|
46712
47252
|
// container so an indicator drawn inside the box — a tab bar, a dot row, a
|
|
@@ -46715,45 +47255,50 @@ const SlideContainer = ({
|
|
|
46715
47255
|
// gesture, so the number stays continuous when the travel commits and the
|
|
46716
47256
|
// current slide changes under it.
|
|
46717
47257
|
const paintTravelProgress = (progress, area) => {
|
|
46718
|
-
const
|
|
46719
|
-
|
|
46720
|
-
|
|
46721
|
-
|
|
46722
|
-
|
|
46723
|
-
|
|
46724
|
-
containerEl.removeAttribute("data-slide-travel-to");
|
|
46725
|
-
return;
|
|
47258
|
+
for (const element of travelPainters()) {
|
|
47259
|
+
if (progress) {
|
|
47260
|
+
element.style.setProperty("--slide-travel-progress", progress);
|
|
47261
|
+
} else {
|
|
47262
|
+
element.style.removeProperty("--slide-travel-progress");
|
|
47263
|
+
}
|
|
46726
47264
|
}
|
|
46727
|
-
|
|
46728
|
-
|
|
46729
|
-
|
|
46730
|
-
|
|
46731
|
-
|
|
47265
|
+
paintTravelToward(progress ? area : null);
|
|
47266
|
+
};
|
|
47267
|
+
const cancelTravelProgressAnimation = () => {
|
|
47268
|
+
for (const animation of progressAnimationsRef.current) {
|
|
47269
|
+
animation.cancel();
|
|
46732
47270
|
}
|
|
47271
|
+
progressAnimationsRef.current = [];
|
|
46733
47272
|
};
|
|
46734
47273
|
|
|
46735
47274
|
// The indicator, brought home at the pace of the travel it belongs to: the
|
|
46736
47275
|
// same duration and the same easing as the track, so the trait and the slides
|
|
46737
47276
|
// are one movement. The value it lands on is the one nothing writes (0), so
|
|
46738
|
-
// the animation is left to fall away on its own
|
|
46739
|
-
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
|
|
47277
|
+
// the animation is left to fall away on its own — only the name of the slide
|
|
47278
|
+
// being leant on is taken back by hand, at the end.
|
|
47279
|
+
const animateTravelProgress = (from, durationMs, easing, area) => {
|
|
47280
|
+
// Read again at the start of every travel, not only at every render: a
|
|
47281
|
+
// follower appearing does not make this box render, and the travel it must
|
|
47282
|
+
// draw is the one about to start.
|
|
47283
|
+
followerElementsRef.current = readFollowerElements();
|
|
47284
|
+
cancelTravelProgressAnimation();
|
|
46743
47285
|
paintTravelProgress(0);
|
|
46744
|
-
|
|
47286
|
+
const painters = travelPainters();
|
|
47287
|
+
if (!painters.length || !from || !durationMs) {
|
|
46745
47288
|
return;
|
|
46746
47289
|
}
|
|
46747
|
-
|
|
47290
|
+
paintTravelToward(area);
|
|
47291
|
+
progressAnimationsRef.current = painters.map(element => element.animate([{
|
|
46748
47292
|
"--slide-travel-progress": from
|
|
46749
47293
|
}, {
|
|
46750
47294
|
"--slide-travel-progress": 0
|
|
46751
47295
|
}], {
|
|
46752
47296
|
duration: durationMs,
|
|
46753
47297
|
easing
|
|
46754
|
-
});
|
|
46755
|
-
|
|
46756
|
-
|
|
47298
|
+
}));
|
|
47299
|
+
progressAnimationsRef.current[0].finished.then(() => {
|
|
47300
|
+
progressAnimationsRef.current = [];
|
|
47301
|
+
paintTravelToward(null);
|
|
46757
47302
|
}, () => {
|
|
46758
47303
|
// cancelled by the next travel — that one says where the trait goes
|
|
46759
47304
|
});
|
|
@@ -46826,7 +47371,7 @@ const SlideContainer = ({
|
|
|
46826
47371
|
settleTravel();
|
|
46827
47372
|
return;
|
|
46828
47373
|
}
|
|
46829
|
-
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out");
|
|
47374
|
+
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out", drag.areaPulled);
|
|
46830
47375
|
const animation = track.animate([{
|
|
46831
47376
|
translate: drag.offset
|
|
46832
47377
|
}, {
|
|
@@ -46952,8 +47497,8 @@ const SlideContainer = ({
|
|
|
46952
47497
|
caughtTravel = null;
|
|
46953
47498
|
trackAnimationRef.current?.cancel();
|
|
46954
47499
|
trackAnimationRef.current = null;
|
|
46955
|
-
|
|
46956
|
-
|
|
47500
|
+
cancelTravelProgressAnimation();
|
|
47501
|
+
followerElementsRef.current = readFollowerElements();
|
|
46957
47502
|
drag.axis = axis;
|
|
46958
47503
|
drag.areaBack = areaBack;
|
|
46959
47504
|
drag.areaOn = areaOn;
|
|
@@ -47098,6 +47643,10 @@ const SlideContainer = ({
|
|
|
47098
47643
|
...handlers
|
|
47099
47644
|
});
|
|
47100
47645
|
if (!gesture) {
|
|
47646
|
+
// Not a press this box can be about — something that reads the pointer
|
|
47647
|
+
// itself, a box below it that travels the same way. Whatever the press
|
|
47648
|
+
// stopped on its way in goes back on its way.
|
|
47649
|
+
handlers.onGiveUp();
|
|
47101
47650
|
return;
|
|
47102
47651
|
}
|
|
47103
47652
|
handlers.drag.gesture = gesture;
|
|
@@ -47144,7 +47693,7 @@ const SlideContainer = ({
|
|
|
47144
47693
|
return () => {
|
|
47145
47694
|
dragRef.current?.gesture?.stop();
|
|
47146
47695
|
dragRef.current = null;
|
|
47147
|
-
|
|
47696
|
+
cancelTravelProgressAnimation();
|
|
47148
47697
|
};
|
|
47149
47698
|
}, []);
|
|
47150
47699
|
|
|
@@ -47209,10 +47758,16 @@ const SlideContainer = ({
|
|
|
47209
47758
|
"data-slide-container": ""
|
|
47210
47759
|
// Which axes a touch may travel on, said in the DOM: what the browser
|
|
47211
47760
|
// does with a finger is decided by CSS (touch-action) before any of this
|
|
47212
|
-
// has seen the gesture
|
|
47761
|
+
// has seen the gesture — and it is also what a box HOLDING this one reads
|
|
47762
|
+
// to know the gesture is not its own (see drag_to_travel.js).
|
|
47213
47763
|
,
|
|
47214
47764
|
|
|
47215
47765
|
"data-travel-by-drag": dragAxes ?? undefined
|
|
47766
|
+
// The same fact for a wheel, and only for that second reason: this box
|
|
47767
|
+
// takes the push, whatever the box around it also travels on.
|
|
47768
|
+
,
|
|
47769
|
+
|
|
47770
|
+
"data-travel-by-wheel": scrollAxes ?? undefined
|
|
47216
47771
|
// The same fact, read by the shared gesture stylesheet: what scrolls
|
|
47217
47772
|
// inside a box that travels must not spill onto the page behind it (see
|
|
47218
47773
|
// drag_to_travel.js).
|
|
@@ -59490,8 +60045,16 @@ const useWheelInteractions = ({
|
|
|
59490
60045
|
document.removeEventListener("wheel", onDocumentWheel, {
|
|
59491
60046
|
capture: true
|
|
59492
60047
|
});
|
|
60048
|
+
releaseWheelGesture(vp);
|
|
59493
60049
|
};
|
|
59494
60050
|
const keepClaimingGesture = () => {
|
|
60051
|
+
// Said out loud as well as swallowed: preventDefault only settles it with
|
|
60052
|
+
// the browser, and a box that travels with the wheel answers the burst
|
|
60053
|
+
// from a listener of its own — it asks who owns the gesture instead (see
|
|
60054
|
+
// wheel_gesture.js in @jsenv/dom).
|
|
60055
|
+
claimWheelGesture(vp, {
|
|
60056
|
+
delay: WHEEL_GESTURE_MAX_GAP
|
|
60057
|
+
});
|
|
59495
60058
|
if (!gestureGuardTimer) {
|
|
59496
60059
|
document.addEventListener("wheel", onDocumentWheel, {
|
|
59497
60060
|
capture: true,
|
|
@@ -59502,6 +60065,12 @@ const useWheelInteractions = ({
|
|
|
59502
60065
|
gestureGuardTimer = setTimeout(stopClaimingGesture, WHEEL_GESTURE_MAX_GAP);
|
|
59503
60066
|
};
|
|
59504
60067
|
const onWheel = e => {
|
|
60068
|
+
// The burst belongs to something else — a box that travels with the
|
|
60069
|
+
// wheel, another wheel the pointer has just left. It is theirs until the
|
|
60070
|
+
// events stop coming.
|
|
60071
|
+
if (wheelGestureIsTakenFrom(vp)) {
|
|
60072
|
+
return;
|
|
60073
|
+
}
|
|
59505
60074
|
const raw = isHorizontal ? e.deltaX || e.deltaY : e.deltaY;
|
|
59506
60075
|
if (!raw) {
|
|
59507
60076
|
return;
|