@jsenv/navi 0.29.25 → 0.29.26
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 +665 -176
- package/dist/jsenv_navi.js.map +103 -54
- package/docs/AI_INSTRUCTIONS.md +7 -3
- package/docs/drag_to_travel.md +24 -2
- package/docs/navigation.md +253 -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";
|
|
@@ -34654,17 +34654,25 @@ const Route = props => {
|
|
|
34654
34654
|
});
|
|
34655
34655
|
};
|
|
34656
34656
|
/**
|
|
34657
|
-
* The
|
|
34657
|
+
* The pages a tree of <Route> children is made of, in the order they are
|
|
34658
34658
|
* written. Reading them is what turns a router into a row one can walk: "one
|
|
34659
34659
|
* step that way" is a fact about the order the branches were declared in, and
|
|
34660
34660
|
* nothing in a URL says it.
|
|
34661
34661
|
*
|
|
34662
|
+
* A page is `{ route, params }`, never the route alone: a section of a page is
|
|
34663
|
+
* as often a PARAM as it is a route of its own — `<Route route={PAGE}
|
|
34664
|
+
* routeParams={{ section: "done" }}>` is how this very file selects a branch on
|
|
34665
|
+
* one — and three branches of the same route are then the same object three
|
|
34666
|
+
* times. Told apart by their params, they are three pages one walks between;
|
|
34667
|
+
* told apart by identity, they are one page and there is nowhere to walk.
|
|
34668
|
+
* `params` is undefined for a branch that is a route on its own.
|
|
34669
|
+
*
|
|
34662
34670
|
* The same walk the container does to find the active branch (collectBranches),
|
|
34663
34671
|
* except that it keeps every leaf rather than the one that matches — and reads
|
|
34664
34672
|
* no signal, so asking does not subscribe the asker to anything.
|
|
34665
34673
|
*/
|
|
34666
|
-
const
|
|
34667
|
-
const
|
|
34674
|
+
const collectRoutePages = children => {
|
|
34675
|
+
const pages = [];
|
|
34668
34676
|
const visit = child => {
|
|
34669
34677
|
if (!child || child === true || child === false) {
|
|
34670
34678
|
return;
|
|
@@ -34680,18 +34688,22 @@ const collectRoutes = children => {
|
|
|
34680
34688
|
}
|
|
34681
34689
|
const {
|
|
34682
34690
|
children: nodeChildren,
|
|
34683
|
-
route
|
|
34691
|
+
route,
|
|
34692
|
+
routeParams
|
|
34684
34693
|
} = child.props;
|
|
34685
34694
|
if (nodeChildren) {
|
|
34686
34695
|
visit(nodeChildren);
|
|
34687
34696
|
return;
|
|
34688
34697
|
}
|
|
34689
34698
|
if (route) {
|
|
34690
|
-
|
|
34699
|
+
pages.push({
|
|
34700
|
+
route,
|
|
34701
|
+
params: routeParams
|
|
34702
|
+
});
|
|
34691
34703
|
}
|
|
34692
34704
|
};
|
|
34693
34705
|
visit(children);
|
|
34694
|
-
return
|
|
34706
|
+
return pages;
|
|
34695
34707
|
};
|
|
34696
34708
|
|
|
34697
34709
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
@@ -34897,14 +34909,19 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
34897
34909
|
// transition carries was measured once, at the start, against a destination
|
|
34898
34910
|
// this travel is no longer going to.
|
|
34899
34911
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
34912
|
+
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
34913
|
+
const TRAVEL_NAME = "navi-route-travel";
|
|
34900
34914
|
const css$R = /* css */`
|
|
34915
|
+
/* The name that makes the page inside this box a picture of its own during a
|
|
34916
|
+
transition — rather than part of the one big picture the document takes, so
|
|
34917
|
+
the two pages can move past each other while everything else stays where it
|
|
34918
|
+
is — is not written here: it is worn only for the length of a travel (see
|
|
34919
|
+
nameForTravel). A name belongs to ONE element at a time, and a page can hold
|
|
34920
|
+
several of these boxes at once — a section of the url and a search param of
|
|
34921
|
+
the root route are two rows of tabs, both live, and only one of them is ever
|
|
34922
|
+
travelling. */
|
|
34901
34923
|
.navi_route_travel {
|
|
34902
34924
|
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
34925
|
/* The gesture takes the axis the pages travel on and leaves the other one
|
|
34909
34926
|
to the page, so a list still scrolls under the same finger. */
|
|
34910
34927
|
touch-action: pan-y;
|
|
@@ -35102,21 +35119,30 @@ const css$R = /* css */`
|
|
|
35102
35119
|
|
|
35103
35120
|
/**
|
|
35104
35121
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
35105
|
-
* routes?: Array<object>,
|
|
35122
|
+
* routes?: Array<object|{route: object, params?: object}>,
|
|
35106
35123
|
* axis?: "x"|"y",
|
|
35107
35124
|
* travelByDrag?: boolean,
|
|
35108
|
-
* onTravel?: (detail: {route: object, cause: string}) => void|Promise<void>,
|
|
35125
|
+
* onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
|
|
35109
35126
|
* }>}
|
|
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
|
|
35127
|
+
* @param {Array<object|{route: object, params?: object}>} [props.routes] - the
|
|
35128
|
+
* tabs, in the order they are shown. Read from the <Route> children by
|
|
35129
|
+
* default, in the order they are written: the router already holds that list,
|
|
35130
|
+
* and asking a caller to write it twice is asking for the two to disagree.
|
|
35131
|
+
* Pass it to say another order, when the pages are not children of this box,
|
|
35132
|
+
* or to name a tab the children cannot — the section a <Route fallback> shows
|
|
35133
|
+
* is a tab like the others, and only its params say which one.
|
|
35134
|
+
*
|
|
35135
|
+
* An entry is a route, or `{ route, params }` when the tabs of the row are a
|
|
35136
|
+
* PARAM of one route rather than routes of their own (the form
|
|
35137
|
+
* `<Route routeParams>` selects a branch on, and the form that lets a link
|
|
35138
|
+
* with no params reopen the section one was looking at). Written as bare
|
|
35139
|
+
* routes, three tabs of one route are the same object three times: there is
|
|
35140
|
+
* then one tab, and nowhere to travel.
|
|
35115
35141
|
* @param {"x"|"y"} [props.axis="x"] - which way the pages are laid out.
|
|
35116
35142
|
* @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
|
|
35117
35143
|
* 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
|
|
35144
|
+
* @param {(detail: {route: object, params: object|undefined, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
35145
|
+
* - how to go to a tab. The default REPLACES the current history entry
|
|
35120
35146
|
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
35121
35147
|
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
35122
35148
|
* page under six entries. A tab pressed is the other case and pushes, which
|
|
@@ -35139,8 +35165,9 @@ const RouteTravel = ({
|
|
|
35139
35165
|
axis = "x",
|
|
35140
35166
|
travelByDrag = true,
|
|
35141
35167
|
onTravel = ({
|
|
35142
|
-
route
|
|
35143
|
-
|
|
35168
|
+
route,
|
|
35169
|
+
params
|
|
35170
|
+
}) => route.redirectTo(params),
|
|
35144
35171
|
className,
|
|
35145
35172
|
children,
|
|
35146
35173
|
...rest
|
|
@@ -35152,51 +35179,58 @@ const RouteTravel = ({
|
|
|
35152
35179
|
// left, the animations the finger drives, and what to do with them once the
|
|
35153
35180
|
// browser has them ready. Null when no page is on its way anywhere.
|
|
35154
35181
|
const travelRef = useRef(null);
|
|
35155
|
-
// The
|
|
35182
|
+
// The page this box has ASKED for and is still waiting to see arrive.
|
|
35156
35183
|
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
35157
35184
|
// travel decided anything about it — sometimes after the travel was undone —
|
|
35158
|
-
// and read back as "the
|
|
35185
|
+
// and read back as "the page changed" it would start a second travel nobody
|
|
35159
35186
|
// asked for, over pictures that are already showing something else.
|
|
35160
|
-
const
|
|
35187
|
+
const pageAskedForRef = useRef(null);
|
|
35161
35188
|
// What a press stopped in flight, until the gesture says what it is about.
|
|
35162
35189
|
const caughtAtPressRef = useRef(null);
|
|
35163
35190
|
// The latest way to answer a gesture, for a watcher that outlives every
|
|
35164
35191
|
// render (see the wheel effect below).
|
|
35165
35192
|
const travelHandlersRef = useRef(null);
|
|
35166
35193
|
const pointerDownRef = useRef(null);
|
|
35167
|
-
const
|
|
35168
|
-
const
|
|
35169
|
-
|
|
35170
|
-
|
|
35171
|
-
// is
|
|
35172
|
-
|
|
35173
|
-
for
|
|
35174
|
-
|
|
35175
|
-
|
|
35176
|
-
}
|
|
35177
|
-
}
|
|
35194
|
+
const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
|
|
35195
|
+
const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
|
|
35196
|
+
const pages = pagesFromProp || pagesFromChildren;
|
|
35197
|
+
|
|
35198
|
+
// Which page is on screen, read from the pages themselves: every one of them
|
|
35199
|
+
// is read, so this re-renders when any of them starts or stops matching — and
|
|
35200
|
+
// for a row whose tabs are params of one route, when the params move from one
|
|
35201
|
+
// tab to the next (see pageIsCurrent).
|
|
35202
|
+
const currentIndex = currentPageIndex(pages);
|
|
35178
35203
|
// The page that was on screen when the change now happening was asked for:
|
|
35179
35204
|
// a travel is between two of them, and by the time anything renders the first
|
|
35180
35205
|
// one is already gone. Written after each render (below), so a subscriber
|
|
35181
35206
|
// reading it — they all run before Preact flushes — reads the one being left.
|
|
35182
35207
|
const currentIndexRef = useRef(currentIndex);
|
|
35183
35208
|
|
|
35209
|
+
// Where a page is asked for, whoever asks: a page is a route AND the params
|
|
35210
|
+
// that say which of its tabs, and a caller told only the route would send the
|
|
35211
|
+
// row back to whichever tab the URL already says (see redirectTo).
|
|
35212
|
+
const travelTo = (page, cause) => onTravel({
|
|
35213
|
+
route: page.route,
|
|
35214
|
+
params: page.params,
|
|
35215
|
+
cause
|
|
35216
|
+
});
|
|
35217
|
+
|
|
35184
35218
|
// One travel, whoever asked for it: a finger, a tab pressed, the browser's
|
|
35185
35219
|
// own back button. What differs is only who moves it — the finger drives it
|
|
35186
35220
|
// frame by frame (`scrub`), everything else lets it play.
|
|
35187
35221
|
const beginTravel = ({
|
|
35188
|
-
|
|
35189
|
-
|
|
35222
|
+
page,
|
|
35223
|
+
fromPage,
|
|
35190
35224
|
direction,
|
|
35191
35225
|
scrub,
|
|
35192
35226
|
change
|
|
35193
35227
|
}) => {
|
|
35194
35228
|
const travel = {
|
|
35195
|
-
|
|
35229
|
+
page,
|
|
35196
35230
|
// The page this set off from, kept rather than looked up again: the URL
|
|
35197
35231
|
// changes at the first pixel, so a moment later nothing on screen
|
|
35198
35232
|
// remembers where it started.
|
|
35199
|
-
|
|
35233
|
+
fromPage,
|
|
35200
35234
|
direction,
|
|
35201
35235
|
scrub,
|
|
35202
35236
|
ratio: 0,
|
|
@@ -35207,12 +35241,16 @@ const RouteTravel = ({
|
|
|
35207
35241
|
ended: false
|
|
35208
35242
|
};
|
|
35209
35243
|
travelRef.current = travel;
|
|
35244
|
+
// Taken before the picture is: the browser reads the name off the DOM as it
|
|
35245
|
+
// stands when the transition starts, and this box is only a picture of its
|
|
35246
|
+
// own for as long as it is the one travelling.
|
|
35247
|
+
nameForTravel(elementRef.current);
|
|
35210
35248
|
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
35211
35249
|
if (scrub) {
|
|
35212
35250
|
holdPictures(travel);
|
|
35213
35251
|
document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
|
|
35214
35252
|
}
|
|
35215
|
-
|
|
35253
|
+
pageAskedForRef.current = page;
|
|
35216
35254
|
// The box as it stands before anything moves: rendering is held, so this is
|
|
35217
35255
|
// still the page being left (see holdTravelHeight).
|
|
35218
35256
|
const heightBefore = elementRef.current.getBoundingClientRect().height;
|
|
@@ -35223,7 +35261,7 @@ const RouteTravel = ({
|
|
|
35223
35261
|
// The picture the browser is about to take must be of the page that was
|
|
35224
35262
|
// asked for, and a route matching is not yet a page rendered.
|
|
35225
35263
|
const viewTransition = startViewTransition(async () => {
|
|
35226
|
-
await
|
|
35264
|
+
await whilePageRenders(page, async () => {
|
|
35227
35265
|
releaseRendering();
|
|
35228
35266
|
if (change) {
|
|
35229
35267
|
await change();
|
|
@@ -35276,21 +35314,41 @@ const RouteTravel = ({
|
|
|
35276
35314
|
};
|
|
35277
35315
|
|
|
35278
35316
|
// A page change nobody here asked for: a tab pressed, a key, the back button.
|
|
35279
|
-
// The transition is started from the
|
|
35280
|
-
//
|
|
35281
|
-
//
|
|
35317
|
+
// The transition is started from what the router SAYS rather than from a
|
|
35318
|
+
// render, because a render is one flush too late — by then the DOM holds the
|
|
35319
|
+
// new page and the picture of the old one cannot be taken anymore.
|
|
35320
|
+
//
|
|
35321
|
+
// Watched as a position in the row rather than route by route. A route
|
|
35322
|
+
// announces its own status, and the row's tabs can all be one route: the
|
|
35323
|
+
// announcement then says a section changed without saying which is on screen,
|
|
35324
|
+
// and it says it about things this row does not move for (a route that has
|
|
35325
|
+
// now been visited, a param of its own that is not a tab of this row). Worse,
|
|
35326
|
+
// a status is published from inside the routing and the params it carries are
|
|
35327
|
+
// the ones known at that instant — a section that lands as a signal settles
|
|
35328
|
+
// is announced late, or not at all. The signals ARE the position, so the
|
|
35329
|
+
// position is read from them: one computed over the whole row, notified once
|
|
35330
|
+
// per move, whichever route moved and whether by matching or by params.
|
|
35282
35331
|
useLayoutEffect(() => {
|
|
35283
|
-
const
|
|
35284
|
-
|
|
35285
|
-
|
|
35286
|
-
|
|
35332
|
+
const currentIndexSignal = computed(() => currentPageIndex(pages));
|
|
35333
|
+
const onRowMove = index => {
|
|
35334
|
+
if (index === -1) {
|
|
35335
|
+
return;
|
|
35336
|
+
}
|
|
35337
|
+
if (index === currentIndexRef.current) {
|
|
35338
|
+
// Where the row already was — the first reading of all, and any move
|
|
35339
|
+
// this box has already taken note of (a render writes it too). What it
|
|
35340
|
+
// was still waiting for is here nonetheless, so the wait is called off.
|
|
35341
|
+
if (samePage(pageAskedForRef.current, pages[index])) {
|
|
35342
|
+
pageAskedForRef.current = null;
|
|
35343
|
+
}
|
|
35287
35344
|
return;
|
|
35288
35345
|
}
|
|
35346
|
+
const page = pages[index];
|
|
35289
35347
|
// A page this box asked for itself — a travel's own navigation, or one
|
|
35290
35348
|
// it had given up waiting on: what arrives here is the answer to a
|
|
35291
35349
|
// question already answered, not somebody going somewhere.
|
|
35292
|
-
if (
|
|
35293
|
-
|
|
35350
|
+
if (samePage(pageAskedForRef.current, page)) {
|
|
35351
|
+
pageAskedForRef.current = null;
|
|
35294
35352
|
currentIndexRef.current = index;
|
|
35295
35353
|
return;
|
|
35296
35354
|
}
|
|
@@ -35298,13 +35356,13 @@ const RouteTravel = ({
|
|
|
35298
35356
|
// is not coming, or no longer means anything. Forgotten here rather
|
|
35299
35357
|
// than kept, or the next press on that very tab would be taken for the
|
|
35300
35358
|
// late answer to a question nobody remembers asking.
|
|
35301
|
-
|
|
35359
|
+
pageAskedForRef.current = null;
|
|
35302
35360
|
// Asked for a page while one was already on its way: the travel in
|
|
35303
35361
|
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
35304
35362
|
// top would leave this one's pictures to be dropped mid-slide.
|
|
35305
35363
|
if (travelRef.current) {
|
|
35306
35364
|
currentIndexRef.current = index;
|
|
35307
|
-
retargetTravel(travelRef.current,
|
|
35365
|
+
retargetTravel(travelRef.current, page);
|
|
35308
35366
|
return;
|
|
35309
35367
|
}
|
|
35310
35368
|
const fromIndex = currentIndexRef.current;
|
|
@@ -35315,18 +35373,18 @@ const RouteTravel = ({
|
|
|
35315
35373
|
return;
|
|
35316
35374
|
}
|
|
35317
35375
|
beginTravel({
|
|
35318
|
-
|
|
35319
|
-
|
|
35376
|
+
page,
|
|
35377
|
+
fromPage: pages[fromIndex],
|
|
35320
35378
|
direction: index > fromIndex ? "forward" : "back",
|
|
35321
35379
|
scrub: false
|
|
35322
35380
|
});
|
|
35323
|
-
}));
|
|
35324
|
-
return () => {
|
|
35325
|
-
for (const unsubscribe of unsubscribes) {
|
|
35326
|
-
unsubscribe();
|
|
35327
|
-
}
|
|
35328
35381
|
};
|
|
35329
|
-
|
|
35382
|
+
// `subscribe` rather than `effect`: it hands the value to a callback that
|
|
35383
|
+
// is NOT being tracked, and what this one does — navigate, ask the router
|
|
35384
|
+
// for another page — reads and writes the very signals the row is watched
|
|
35385
|
+
// through.
|
|
35386
|
+
return currentIndexSignal.subscribe(onRowMove);
|
|
35387
|
+
}, [pages]);
|
|
35330
35388
|
|
|
35331
35389
|
// Rendering is held for the length of a navigation, so that whatever picture
|
|
35332
35390
|
// this box is about to take is of the page being LEFT (see holdRendering).
|
|
@@ -35363,6 +35421,13 @@ const RouteTravel = ({
|
|
|
35363
35421
|
// Let go of far enough: the movement carries on from under the finger, at its
|
|
35364
35422
|
// own pace, to the end.
|
|
35365
35423
|
const finishTravel = travel => {
|
|
35424
|
+
// Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
|
|
35425
|
+
// set them off: left standing after the release, this travel would go on
|
|
35426
|
+
// claiming a hand that is no longer there — and everything that asks
|
|
35427
|
+
// "is somebody holding this?" before touching it (a press on the tab one
|
|
35428
|
+
// came from, a wheel push) would be answered yes and do nothing, while the
|
|
35429
|
+
// pages carry on to a page the router has already left.
|
|
35430
|
+
travel.scrub = false;
|
|
35366
35431
|
releaseHold();
|
|
35367
35432
|
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
35368
35433
|
};
|
|
@@ -35415,19 +35480,20 @@ const RouteTravel = ({
|
|
|
35415
35480
|
Promise.resolve();
|
|
35416
35481
|
backAtTheStart.then(async () => {
|
|
35417
35482
|
try {
|
|
35418
|
-
|
|
35483
|
+
pageAskedForRef.current = travel.fromPage;
|
|
35419
35484
|
// The page that was left is put back UNDER the picture before the
|
|
35420
35485
|
// picture is dropped, so the two are the same thing at the moment they
|
|
35421
35486
|
// are swapped: that only holds once the page is really back.
|
|
35422
|
-
if (travel.
|
|
35487
|
+
if (pageIsCurrent(travel.fromPage)) {
|
|
35423
35488
|
// It never left: the press that set this revert off put it back
|
|
35424
35489
|
// there, and the pages have been held where they were until now.
|
|
35425
35490
|
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
35426
35491
|
// render that never comes and a page frozen under its own pictures.
|
|
35427
35492
|
releaseRendering();
|
|
35428
35493
|
} else {
|
|
35429
|
-
await
|
|
35430
|
-
route: travel.
|
|
35494
|
+
await whilePageRenders(travel.fromPage, () => onTravel({
|
|
35495
|
+
route: travel.fromPage.route,
|
|
35496
|
+
params: travel.fromPage.params,
|
|
35431
35497
|
cause: "revert"
|
|
35432
35498
|
}));
|
|
35433
35499
|
}
|
|
@@ -35466,8 +35532,8 @@ const RouteTravel = ({
|
|
|
35466
35532
|
// change — only what is being brought in against it, and that one is LIVE:
|
|
35467
35533
|
// pointing the router elsewhere is all it takes for the picture to show that
|
|
35468
35534
|
// page instead.
|
|
35469
|
-
const redirectTravel = (travel,
|
|
35470
|
-
travel.
|
|
35535
|
+
const redirectTravel = (travel, page, direction) => {
|
|
35536
|
+
travel.page = page;
|
|
35471
35537
|
// Everything the transition carries that is NOT the pages was measured
|
|
35472
35538
|
// against a destination this travel is no longer going to (see the CSS).
|
|
35473
35539
|
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
@@ -35488,28 +35554,28 @@ const RouteTravel = ({
|
|
|
35488
35554
|
|
|
35489
35555
|
// Somebody asked for a page while one was on its way. Where they asked for
|
|
35490
35556
|
// decides what that means.
|
|
35491
|
-
const retargetTravel = (travel,
|
|
35557
|
+
const retargetTravel = (travel, page) => {
|
|
35492
35558
|
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
35493
35559
|
// A hand is holding the pages, or they are already on their way back:
|
|
35494
35560
|
// either way this travel's end is decided by somebody else.
|
|
35495
35561
|
return;
|
|
35496
35562
|
}
|
|
35497
|
-
if (
|
|
35563
|
+
if (samePage(page, travel.page)) {
|
|
35498
35564
|
// Already on its way there.
|
|
35499
35565
|
return;
|
|
35500
35566
|
}
|
|
35501
|
-
if (
|
|
35567
|
+
if (samePage(page, travel.fromPage)) {
|
|
35502
35568
|
// Back where it set off from: that is not another travel, it is this one
|
|
35503
35569
|
// undone — the same pictures, run backwards.
|
|
35504
35570
|
revertTravel(travel);
|
|
35505
35571
|
return;
|
|
35506
35572
|
}
|
|
35507
|
-
const fromIndex =
|
|
35508
|
-
const toIndex =
|
|
35573
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35574
|
+
const toIndex = pageIndexOf(pages, page);
|
|
35509
35575
|
if (fromIndex === -1 || toIndex === -1) {
|
|
35510
35576
|
return;
|
|
35511
35577
|
}
|
|
35512
|
-
redirectTravel(travel,
|
|
35578
|
+
redirectTravel(travel, page, toIndex > fromIndex ? "forward" : "back");
|
|
35513
35579
|
};
|
|
35514
35580
|
const endTravel = travel => {
|
|
35515
35581
|
if (travel.ended) {
|
|
@@ -35523,6 +35589,10 @@ const RouteTravel = ({
|
|
|
35523
35589
|
releaseHold(travel);
|
|
35524
35590
|
if (travelRef.current === travel) {
|
|
35525
35591
|
travelRef.current = null;
|
|
35592
|
+
// Given back, so another box may wear it: kept, two of them on a page
|
|
35593
|
+
// would both answer to it and the browser refuses the whole transition
|
|
35594
|
+
// rather than pick.
|
|
35595
|
+
unnameAfterTravel(elementRef.current);
|
|
35526
35596
|
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
35527
35597
|
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
35528
35598
|
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
@@ -35587,24 +35657,21 @@ const RouteTravel = ({
|
|
|
35587
35657
|
}
|
|
35588
35658
|
// Dragging the page towards the end of the axis brings in what is
|
|
35589
35659
|
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
35590
|
-
const
|
|
35591
|
-
if (!
|
|
35660
|
+
const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
35661
|
+
if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
35592
35662
|
return false;
|
|
35593
35663
|
}
|
|
35594
35664
|
if (CAN_KEEP_PICTURE) {
|
|
35595
35665
|
beginTravel({
|
|
35596
|
-
|
|
35597
|
-
|
|
35666
|
+
page,
|
|
35667
|
+
fromPage: pages[currentIndex],
|
|
35598
35668
|
direction: sign > 0 ? "back" : "forward",
|
|
35599
35669
|
scrub: true,
|
|
35600
|
-
change: () =>
|
|
35601
|
-
route,
|
|
35602
|
-
cause: "drag"
|
|
35603
|
-
})
|
|
35670
|
+
change: () => travelTo(page, "drag")
|
|
35604
35671
|
});
|
|
35605
35672
|
} else {
|
|
35606
35673
|
travelRef.current = {
|
|
35607
|
-
|
|
35674
|
+
page,
|
|
35608
35675
|
noPicture: true,
|
|
35609
35676
|
ended: false
|
|
35610
35677
|
};
|
|
@@ -35656,9 +35723,9 @@ const RouteTravel = ({
|
|
|
35656
35723
|
// Where we are is what THIS travel was bringing in — the URL changed at
|
|
35657
35724
|
// the first pixel, so `currentIndex` belongs to a render this gesture
|
|
35658
35725
|
// is older than.
|
|
35659
|
-
const fromIndex =
|
|
35660
|
-
const
|
|
35661
|
-
if (fromIndex === -1 || !
|
|
35726
|
+
const fromIndex = pageIndexOf(pages, travel.page);
|
|
35727
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35728
|
+
if (fromIndex === -1 || !page) {
|
|
35662
35729
|
return false;
|
|
35663
35730
|
}
|
|
35664
35731
|
// At their very end before they are let go of: what ends the travel in
|
|
@@ -35668,14 +35735,11 @@ const RouteTravel = ({
|
|
|
35668
35735
|
scrubTravel(travel, 1);
|
|
35669
35736
|
travel.ratio = 1;
|
|
35670
35737
|
beginTravel({
|
|
35671
|
-
|
|
35672
|
-
|
|
35738
|
+
page,
|
|
35739
|
+
fromPage: travel.page,
|
|
35673
35740
|
direction,
|
|
35674
35741
|
scrub: true,
|
|
35675
|
-
change: () =>
|
|
35676
|
-
route,
|
|
35677
|
-
cause: "drag"
|
|
35678
|
-
})
|
|
35742
|
+
change: () => travelTo(page, "drag")
|
|
35679
35743
|
});
|
|
35680
35744
|
return {
|
|
35681
35745
|
size: boxSizeOnAxis(),
|
|
@@ -35690,17 +35754,14 @@ const RouteTravel = ({
|
|
|
35690
35754
|
// other neighbour is enough for it to show that one instead. The travel
|
|
35691
35755
|
// turns around where it stands, on the same transition and under the same
|
|
35692
35756
|
// hand, and there is no gap at all.
|
|
35693
|
-
const fromIndex =
|
|
35694
|
-
const
|
|
35695
|
-
if (fromIndex === -1 || !
|
|
35757
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35758
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35759
|
+
if (fromIndex === -1 || !page) {
|
|
35696
35760
|
return false;
|
|
35697
35761
|
}
|
|
35698
|
-
redirectTravel(travel,
|
|
35699
|
-
|
|
35700
|
-
|
|
35701
|
-
route,
|
|
35702
|
-
cause: "drag"
|
|
35703
|
-
});
|
|
35762
|
+
redirectTravel(travel, page, direction);
|
|
35763
|
+
pageAskedForRef.current = page;
|
|
35764
|
+
travelTo(page, "drag");
|
|
35704
35765
|
return {
|
|
35705
35766
|
size: boxSizeOnAxis(),
|
|
35706
35767
|
travelBack: sign > 0,
|
|
@@ -35723,10 +35784,7 @@ const RouteTravel = ({
|
|
|
35723
35784
|
if (travel.noPicture) {
|
|
35724
35785
|
travelRef.current = null;
|
|
35725
35786
|
if (travels) {
|
|
35726
|
-
|
|
35727
|
-
route: travel.route,
|
|
35728
|
-
cause: "drag"
|
|
35729
|
-
});
|
|
35787
|
+
travelTo(travel.page, "drag");
|
|
35730
35788
|
}
|
|
35731
35789
|
return;
|
|
35732
35790
|
}
|
|
@@ -35758,6 +35816,16 @@ const RouteTravel = ({
|
|
|
35758
35816
|
if (currentIndex === -1) {
|
|
35759
35817
|
return;
|
|
35760
35818
|
}
|
|
35819
|
+
// A press that never became a gesture: whatever it stopped goes on its way,
|
|
35820
|
+
// from where the finger caught it.
|
|
35821
|
+
const giveUp = () => {
|
|
35822
|
+
gestureRef.current = null;
|
|
35823
|
+
const caught = caughtAtPressRef.current;
|
|
35824
|
+
caughtAtPressRef.current = null;
|
|
35825
|
+
if (caught && !caught.ended) {
|
|
35826
|
+
releaseHold(caught);
|
|
35827
|
+
}
|
|
35828
|
+
};
|
|
35761
35829
|
const gesture = startDragToTravel(pointerDownEvent, {
|
|
35762
35830
|
element: elementRef.current,
|
|
35763
35831
|
axes: axis,
|
|
@@ -35765,17 +35833,14 @@ const RouteTravel = ({
|
|
|
35765
35833
|
// is answered from its first pixel, on the axis the pages travel.
|
|
35766
35834
|
immediate: caughtAtPressRef.current ? axis : false,
|
|
35767
35835
|
...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
|
-
}
|
|
35836
|
+
onGiveUp: giveUp
|
|
35778
35837
|
});
|
|
35838
|
+
if (!gesture) {
|
|
35839
|
+
// Not a press this box can be about — something that reads the pointer
|
|
35840
|
+
// itself, a box below it that travels the same way.
|
|
35841
|
+
giveUp();
|
|
35842
|
+
return;
|
|
35843
|
+
}
|
|
35779
35844
|
gestureRef.current = gesture;
|
|
35780
35845
|
};
|
|
35781
35846
|
|
|
@@ -35827,13 +35892,13 @@ const RouteTravel = ({
|
|
|
35827
35892
|
}
|
|
35828
35893
|
// Where the box is going, which is not where it is: a step asked for while
|
|
35829
35894
|
// a travel plays is the page after the one on its way.
|
|
35830
|
-
const
|
|
35831
|
-
const fromIndex =
|
|
35895
|
+
const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
|
|
35896
|
+
const fromIndex = pageIndexOf(pages, fromPage);
|
|
35832
35897
|
if (fromIndex === -1) {
|
|
35833
35898
|
return;
|
|
35834
35899
|
}
|
|
35835
|
-
const
|
|
35836
|
-
if (!
|
|
35900
|
+
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35901
|
+
if (!page) {
|
|
35837
35902
|
return;
|
|
35838
35903
|
}
|
|
35839
35904
|
if (travelInFlight) {
|
|
@@ -35844,14 +35909,11 @@ const RouteTravel = ({
|
|
|
35844
35909
|
travelInFlight.ratio = 1;
|
|
35845
35910
|
}
|
|
35846
35911
|
beginTravel({
|
|
35847
|
-
|
|
35848
|
-
|
|
35912
|
+
page,
|
|
35913
|
+
fromPage,
|
|
35849
35914
|
direction: sign > 0 ? "back" : "forward",
|
|
35850
35915
|
scrub: false,
|
|
35851
|
-
change: () =>
|
|
35852
|
-
route,
|
|
35853
|
-
cause: "wheel"
|
|
35854
|
-
})
|
|
35916
|
+
change: () => travelTo(page, "wheel")
|
|
35855
35917
|
});
|
|
35856
35918
|
};
|
|
35857
35919
|
|
|
@@ -35882,7 +35944,15 @@ const RouteTravel = ({
|
|
|
35882
35944
|
// page (see drag_to_travel.js).
|
|
35883
35945
|
,
|
|
35884
35946
|
|
|
35885
|
-
"data-drag-travel": travelByDrag ? axis : undefined
|
|
35947
|
+
"data-drag-travel": travelByDrag ? axis : undefined
|
|
35948
|
+
// The same fact said once per gesture, and for the other question the
|
|
35949
|
+
// DOM answers: a box that travels INSIDE this one — a row of slides in a
|
|
35950
|
+
// page — takes the axis it walks, and these are what it reads to know
|
|
35951
|
+
// this box walks it too.
|
|
35952
|
+
,
|
|
35953
|
+
|
|
35954
|
+
"data-travel-by-drag": travelByDrag ? axis : undefined,
|
|
35955
|
+
"data-travel-by-wheel": travelByDrag ? axis : undefined,
|
|
35886
35956
|
onPointerDown: onPointerDown,
|
|
35887
35957
|
children: children
|
|
35888
35958
|
});
|
|
@@ -36076,7 +36146,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
36076
36146
|
}
|
|
36077
36147
|
};
|
|
36078
36148
|
|
|
36079
|
-
// A
|
|
36149
|
+
// A page change, carried out and then waited for until the page it selects is
|
|
36080
36150
|
// really on screen. The container doing the swapping is the only one who knows
|
|
36081
36151
|
// when that is (observeRouteRender): a route matching is a signal changing, and
|
|
36082
36152
|
// how many passes Preact takes to answer it is its own business.
|
|
@@ -36086,7 +36156,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
36086
36156
|
// inside the callback of a view transition: the browser has stopped rendering
|
|
36087
36157
|
// and is waiting on this very promise to take its picture, so a wait that never
|
|
36088
36158
|
// ends is a page frozen under a transition that never became ready.
|
|
36089
|
-
const
|
|
36159
|
+
const whilePageRenders = async (page, change) => {
|
|
36090
36160
|
let stopListening;
|
|
36091
36161
|
const rendered = new Promise(resolve => {
|
|
36092
36162
|
// Listened for before the change, or a render landing while the change is
|
|
@@ -36095,7 +36165,7 @@ const whileRouteRenders = async (route, change) => {
|
|
|
36095
36165
|
});
|
|
36096
36166
|
try {
|
|
36097
36167
|
await change();
|
|
36098
|
-
if (
|
|
36168
|
+
if (pageIsCurrent(page)) {
|
|
36099
36169
|
await rendered;
|
|
36100
36170
|
}
|
|
36101
36171
|
} finally {
|
|
@@ -36103,9 +36173,76 @@ const whileRouteRenders = async (route, change) => {
|
|
|
36103
36173
|
}
|
|
36104
36174
|
};
|
|
36105
36175
|
|
|
36176
|
+
// A page of the row: a route, and the params that say which of its tabs when
|
|
36177
|
+
// several of them share it. Written as a bare route by a caller whose tabs are
|
|
36178
|
+
// routes of their own — which is the same page with nothing to tell apart.
|
|
36179
|
+
const normalizePage = page => page.isRoute ? {
|
|
36180
|
+
route: page,
|
|
36181
|
+
params: undefined
|
|
36182
|
+
} : page;
|
|
36183
|
+
|
|
36184
|
+
// Two pages are the same page when they select the same thing, not when they
|
|
36185
|
+
// were written by the same hand: the params of a tab are a literal in JSX, so
|
|
36186
|
+
// every render builds another object for what is plainly the same tab.
|
|
36187
|
+
const samePage = (a, b) => {
|
|
36188
|
+
if (a === b) {
|
|
36189
|
+
return true;
|
|
36190
|
+
}
|
|
36191
|
+
if (!a || !b) {
|
|
36192
|
+
return false;
|
|
36193
|
+
}
|
|
36194
|
+
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
36195
|
+
};
|
|
36196
|
+
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
|
|
36197
|
+
|
|
36198
|
+
// Whether this page is the one on screen. `matchesParams` reads paramsSignal,
|
|
36199
|
+
// so a caller reading this during a render is subscribed to the param changes
|
|
36200
|
+
// that walk from one tab to the next — matchingSignal alone never moves there,
|
|
36201
|
+
// and a row whose tabs are params of one route would never re-render.
|
|
36202
|
+
//
|
|
36203
|
+
// The params are read only for a route that matches, and that is not a signal
|
|
36204
|
+
// left unread: a reader wakes on anything it read last time, so what matters is
|
|
36205
|
+
// that everything able to make this answer change is among them.
|
|
36206
|
+
// matchingSignal is read whatever happens, and it is a NECESSARY condition —
|
|
36207
|
+
// while it is false no param of that route can put this page on screen, and the
|
|
36208
|
+
// day one could, matchingSignal itself has to turn true to say so, which is the
|
|
36209
|
+
// read that brings the params back in. (Asking anyway would be worse than
|
|
36210
|
+
// useless: the params of a route that does not match are not params.)
|
|
36211
|
+
const pageIsCurrent = ({
|
|
36212
|
+
route,
|
|
36213
|
+
params
|
|
36214
|
+
}) => {
|
|
36215
|
+
if (!route.matchingSignal.value) {
|
|
36216
|
+
return false;
|
|
36217
|
+
}
|
|
36218
|
+
return params ? route.matchesParams(params) : true;
|
|
36219
|
+
};
|
|
36220
|
+
// Every page is read, never only up to the one that answers yes: a page that is
|
|
36221
|
+
// not the current one today is the one that must wake the reader tomorrow.
|
|
36222
|
+
const currentPageIndex = pages => {
|
|
36223
|
+
let currentIndex = -1;
|
|
36224
|
+
for (let i = 0; i < pages.length; i++) {
|
|
36225
|
+
if (pageIsCurrent(pages[i])) {
|
|
36226
|
+
currentIndex = i;
|
|
36227
|
+
}
|
|
36228
|
+
}
|
|
36229
|
+
return currentIndex;
|
|
36230
|
+
};
|
|
36231
|
+
|
|
36106
36232
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
36107
36233
|
const ignoreSkipped = () => {};
|
|
36108
36234
|
|
|
36235
|
+
// The name is lent to the box that is travelling and taken back afterwards.
|
|
36236
|
+
// There is one transition in a document at a time, so one box wears it at a
|
|
36237
|
+
// time — and the others, unnamed, are simply not captured: they stay live
|
|
36238
|
+
// under the pictures rather than being frozen with the page.
|
|
36239
|
+
const nameForTravel = element => {
|
|
36240
|
+
element.style.viewTransitionName = TRAVEL_NAME;
|
|
36241
|
+
};
|
|
36242
|
+
const unnameAfterTravel = element => {
|
|
36243
|
+
element.style.viewTransitionName = "";
|
|
36244
|
+
};
|
|
36245
|
+
|
|
36109
36246
|
const routeAction = (
|
|
36110
36247
|
routeOrRoutes,
|
|
36111
36248
|
action,
|
|
@@ -38068,8 +38205,10 @@ const BinderItemContext = createContext(null);
|
|
|
38068
38205
|
|
|
38069
38206
|
/**
|
|
38070
38207
|
* What a <Link> learns from the <Nav> around it: where to draw the bar that
|
|
38071
|
-
* says "you are here",
|
|
38072
|
-
*
|
|
38208
|
+
* says "you are here", the name under which the browser is to recognise that
|
|
38209
|
+
* bar from one page to the next, and — for a row of tabs that are slides — which
|
|
38210
|
+
* <SlideContainer> they are about and which of its slides is on screen (see
|
|
38211
|
+
* nav.jsx).
|
|
38073
38212
|
*/
|
|
38074
38213
|
const NavContext = createContext(null);
|
|
38075
38214
|
|
|
@@ -38502,6 +38641,12 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
38502
38641
|
* instead of a raw `href`: the URL is built from the route (see
|
|
38503
38642
|
* `routeParams`) and "current" is derived from whether the route matches.
|
|
38504
38643
|
* @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
|
|
38644
|
+
* @param {string} [props.slide] - Makes this a tab for a slide rather than for
|
|
38645
|
+
* a URL: the area of a `<SlideContainer>` it goes to. The container is the one
|
|
38646
|
+
* the surrounding `<Nav slideContainer={id}>` names, and it is also what says
|
|
38647
|
+
* whether this tab is the current one. Nothing is written to the URL — these
|
|
38648
|
+
* are places within one screen, not pages of their own — so there is no href
|
|
38649
|
+
* and the tab behaves like a button.
|
|
38505
38650
|
* @param {string} [props.target] - Native anchor target; defaults from
|
|
38506
38651
|
* internal/external detection when omitted.
|
|
38507
38652
|
* @param {string} [props.rel] - Native anchor rel; defaults to
|
|
@@ -38591,6 +38736,7 @@ const LinkPlain = props => {
|
|
|
38591
38736
|
target,
|
|
38592
38737
|
rel,
|
|
38593
38738
|
anchor,
|
|
38739
|
+
slide,
|
|
38594
38740
|
value = href,
|
|
38595
38741
|
// visual
|
|
38596
38742
|
variant,
|
|
@@ -38641,7 +38787,9 @@ const LinkPlain = props => {
|
|
|
38641
38787
|
isAnchor,
|
|
38642
38788
|
isCurrent
|
|
38643
38789
|
} = getHrefTargetInfo(href);
|
|
38644
|
-
|
|
38790
|
+
// A tab that is a SLIDE is current when the container is on it — which the
|
|
38791
|
+
// <Nav> around reads off that container, so nothing here has to be told.
|
|
38792
|
+
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
38645
38793
|
useReportCurrentToBinderItem(innerCurrent);
|
|
38646
38794
|
controlHostProps.basePseudoState = {
|
|
38647
38795
|
...basePseudoState,
|
|
@@ -38728,6 +38876,12 @@ const LinkPlain = props => {
|
|
|
38728
38876
|
onClick,
|
|
38729
38877
|
preventDefault
|
|
38730
38878
|
} = props;
|
|
38879
|
+
// Travelling there is the container's business, said as the command anything
|
|
38880
|
+
// else in the page would say it with: the tab knows the name of a slide and
|
|
38881
|
+
// the id of the box, and nothing more about either.
|
|
38882
|
+
const goToSlide = (element, event) => {
|
|
38883
|
+
triggerNaviCommand(element, `--navi-go-to-slide:${slide}`, event);
|
|
38884
|
+
};
|
|
38731
38885
|
return jsxs(Text, {
|
|
38732
38886
|
as: "a",
|
|
38733
38887
|
color: anchor && !innerChildren ? "inherit" : undefined,
|
|
@@ -38740,6 +38894,7 @@ const LinkPlain = props => {
|
|
|
38740
38894
|
// was handed.
|
|
38741
38895
|
preventDefault: undefined,
|
|
38742
38896
|
anchor: undefined,
|
|
38897
|
+
slide: undefined,
|
|
38743
38898
|
revealOnInteraction: undefined,
|
|
38744
38899
|
variant: undefined,
|
|
38745
38900
|
current: undefined,
|
|
@@ -38753,15 +38908,43 @@ const LinkPlain = props => {
|
|
|
38753
38908
|
hrefFallback: undefined,
|
|
38754
38909
|
onClick: e => {
|
|
38755
38910
|
onClick?.(e);
|
|
38911
|
+
if (slide) {
|
|
38912
|
+
goToSlide(e.currentTarget, e);
|
|
38913
|
+
}
|
|
38756
38914
|
if (preventDefault) {
|
|
38757
38915
|
e.preventDefault();
|
|
38758
38916
|
}
|
|
38917
|
+
}
|
|
38918
|
+
// A tab with no href is not a link the browser knows how to press: it is
|
|
38919
|
+
// focusable because it says so (tabIndex below) and it answers the two
|
|
38920
|
+
// keys a button answers, since that is what it behaves like.
|
|
38921
|
+
,
|
|
38922
|
+
|
|
38923
|
+
onKeyDown: e => {
|
|
38924
|
+
props.onKeyDown?.(e);
|
|
38925
|
+
if (!slide || e.defaultPrevented) {
|
|
38926
|
+
return;
|
|
38927
|
+
}
|
|
38928
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
38929
|
+
e.preventDefault();
|
|
38930
|
+
goToSlide(e.currentTarget, e);
|
|
38931
|
+
}
|
|
38759
38932
|
},
|
|
38760
38933
|
href: href,
|
|
38761
38934
|
rel: innerRel,
|
|
38762
|
-
target: innerTarget === "_self" ? undefined : target
|
|
38935
|
+
target: innerTarget === "_self" ? undefined : target
|
|
38936
|
+
// Which slide this tab is, and which box to say it to: read by the <Nav>
|
|
38937
|
+
// around it to place the row's own bar, and by the command above to find
|
|
38938
|
+
// the container across the document.
|
|
38939
|
+
,
|
|
38940
|
+
|
|
38941
|
+
"data-slide-target": slide,
|
|
38942
|
+
commandfor: slide ? nav?.slideContainer : undefined,
|
|
38943
|
+
"aria-controls": slide ? nav?.slideContainer : undefined,
|
|
38944
|
+
tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
|
|
38945
|
+
role: slide ? "tab" : props.role,
|
|
38763
38946
|
"aria-current": isCurrent ? "page" : undefined,
|
|
38764
|
-
"aria-selected": selectionContext ? selected : undefined,
|
|
38947
|
+
"aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
|
|
38765
38948
|
"data-value-event": "navi_value",
|
|
38766
38949
|
onnavi_value: e => {
|
|
38767
38950
|
e.detail.setValue(value);
|
|
@@ -38824,6 +39007,75 @@ const css$N = /* css */`
|
|
|
38824
39007
|
--nav-padding: 0px;
|
|
38825
39008
|
--nav-border-radius: 0px;
|
|
38826
39009
|
--nav-background: transparent;
|
|
39010
|
+
--nav-current-indicator-size: 2px;
|
|
39011
|
+
--nav-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
39012
|
+
}
|
|
39013
|
+
}
|
|
39014
|
+
|
|
39015
|
+
/* The bar of a nav whose tabs are SLIDES: one element for the whole row,
|
|
39016
|
+
placed over the current tab and interpolated towards the one the picture
|
|
39017
|
+
leans on (see paintIndicatorGeometry). The two ends are written in pixels
|
|
39018
|
+
as plain numbers, so the whole of the movement is a calc() the browser
|
|
39019
|
+
runs itself — the trait then follows a finger dragging the slides without a
|
|
39020
|
+
render per frame, and rides the same animation as the track when the travel
|
|
39021
|
+
was asked for rather than dragged.
|
|
39022
|
+
No named view transition here, unlike the bar of a nav made of routes:
|
|
39023
|
+
there is no transition to be part of — the slides travel under an animation
|
|
39024
|
+
of their own, which a finger can hold. */
|
|
39025
|
+
.navi_nav[data-nav-indicator] {
|
|
39026
|
+
position: relative;
|
|
39027
|
+
|
|
39028
|
+
> .navi_nav_indicator {
|
|
39029
|
+
--x-nav-indicator-position: calc(
|
|
39030
|
+
var(--nav-indicator-position) + var(--slide-travel-progress) *
|
|
39031
|
+
var(--nav-indicator-position-delta)
|
|
39032
|
+
);
|
|
39033
|
+
--x-nav-indicator-length: calc(
|
|
39034
|
+
var(--nav-indicator-length) + var(--slide-travel-progress) *
|
|
39035
|
+
var(--nav-indicator-length-delta)
|
|
39036
|
+
);
|
|
39037
|
+
|
|
39038
|
+
position: absolute;
|
|
39039
|
+
z-index: 1;
|
|
39040
|
+
background: var(--nav-current-indicator-color);
|
|
39041
|
+
border-radius: 0.1px;
|
|
39042
|
+
pointer-events: none;
|
|
39043
|
+
}
|
|
39044
|
+
/* Nothing to draw until the row has been measured: a tab bar whose current
|
|
39045
|
+
tab is not among its links (a container on a slide no tab names) has no
|
|
39046
|
+
place to put the trait. */
|
|
39047
|
+
&:not([data-nav-indicator-measured]) > .navi_nav_indicator {
|
|
39048
|
+
display: none;
|
|
39049
|
+
}
|
|
39050
|
+
|
|
39051
|
+
&[data-nav-indicator="top"],
|
|
39052
|
+
&[data-nav-indicator="bottom"] {
|
|
39053
|
+
> .navi_nav_indicator {
|
|
39054
|
+
left: calc(var(--x-nav-indicator-position) * 1px);
|
|
39055
|
+
width: calc(var(--x-nav-indicator-length) * 1px);
|
|
39056
|
+
height: var(--nav-current-indicator-size);
|
|
39057
|
+
}
|
|
39058
|
+
}
|
|
39059
|
+
&[data-nav-indicator="top"] > .navi_nav_indicator {
|
|
39060
|
+
top: 0;
|
|
39061
|
+
}
|
|
39062
|
+
&[data-nav-indicator="bottom"] > .navi_nav_indicator {
|
|
39063
|
+
bottom: 0;
|
|
39064
|
+
}
|
|
39065
|
+
|
|
39066
|
+
&[data-nav-indicator="left"],
|
|
39067
|
+
&[data-nav-indicator="right"] {
|
|
39068
|
+
> .navi_nav_indicator {
|
|
39069
|
+
top: calc(var(--x-nav-indicator-position) * 1px);
|
|
39070
|
+
width: var(--nav-current-indicator-size);
|
|
39071
|
+
height: calc(var(--x-nav-indicator-length) * 1px);
|
|
39072
|
+
}
|
|
39073
|
+
}
|
|
39074
|
+
&[data-nav-indicator="left"] > .navi_nav_indicator {
|
|
39075
|
+
left: 0;
|
|
39076
|
+
}
|
|
39077
|
+
&[data-nav-indicator="right"] > .navi_nav_indicator {
|
|
39078
|
+
right: 0;
|
|
38827
39079
|
}
|
|
38828
39080
|
}
|
|
38829
39081
|
|
|
@@ -38979,23 +39231,43 @@ const NavStyleCSSVars = {
|
|
|
38979
39231
|
paddingRight: "--nav-padding-right",
|
|
38980
39232
|
paddingBottom: "--nav-padding-bottom",
|
|
38981
39233
|
paddingLeft: "--nav-padding-left",
|
|
38982
|
-
background: "--nav-background"
|
|
39234
|
+
background: "--nav-background",
|
|
39235
|
+
currentIndicatorColor: "--nav-current-indicator-color",
|
|
39236
|
+
currentIndicatorSize: "--nav-current-indicator-size"
|
|
39237
|
+
};
|
|
39238
|
+
const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
39239
|
+
if (currentIndicator === true) {
|
|
39240
|
+
return vertical ? "left" : "bottom";
|
|
39241
|
+
}
|
|
39242
|
+
if (currentIndicator === "top" || currentIndicator === "bottom" || currentIndicator === "left" || currentIndicator === "right") {
|
|
39243
|
+
return currentIndicator;
|
|
39244
|
+
}
|
|
39245
|
+
return null;
|
|
38983
39246
|
};
|
|
39247
|
+
|
|
38984
39248
|
/**
|
|
38985
39249
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
38986
39250
|
* currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
|
|
38987
39251
|
* currentIndicatorSlides?: boolean,
|
|
39252
|
+
* slideContainer?: string,
|
|
38988
39253
|
* }>}
|
|
38989
39254
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
|
|
38990
39255
|
* bar that says which tab one is on, said once here rather than on every
|
|
38991
39256
|
* `<Link>`. A link may still say otherwise for itself.
|
|
38992
39257
|
* @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
|
|
38993
39258
|
* 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.
|
|
39259
|
+
* going out on one and coming back on the other. For a nav made of routes it
|
|
39260
|
+
* does so by being NAMED, which is all the browser needs: any change played as
|
|
39261
|
+
* a view transition animates it on the same clock as everything else in that
|
|
39262
|
+
* transition. Inside a `RouteTravel` that means it follows the pages, and the
|
|
39263
|
+
* thumb dragging them, without either of them being told about the other. For
|
|
39264
|
+
* a nav made of slides (`slideContainer`) the bar is one element for the whole
|
|
39265
|
+
* row, and it reads the travel the container publishes.
|
|
39266
|
+
* @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
|
|
39267
|
+
* tabs are about: each one says which slide it is (`<Link slide="…">`), the
|
|
39268
|
+
* container says which one is on screen, and pressing a tab travels there.
|
|
39269
|
+
* Tabs that are places in the same screen rather than pages of their own —
|
|
39270
|
+
* nothing is written to the URL and nothing is a link.
|
|
38999
39271
|
*/
|
|
39000
39272
|
const Nav = ({
|
|
39001
39273
|
children,
|
|
@@ -39008,21 +39280,132 @@ const Nav = ({
|
|
|
39008
39280
|
currentIndicatorSlides = true,
|
|
39009
39281
|
panelPosition,
|
|
39010
39282
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
39283
|
+
slideContainer,
|
|
39011
39284
|
...props
|
|
39012
39285
|
}) => {
|
|
39013
39286
|
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
39287
|
+
const defaultRef = useRef();
|
|
39288
|
+
props.ref = props.ref || defaultRef;
|
|
39289
|
+
const navRef = props.ref;
|
|
39014
39290
|
const indicatorNameRef = useRef(null);
|
|
39015
39291
|
if (indicatorNameRef.current === null) {
|
|
39016
39292
|
indicatorNameRef.current = `navi-nav-indicator-${++navCount}`;
|
|
39017
39293
|
}
|
|
39294
|
+
const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
|
|
39295
|
+
const slideContainerElementRef = useRef(null);
|
|
39296
|
+
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
39297
|
+
|
|
39298
|
+
// Where the trait is and where it is headed, as four numbers of pixels the
|
|
39299
|
+
// CSS above interpolates between (see the .navi_nav_indicator rules). Written
|
|
39300
|
+
// by hand rather than rendered: it is read off the row as it stands, and the
|
|
39301
|
+
// travel it must agree with starts in the same frame the container publishes
|
|
39302
|
+
// it — a render would land after the movement had begun.
|
|
39303
|
+
const paintIndicatorGeometry = () => {
|
|
39304
|
+
const navElement = navRef.current;
|
|
39305
|
+
const containerElement = slideContainerElementRef.current;
|
|
39306
|
+
if (!navElement || !containerElement || !indicatorPosition) {
|
|
39307
|
+
return;
|
|
39308
|
+
}
|
|
39309
|
+
const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
|
|
39310
|
+
const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
|
|
39311
|
+
const currentArea = containerElement.getAttribute("data-slide-current");
|
|
39312
|
+
const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
|
|
39313
|
+
if (currentIndex === -1) {
|
|
39314
|
+
// On a slide no tab in this row names: there is no tab to sit under.
|
|
39315
|
+
navElement.removeAttribute("data-nav-indicator-measured");
|
|
39316
|
+
return;
|
|
39317
|
+
}
|
|
39318
|
+
const measure = tabElement => vertical ? {
|
|
39319
|
+
position: tabElement.offsetTop,
|
|
39320
|
+
length: tabElement.offsetHeight
|
|
39321
|
+
} : {
|
|
39322
|
+
position: tabElement.offsetLeft,
|
|
39323
|
+
length: tabElement.offsetWidth
|
|
39324
|
+
};
|
|
39325
|
+
const currentMeasure = measure(tabElements[currentIndex]);
|
|
39326
|
+
const towardArea = containerElement.getAttribute("data-slide-travel-toward");
|
|
39327
|
+
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
39328
|
+
let positionDelta = 0;
|
|
39329
|
+
let lengthDelta = 0;
|
|
39330
|
+
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
39331
|
+
const towardMeasure = measure(tabElements[towardIndex]);
|
|
39332
|
+
// What one box of travel is worth in pixels of this row, signed so that
|
|
39333
|
+
// the trait is exactly on the other tab when the progress is at its own
|
|
39334
|
+
// end: the container counts +1 when the picture leans on a slide sitting
|
|
39335
|
+
// BEFORE the current one and -1 when it sits after.
|
|
39336
|
+
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
39337
|
+
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
39338
|
+
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
39339
|
+
}
|
|
39340
|
+
const {
|
|
39341
|
+
style
|
|
39342
|
+
} = navElement;
|
|
39343
|
+
style.setProperty("--nav-indicator-position", currentMeasure.position);
|
|
39344
|
+
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
39345
|
+
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
39346
|
+
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
39347
|
+
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
39348
|
+
};
|
|
39349
|
+
// Reached through a ref by everything watching the DOM below: those watchers
|
|
39350
|
+
// outlive a render, and what they must run is the version of this that knows
|
|
39351
|
+
// about the row as it is now.
|
|
39352
|
+
const paintIndicatorGeometryRef = useRef(null);
|
|
39353
|
+
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
39354
|
+
useLayoutEffect(() => {
|
|
39355
|
+
if (!slideContainer) {
|
|
39356
|
+
return undefined;
|
|
39357
|
+
}
|
|
39358
|
+
const containerElement = document.getElementById(slideContainer);
|
|
39359
|
+
if (!containerElement) {
|
|
39360
|
+
console.warn(`<Nav slideContainer="${slideContainer}"> but no element with that id found`);
|
|
39361
|
+
return undefined;
|
|
39362
|
+
}
|
|
39363
|
+
slideContainerElementRef.current = containerElement;
|
|
39364
|
+
const readContainer = () => {
|
|
39365
|
+
setCurrentSlideArea(containerElement.getAttribute("data-slide-current") ?? undefined);
|
|
39366
|
+
paintIndicatorGeometryRef.current();
|
|
39367
|
+
};
|
|
39368
|
+
readContainer();
|
|
39369
|
+
// The container says where one is and what the picture leans on, and says
|
|
39370
|
+
// it in the DOM: nothing here is told, everything is read — which is what
|
|
39371
|
+
// lets this row sit anywhere on the page (above the box, in a fixed bar)
|
|
39372
|
+
// rather than inside it.
|
|
39373
|
+
const attributeObserver = new MutationObserver(readContainer);
|
|
39374
|
+
attributeObserver.observe(containerElement, {
|
|
39375
|
+
attributes: true,
|
|
39376
|
+
attributeFilter: ["data-slide-current", "data-slide-travel-toward"]
|
|
39377
|
+
});
|
|
39378
|
+
// A row whose tabs changed width — a badge count, a font that just
|
|
39379
|
+
// arrived, a window resized — is measured again: what was written is
|
|
39380
|
+
// pixels, and pixels go stale.
|
|
39381
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
39382
|
+
paintIndicatorGeometryRef.current();
|
|
39383
|
+
});
|
|
39384
|
+
sizeObserver.observe(navRef.current);
|
|
39385
|
+
return () => {
|
|
39386
|
+
attributeObserver.disconnect();
|
|
39387
|
+
sizeObserver.disconnect();
|
|
39388
|
+
slideContainerElementRef.current = null;
|
|
39389
|
+
};
|
|
39390
|
+
}, [slideContainer]);
|
|
39391
|
+
|
|
39392
|
+
// Said after every commit: a tab added, removed or renamed moves the trait,
|
|
39393
|
+
// and no observer above watches this row's own children.
|
|
39394
|
+
useLayoutEffect(() => {
|
|
39395
|
+
paintIndicatorGeometry();
|
|
39396
|
+
});
|
|
39018
39397
|
const navContextValue = useMemo(() => ({
|
|
39019
|
-
|
|
39398
|
+
// The bar belongs to the row itself when the tabs are slides, so the
|
|
39399
|
+
// links draw none of their own.
|
|
39400
|
+
currentIndicator: slideContainer ? undefined : currentIndicator,
|
|
39020
39401
|
// Read by the link that is current, and by it alone: a name belongs to
|
|
39021
39402
|
// one element at a time, and the bar exists in every tab.
|
|
39022
|
-
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null
|
|
39023
|
-
|
|
39403
|
+
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null,
|
|
39404
|
+
slideContainer,
|
|
39405
|
+
currentSlideArea
|
|
39406
|
+
}), [currentIndicator, currentIndicatorSlides, slideContainer, currentSlideArea]);
|
|
39024
39407
|
children = toChildArray(children);
|
|
39025
|
-
return
|
|
39408
|
+
return jsxs(Box, {
|
|
39026
39409
|
as: "nav",
|
|
39027
39410
|
row: vertical,
|
|
39028
39411
|
column: !vertical,
|
|
@@ -39031,15 +39414,30 @@ const Nav = ({
|
|
|
39031
39414
|
"data-expand": expand || expandX ? "" : undefined,
|
|
39032
39415
|
"data-vertical": vertical ? "" : undefined,
|
|
39033
39416
|
"data-panel-position": panelPosition,
|
|
39417
|
+
"data-nav-indicator": indicatorPosition ?? undefined
|
|
39418
|
+
// "write your travel here too": a custom property cannot be read across
|
|
39419
|
+
// the DOM, so the container paints its progress onto this element and the
|
|
39420
|
+
// trait follows in CSS alone (see SlideContainer's followerElements).
|
|
39421
|
+
,
|
|
39422
|
+
|
|
39423
|
+
"data-slide-container-follows": slideContainer
|
|
39424
|
+
// Tabs over one screen, not links to pages: a screen reader is told so,
|
|
39425
|
+
// and told which way the row runs.
|
|
39426
|
+
,
|
|
39427
|
+
|
|
39428
|
+
role: slideContainer ? "tablist" : undefined,
|
|
39429
|
+
"aria-orientation": slideContainer && vertical ? "vertical" : undefined,
|
|
39034
39430
|
expand: expand,
|
|
39035
39431
|
expandX: expandX,
|
|
39036
39432
|
spacing: spacing,
|
|
39037
39433
|
...props,
|
|
39038
39434
|
styleCSSVars: NavStyleCSSVars,
|
|
39039
|
-
children: jsx(
|
|
39435
|
+
children: [indicatorPosition && jsx("span", {
|
|
39436
|
+
className: "navi_nav_indicator"
|
|
39437
|
+
}), jsx(NavContext.Provider, {
|
|
39040
39438
|
value: navContextValue,
|
|
39041
39439
|
children: children
|
|
39042
|
-
})
|
|
39440
|
+
})]
|
|
39043
39441
|
});
|
|
39044
39442
|
};
|
|
39045
39443
|
|
|
@@ -45922,7 +46320,16 @@ const SlideContainer = ({
|
|
|
45922
46320
|
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
45923
46321
|
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
45924
46322
|
const travelProgressFromRef = useRef(null);
|
|
45925
|
-
|
|
46323
|
+
// One per element painting the progress: the box, plus everything following
|
|
46324
|
+
// it (see followerElementsRef). All started together and with the same
|
|
46325
|
+
// options, so they are one movement said in several places.
|
|
46326
|
+
const progressAnimationsRef = useRef([]);
|
|
46327
|
+
// Elements outside the box that draw something about this travel — a tab bar
|
|
46328
|
+
// above it, most of all. A custom property cannot be read across the DOM, so
|
|
46329
|
+
// the progress is WRITTEN on each of them: they then interpolate whatever they
|
|
46330
|
+
// draw in CSS alone, at the pace of the travel and under the finger, with
|
|
46331
|
+
// nothing measured per frame.
|
|
46332
|
+
const followerElementsRef = useRef([]);
|
|
45926
46333
|
const current = rollingArea ?? provisionalArea ?? currentProp ?? currentAreaState;
|
|
45927
46334
|
const vertical = layout === "column";
|
|
45928
46335
|
// What the map has, and what each way of asking is allowed to use of it.
|
|
@@ -46048,6 +46455,28 @@ const SlideContainer = ({
|
|
|
46048
46455
|
...map
|
|
46049
46456
|
};
|
|
46050
46457
|
};
|
|
46458
|
+
|
|
46459
|
+
// Who is drawing something about this box from outside it: a tab bar saying
|
|
46460
|
+
// where one is, a row of dots. They name the box they follow by its id, the
|
|
46461
|
+
// way everything else that talks to it across the document does (commandfor).
|
|
46462
|
+
const readFollowerElements = () => {
|
|
46463
|
+
const containerEl = containerRef.current;
|
|
46464
|
+
const {
|
|
46465
|
+
id
|
|
46466
|
+
} = containerEl;
|
|
46467
|
+
if (!id) {
|
|
46468
|
+
return [];
|
|
46469
|
+
}
|
|
46470
|
+
return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
|
|
46471
|
+
};
|
|
46472
|
+
|
|
46473
|
+
// Which slide is on screen, said in the DOM: it is what anything outside the
|
|
46474
|
+
// box reads to know where one is (a tab bar marking its current tab), and
|
|
46475
|
+
// there is nothing else for it to read — a slide the container holds by
|
|
46476
|
+
// itself is known to no one else.
|
|
46477
|
+
const paintCurrentArea = area => {
|
|
46478
|
+
containerRef.current?.setAttribute("data-slide-current", area);
|
|
46479
|
+
};
|
|
46051
46480
|
const markAnswered = area => {
|
|
46052
46481
|
const order = readMap().slideElements.map(readArea);
|
|
46053
46482
|
const rank = order.indexOf(area);
|
|
@@ -46130,6 +46559,10 @@ const SlideContainer = ({
|
|
|
46130
46559
|
// the children free — their shape says nothing about the arrangement, the map
|
|
46131
46560
|
// does — and it is also the only place that has to agree with itself.
|
|
46132
46561
|
useLayoutEffect(() => {
|
|
46562
|
+
// Read on every render rather than subscribed to: a follower says who it
|
|
46563
|
+
// follows in the DOM, and the render that mounted one is the render this
|
|
46564
|
+
// runs after.
|
|
46565
|
+
followerElementsRef.current = readFollowerElements();
|
|
46133
46566
|
const {
|
|
46134
46567
|
slideElements,
|
|
46135
46568
|
placeOf
|
|
@@ -46142,6 +46575,7 @@ const SlideContainer = ({
|
|
|
46142
46575
|
// shown, the way a stack of pages opens on its first page.
|
|
46143
46576
|
slideElements[0];
|
|
46144
46577
|
const currentArea = readArea(currentElement);
|
|
46578
|
+
paintCurrentArea(currentArea);
|
|
46145
46579
|
const realPlaceOf = area => placeOf.get(area) || {
|
|
46146
46580
|
x: 0,
|
|
46147
46581
|
y: 0
|
|
@@ -46287,7 +46721,9 @@ const SlideContainer = ({
|
|
|
46287
46721
|
// there was one, from a whole box away when the travel was asked for.
|
|
46288
46722
|
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
46289
46723
|
travelProgressFromRef.current = null;
|
|
46290
|
-
|
|
46724
|
+
// The slide the picture leans on for the length of it is the one being
|
|
46725
|
+
// LEFT: it is the second one in the frame until the travel is over.
|
|
46726
|
+
animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
|
|
46291
46727
|
// Presses still waiting behind this one: it is already late, so it is
|
|
46292
46728
|
// sent home at once rather than played out at the pace of someone who
|
|
46293
46729
|
// has stopped pressing. Someone pressing → four times is asking to be
|
|
@@ -46707,6 +47143,30 @@ const SlideContainer = ({
|
|
|
46707
47143
|
paintTravelProgress(drag.progress, drag.areaPulled);
|
|
46708
47144
|
};
|
|
46709
47145
|
|
|
47146
|
+
// Everything that draws this travel: the box itself and whoever follows it.
|
|
47147
|
+
const travelPainters = () => {
|
|
47148
|
+
const containerEl = containerRef.current;
|
|
47149
|
+
if (!containerEl) {
|
|
47150
|
+
return [];
|
|
47151
|
+
}
|
|
47152
|
+
return [containerEl, ...followerElementsRef.current];
|
|
47153
|
+
};
|
|
47154
|
+
|
|
47155
|
+
// Which OTHER slide the picture leans on while it is not on the current one:
|
|
47156
|
+
// the slide being pulled in under a finger, the slide being left during a
|
|
47157
|
+
// travel. Two slides are in the frame and the number below says how far
|
|
47158
|
+
// between them one is — this says which the second one is, so a trait can be
|
|
47159
|
+
// drawn between two places rather than merely offset from one.
|
|
47160
|
+
const paintTravelToward = area => {
|
|
47161
|
+
for (const element of travelPainters()) {
|
|
47162
|
+
if (area) {
|
|
47163
|
+
element.setAttribute("data-slide-travel-toward", area);
|
|
47164
|
+
} else {
|
|
47165
|
+
element.removeAttribute("data-slide-travel-toward");
|
|
47166
|
+
}
|
|
47167
|
+
}
|
|
47168
|
+
};
|
|
47169
|
+
|
|
46710
47170
|
// Where the picture stands relative to the slide that is CURRENT, in boxes:
|
|
46711
47171
|
// 0 on it, +1 one whole box before it, -1 one box after. Written on the
|
|
46712
47172
|
// container so an indicator drawn inside the box — a tab bar, a dot row, a
|
|
@@ -46715,45 +47175,50 @@ const SlideContainer = ({
|
|
|
46715
47175
|
// gesture, so the number stays continuous when the travel commits and the
|
|
46716
47176
|
// current slide changes under it.
|
|
46717
47177
|
const paintTravelProgress = (progress, area) => {
|
|
46718
|
-
const
|
|
46719
|
-
|
|
46720
|
-
|
|
46721
|
-
|
|
46722
|
-
|
|
46723
|
-
|
|
46724
|
-
containerEl.removeAttribute("data-slide-travel-to");
|
|
46725
|
-
return;
|
|
47178
|
+
for (const element of travelPainters()) {
|
|
47179
|
+
if (progress) {
|
|
47180
|
+
element.style.setProperty("--slide-travel-progress", progress);
|
|
47181
|
+
} else {
|
|
47182
|
+
element.style.removeProperty("--slide-travel-progress");
|
|
47183
|
+
}
|
|
46726
47184
|
}
|
|
46727
|
-
|
|
46728
|
-
|
|
46729
|
-
|
|
46730
|
-
|
|
46731
|
-
|
|
47185
|
+
paintTravelToward(progress ? area : null);
|
|
47186
|
+
};
|
|
47187
|
+
const cancelTravelProgressAnimation = () => {
|
|
47188
|
+
for (const animation of progressAnimationsRef.current) {
|
|
47189
|
+
animation.cancel();
|
|
46732
47190
|
}
|
|
47191
|
+
progressAnimationsRef.current = [];
|
|
46733
47192
|
};
|
|
46734
47193
|
|
|
46735
47194
|
// The indicator, brought home at the pace of the travel it belongs to: the
|
|
46736
47195
|
// same duration and the same easing as the track, so the trait and the slides
|
|
46737
47196
|
// 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
|
-
|
|
47197
|
+
// the animation is left to fall away on its own — only the name of the slide
|
|
47198
|
+
// being leant on is taken back by hand, at the end.
|
|
47199
|
+
const animateTravelProgress = (from, durationMs, easing, area) => {
|
|
47200
|
+
// Read again at the start of every travel, not only at every render: a
|
|
47201
|
+
// follower appearing does not make this box render, and the travel it must
|
|
47202
|
+
// draw is the one about to start.
|
|
47203
|
+
followerElementsRef.current = readFollowerElements();
|
|
47204
|
+
cancelTravelProgressAnimation();
|
|
46743
47205
|
paintTravelProgress(0);
|
|
46744
|
-
|
|
47206
|
+
const painters = travelPainters();
|
|
47207
|
+
if (!painters.length || !from || !durationMs) {
|
|
46745
47208
|
return;
|
|
46746
47209
|
}
|
|
46747
|
-
|
|
47210
|
+
paintTravelToward(area);
|
|
47211
|
+
progressAnimationsRef.current = painters.map(element => element.animate([{
|
|
46748
47212
|
"--slide-travel-progress": from
|
|
46749
47213
|
}, {
|
|
46750
47214
|
"--slide-travel-progress": 0
|
|
46751
47215
|
}], {
|
|
46752
47216
|
duration: durationMs,
|
|
46753
47217
|
easing
|
|
46754
|
-
});
|
|
46755
|
-
|
|
46756
|
-
|
|
47218
|
+
}));
|
|
47219
|
+
progressAnimationsRef.current[0].finished.then(() => {
|
|
47220
|
+
progressAnimationsRef.current = [];
|
|
47221
|
+
paintTravelToward(null);
|
|
46757
47222
|
}, () => {
|
|
46758
47223
|
// cancelled by the next travel — that one says where the trait goes
|
|
46759
47224
|
});
|
|
@@ -46826,7 +47291,7 @@ const SlideContainer = ({
|
|
|
46826
47291
|
settleTravel();
|
|
46827
47292
|
return;
|
|
46828
47293
|
}
|
|
46829
|
-
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out");
|
|
47294
|
+
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out", drag.areaPulled);
|
|
46830
47295
|
const animation = track.animate([{
|
|
46831
47296
|
translate: drag.offset
|
|
46832
47297
|
}, {
|
|
@@ -46952,8 +47417,8 @@ const SlideContainer = ({
|
|
|
46952
47417
|
caughtTravel = null;
|
|
46953
47418
|
trackAnimationRef.current?.cancel();
|
|
46954
47419
|
trackAnimationRef.current = null;
|
|
46955
|
-
|
|
46956
|
-
|
|
47420
|
+
cancelTravelProgressAnimation();
|
|
47421
|
+
followerElementsRef.current = readFollowerElements();
|
|
46957
47422
|
drag.axis = axis;
|
|
46958
47423
|
drag.areaBack = areaBack;
|
|
46959
47424
|
drag.areaOn = areaOn;
|
|
@@ -47098,6 +47563,10 @@ const SlideContainer = ({
|
|
|
47098
47563
|
...handlers
|
|
47099
47564
|
});
|
|
47100
47565
|
if (!gesture) {
|
|
47566
|
+
// Not a press this box can be about — something that reads the pointer
|
|
47567
|
+
// itself, a box below it that travels the same way. Whatever the press
|
|
47568
|
+
// stopped on its way in goes back on its way.
|
|
47569
|
+
handlers.onGiveUp();
|
|
47101
47570
|
return;
|
|
47102
47571
|
}
|
|
47103
47572
|
handlers.drag.gesture = gesture;
|
|
@@ -47144,7 +47613,7 @@ const SlideContainer = ({
|
|
|
47144
47613
|
return () => {
|
|
47145
47614
|
dragRef.current?.gesture?.stop();
|
|
47146
47615
|
dragRef.current = null;
|
|
47147
|
-
|
|
47616
|
+
cancelTravelProgressAnimation();
|
|
47148
47617
|
};
|
|
47149
47618
|
}, []);
|
|
47150
47619
|
|
|
@@ -47209,10 +47678,16 @@ const SlideContainer = ({
|
|
|
47209
47678
|
"data-slide-container": ""
|
|
47210
47679
|
// Which axes a touch may travel on, said in the DOM: what the browser
|
|
47211
47680
|
// does with a finger is decided by CSS (touch-action) before any of this
|
|
47212
|
-
// has seen the gesture
|
|
47681
|
+
// has seen the gesture — and it is also what a box HOLDING this one reads
|
|
47682
|
+
// to know the gesture is not its own (see drag_to_travel.js).
|
|
47213
47683
|
,
|
|
47214
47684
|
|
|
47215
47685
|
"data-travel-by-drag": dragAxes ?? undefined
|
|
47686
|
+
// The same fact for a wheel, and only for that second reason: this box
|
|
47687
|
+
// takes the push, whatever the box around it also travels on.
|
|
47688
|
+
,
|
|
47689
|
+
|
|
47690
|
+
"data-travel-by-wheel": scrollAxes ?? undefined
|
|
47216
47691
|
// The same fact, read by the shared gesture stylesheet: what scrolls
|
|
47217
47692
|
// inside a box that travels must not spill onto the page behind it (see
|
|
47218
47693
|
// drag_to_travel.js).
|
|
@@ -59490,8 +59965,16 @@ const useWheelInteractions = ({
|
|
|
59490
59965
|
document.removeEventListener("wheel", onDocumentWheel, {
|
|
59491
59966
|
capture: true
|
|
59492
59967
|
});
|
|
59968
|
+
releaseWheelGesture(vp);
|
|
59493
59969
|
};
|
|
59494
59970
|
const keepClaimingGesture = () => {
|
|
59971
|
+
// Said out loud as well as swallowed: preventDefault only settles it with
|
|
59972
|
+
// the browser, and a box that travels with the wheel answers the burst
|
|
59973
|
+
// from a listener of its own — it asks who owns the gesture instead (see
|
|
59974
|
+
// wheel_gesture.js in @jsenv/dom).
|
|
59975
|
+
claimWheelGesture(vp, {
|
|
59976
|
+
delay: WHEEL_GESTURE_MAX_GAP
|
|
59977
|
+
});
|
|
59495
59978
|
if (!gestureGuardTimer) {
|
|
59496
59979
|
document.addEventListener("wheel", onDocumentWheel, {
|
|
59497
59980
|
capture: true,
|
|
@@ -59502,6 +59985,12 @@ const useWheelInteractions = ({
|
|
|
59502
59985
|
gestureGuardTimer = setTimeout(stopClaimingGesture, WHEEL_GESTURE_MAX_GAP);
|
|
59503
59986
|
};
|
|
59504
59987
|
const onWheel = e => {
|
|
59988
|
+
// The burst belongs to something else — a box that travels with the
|
|
59989
|
+
// wheel, another wheel the pointer has just left. It is theirs until the
|
|
59990
|
+
// events stop coming.
|
|
59991
|
+
if (wheelGestureIsTakenFrom(vp)) {
|
|
59992
|
+
return;
|
|
59993
|
+
}
|
|
59505
59994
|
const raw = isHorizontal ? e.deltaX || e.deltaY : e.deltaY;
|
|
59506
59995
|
if (!raw) {
|
|
59507
59996
|
return;
|