@jsenv/navi 0.29.523 → 0.30.0
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/dist/dev/jsenv_navi.js +1473 -2386
- package/dist/dev/jsenv_navi.js.map +35 -32
- package/dist/jsenv_navi.js +1473 -2372
- package/dist/jsenv_navi.js.map +35 -32
- package/docs/AI_INSTRUCTIONS.md +4 -3
- package/docs/create_and_edit.md +6 -17
- package/docs/dialog_shape.md +7 -0
- package/docs/drag_to_travel.md +115 -132
- package/docs/dynamic_import.md +1 -1
- package/docs/interactions.md +6 -0
- package/docs/mobile_touch.md +3 -3
- package/docs/navigation.md +54 -31
- package/docs/popup_open.md +7 -0
- package/docs/route_transitions.md +6 -16
- package/docs/scroll.md +3 -1
- package/docs/view_transitions.md +4 -7
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
-
import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition,
|
|
6
|
+
import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight, subscribeWindowResizeSettled, subscribeVirtualKeyboardGeometryChange, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent } from "@jsenv/dom";
|
|
7
7
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
9
9
|
import { useState, useLayoutEffect, useErrorBoundary, useContext, useMemo, useRef, useReducer, useEffect, useCallback } from "preact/hooks";
|
|
10
|
-
import { options, isValidElement, createContext, render as render$1, h, toChildArray,
|
|
10
|
+
import { options, isValidElement, createContext, render as render$1, h, toChildArray, cloneElement, createRef, createElement, Fragment as Fragment$1 } from "preact";
|
|
11
11
|
import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatMinuteDuration, formatTime, formatSecondDuration, formatHourDuration, formatTimeRelative, pluralCategory, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
12
12
|
export { createI18n, formatByteSize, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatList, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText, pluralCategory } from "@jsenv/humanize";
|
|
13
13
|
import { createValidity, CHAR_CLASS_PRESETS, durationContainsNaN, compareTwoDurations, TYPE_RULE, durationToSeconds, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, parseDuration, durationToISOString } from "@jsenv/validity";
|
|
@@ -417,8 +417,7 @@ const flushSyncRendering = (fn) => {
|
|
|
417
417
|
* it was left, and the bottom arrives under a page that has already jumped. So
|
|
418
418
|
* whoever writes an arrival's offset builds first the boxes that offset shows
|
|
419
419
|
* (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
|
|
420
|
-
* right after it (the offset
|
|
421
|
-
* layout.
|
|
420
|
+
* right after it (the offset) reads the layout.
|
|
422
421
|
*/
|
|
423
422
|
|
|
424
423
|
const deferredBoxes = new Set();
|
|
@@ -519,9 +518,8 @@ const readTopInWindow = element => {
|
|
|
519
518
|
* around Preact's `options.debounceRendering`, and two of them installed
|
|
520
519
|
* independently restore each other in the wrong order when they let go — every
|
|
521
520
|
* render queued in between is then handed to a wrapper nobody will ever
|
|
522
|
-
* release. Everything that photographs a navigation
|
|
523
|
-
*
|
|
524
|
-
* wrapper of its own.
|
|
521
|
+
* release. Everything that photographs a navigation must therefore hold through
|
|
522
|
+
* this module, never through a wrapper of its own.
|
|
525
523
|
*/
|
|
526
524
|
|
|
527
525
|
|
|
@@ -9829,8 +9827,8 @@ const haveSameMembers = (setA, setB) => {
|
|
|
9829
9827
|
* A route matching is a signal changing, and the page it selects reaches the
|
|
9830
9828
|
* DOM only once Preact has rendered — an unknown number of passes later, in an
|
|
9831
9829
|
* unknown number of microtasks. Anyone who needs the page as it IS rather than
|
|
9832
|
-
* as it has been decided (a
|
|
9833
|
-
* browser
|
|
9830
|
+
* as it has been decided (a route transition about to have its picture taken by
|
|
9831
|
+
* the browser) waits for this instead of counting.
|
|
9834
9832
|
*
|
|
9835
9833
|
* A page waiting on data is announced too, by the boundary showing its loading
|
|
9836
9834
|
* state (see Loading in use_async_data.jsx): what the container could put on
|
|
@@ -23591,10 +23589,10 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
|
|
|
23591
23589
|
*
|
|
23592
23590
|
* A popup is painted OVER the page and stays a DOM descendant of whatever it
|
|
23593
23591
|
* was written in — a dialog inside the card that opens it, a callout beside the
|
|
23594
|
-
* button it belongs to. Its pointer events therefore bubble through
|
|
23595
|
-
* covers, and a box reading a gesture off one of them answers for
|
|
23596
|
-
* nobody aimed at it: a finger held in a sheet opening the card
|
|
23597
|
-
* sheet,
|
|
23592
|
+
* button it belongs to. Its pointer and key events therefore bubble through
|
|
23593
|
+
* boxes it covers, and a box reading a gesture off one of them answers for
|
|
23594
|
+
* something nobody aimed at it: a finger held in a sheet opening the card
|
|
23595
|
+
* underneath the sheet, an arrow pressed in a sheet walking the slides behind it.
|
|
23598
23596
|
*
|
|
23599
23597
|
* The same list, read for the same reason, as the drag sources in @jsenv/dom
|
|
23600
23598
|
* (see DRAG_IGNORED_SELECTOR in drag_to.js) and as the selection rule in
|
|
@@ -24333,26 +24331,7 @@ const ensureDocumentStartViewTransition = () => {
|
|
|
24333
24331
|
startViewTransitionPolyfill.isPolyfill = true;
|
|
24334
24332
|
document.startViewTransition = startViewTransitionPolyfill;
|
|
24335
24333
|
}
|
|
24336
|
-
return startViewTransition$
|
|
24337
|
-
};
|
|
24338
|
-
|
|
24339
|
-
// A transition a finger is holding still (see route_travel.jsx): it has to be
|
|
24340
|
-
// let go of before any other one starts, and this is the only place that knows
|
|
24341
|
-
// a new one is about to. There is a single transition per document — starting
|
|
24342
|
-
// one SKIPS the one in flight — and the hold that keeps pictures under a finger
|
|
24343
|
-
// is written against whatever transition is running, because everything the
|
|
24344
|
-
// gesture must carry along (a trait under a tab row) belongs to that same
|
|
24345
|
-
// transition and has no name of its own here. Left on, it takes hold of the
|
|
24346
|
-
// transition that has just replaced ours: born paused, held by nobody, it never
|
|
24347
|
-
// finishes, and its pictures stand over a page that cannot be touched anymore.
|
|
24348
|
-
let releaseHeldViewTransition = null;
|
|
24349
|
-
const holdViewTransition = (release) => {
|
|
24350
|
-
releaseHeldViewTransition = release;
|
|
24351
|
-
return () => {
|
|
24352
|
-
if (releaseHeldViewTransition === release) {
|
|
24353
|
-
releaseHeldViewTransition = null;
|
|
24354
|
-
}
|
|
24355
|
-
};
|
|
24334
|
+
return startViewTransition$1;
|
|
24356
24335
|
};
|
|
24357
24336
|
|
|
24358
24337
|
// The last transition started here, for code that runs a callback of the
|
|
@@ -24362,12 +24341,7 @@ const holdViewTransition = (release) => {
|
|
|
24362
24341
|
let lastViewTransition = null;
|
|
24363
24342
|
const peekLastViewTransition = () => lastViewTransition;
|
|
24364
24343
|
|
|
24365
|
-
const startViewTransition$
|
|
24366
|
-
if (releaseHeldViewTransition) {
|
|
24367
|
-
const release = releaseHeldViewTransition;
|
|
24368
|
-
releaseHeldViewTransition = null;
|
|
24369
|
-
release();
|
|
24370
|
-
}
|
|
24344
|
+
const startViewTransition$1 = (updateCallback) => {
|
|
24371
24345
|
const viewTransition = document.startViewTransition(updateCallback);
|
|
24372
24346
|
viewTransition.updateCallbackDone.catch(ignoreSkip);
|
|
24373
24347
|
viewTransition.ready.catch(ignoreSkip);
|
|
@@ -26236,6 +26210,9 @@ const createOnKeyDownForShortcutArray = (
|
|
|
26236
26210
|
};
|
|
26237
26211
|
};
|
|
26238
26212
|
/**
|
|
26213
|
+
* A key pressed in a popup nested in the listening element is not offered to
|
|
26214
|
+
* these shortcuts: see applyKeyboardShortcuts.
|
|
26215
|
+
*
|
|
26239
26216
|
* @param {object} shortcuts
|
|
26240
26217
|
* @param {object} [options]
|
|
26241
26218
|
* @param {(target: Element) => boolean} [options.ownsTarget] Whether a
|
|
@@ -26261,6 +26238,13 @@ const createOnKeyDownForShortcuts = (shortcuts, { ownsTarget } = {}) => {
|
|
|
26261
26238
|
const applyKeyboardShortcuts = (shortcuts, keyboardEvent, { ownsTarget }) => {
|
|
26262
26239
|
const currentTarget = keyboardEvent.currentTarget;
|
|
26263
26240
|
const target = keyboardEvent.target;
|
|
26241
|
+
// A popup stays a DOM descendant of what it was written in, so a key pressed
|
|
26242
|
+
// in it bubbles through boxes the reader cannot reach while it is open. Those
|
|
26243
|
+
// boxes have no shortcut to answer it with: an arrow in a sheet does not walk
|
|
26244
|
+
// the slides behind it, and Escape closes the popup it was pressed in.
|
|
26245
|
+
if (isPressOnLayerOver(target, currentTarget)) {
|
|
26246
|
+
return null;
|
|
26247
|
+
}
|
|
26264
26248
|
let canIntercept;
|
|
26265
26249
|
if (target === currentTarget) {
|
|
26266
26250
|
// the event occurs on the element itself so we are sure we won't interfere with native behaviors of child elements
|
|
@@ -26496,6 +26480,9 @@ const isSameKey = (browserEventKey, key) => {
|
|
|
26496
26480
|
* well, whether the gate lets the interaction through or refuses it and says
|
|
26497
26481
|
* why — the same as a number field's arrows.
|
|
26498
26482
|
*
|
|
26483
|
+
* A key pressed in a popup nested in the element is not this element's, like a
|
|
26484
|
+
* press there (see isPressOnLayerOver).
|
|
26485
|
+
*
|
|
26499
26486
|
* The interaction is dispatched under its full name, colon included, so
|
|
26500
26487
|
* `findEvent(event, "keyboard:ctrl+backspace")` gets back to it — the shortcut
|
|
26501
26488
|
* that asked is what the action wants to know, not that a key went down.
|
|
@@ -32008,8 +31995,7 @@ const LoadingFallback = ({
|
|
|
32008
31995
|
// A page that suspends never gets to say it is on screen — its own effects
|
|
32009
31996
|
// are held with it — so this says it for it: what the document shows of the
|
|
32010
31997
|
// page arriving is this. Anyone waiting for the page to be there before
|
|
32011
|
-
// moving (a
|
|
32012
|
-
// would otherwise wait for a render that cannot happen until the data does.
|
|
31998
|
+
// moving (a route transition about to have its picture taken) would otherwise wait for a render that cannot happen until the data does.
|
|
32013
31999
|
useLayoutEffect(() => {
|
|
32014
32000
|
publishRouteRender();
|
|
32015
32001
|
});
|
|
@@ -36377,29 +36363,6 @@ const samePages = (pages, otherPages) => {
|
|
|
36377
36363
|
|
|
36378
36364
|
installImportMetaCssBuild(import.meta);
|
|
36379
36365
|
|
|
36380
|
-
/**
|
|
36381
|
-
* Keep every container showing the page it is showing, whatever the routes say.
|
|
36382
|
-
*
|
|
36383
|
-
* For a caller whose picture of a page is LIVE and must not follow the router:
|
|
36384
|
-
* a travel being undone shows the page it is going back to on both sides at
|
|
36385
|
-
* once if the router is allowed to swap under it (see route_travel.jsx). Only
|
|
36386
|
-
* the pages are held still — the rest of the document goes on rendering, which
|
|
36387
|
-
* is the whole reason this lives here rather than in Preact's own scheduler.
|
|
36388
|
-
*/
|
|
36389
|
-
let routeRenderFrozen = false;
|
|
36390
|
-
const routeRenderFrozenSignal = signal(0);
|
|
36391
|
-
const freezeRouteRender = () => {
|
|
36392
|
-
routeRenderFrozen = true;
|
|
36393
|
-
return () => {
|
|
36394
|
-
if (!routeRenderFrozen) {
|
|
36395
|
-
return;
|
|
36396
|
-
}
|
|
36397
|
-
routeRenderFrozen = false;
|
|
36398
|
-
// Read during every container's render, so letting go brings them all back.
|
|
36399
|
-
routeRenderFrozenSignal.value++;
|
|
36400
|
-
};
|
|
36401
|
-
};
|
|
36402
|
-
|
|
36403
36366
|
/**
|
|
36404
36367
|
* Keep the page a container leaves, hidden, until its route transition is over.
|
|
36405
36368
|
*
|
|
@@ -36470,8 +36433,11 @@ const debug$1 = (...args) => {
|
|
|
36470
36433
|
* collectBranches).
|
|
36471
36434
|
*/
|
|
36472
36435
|
const Route = props => {
|
|
36436
|
+
const inRow = useContext(RouteRowContext);
|
|
36473
36437
|
if (props.children) {
|
|
36474
|
-
return jsx(
|
|
36438
|
+
return inRow ? jsx(RouteRowContainer, {
|
|
36439
|
+
...props
|
|
36440
|
+
}) : jsx(RouteContainer, {
|
|
36475
36441
|
...props
|
|
36476
36442
|
});
|
|
36477
36443
|
}
|
|
@@ -36535,6 +36501,105 @@ const collectRoutePages = children => {
|
|
|
36535
36501
|
return pages;
|
|
36536
36502
|
};
|
|
36537
36503
|
|
|
36504
|
+
/**
|
|
36505
|
+
* Whether the next container down is a row a hand scrolls: every page has a
|
|
36506
|
+
* place on it from the start, and the browser moves between them (see
|
|
36507
|
+
* RouteTravel's `nativeScroll`). Told to one container, not to the pages it holds.
|
|
36508
|
+
*/
|
|
36509
|
+
const RouteRowContext = createContext(false);
|
|
36510
|
+
|
|
36511
|
+
// A container as a row: one pane per leaf, in the order they are written. A page
|
|
36512
|
+
// still builds when its route first matches, since a route's actions only ask
|
|
36513
|
+
// while it does: before that its pane is empty. Once built it stays, with what
|
|
36514
|
+
// it holds and where it was scrolled to, so a hand brings back the page itself
|
|
36515
|
+
// and the address coming back to it finds it there.
|
|
36516
|
+
//
|
|
36517
|
+
// A page not on screen is treated as one kept while leaving, except that this
|
|
36518
|
+
// can be undone: it does not re-render for what it reads (isShown), and it steps
|
|
36519
|
+
// out of whatever it fills outside its pane (leavingSignal).
|
|
36520
|
+
const RouteRowContainer = ({
|
|
36521
|
+
children
|
|
36522
|
+
}) => {
|
|
36523
|
+
const {
|
|
36524
|
+
activeBranch,
|
|
36525
|
+
leaves
|
|
36526
|
+
} = collectBranches(children);
|
|
36527
|
+
const childrenRef = useRef(null);
|
|
36528
|
+
childrenRef.current = children;
|
|
36529
|
+
const readShownBranchRef = useRef(null);
|
|
36530
|
+
if (!readShownBranchRef.current) {
|
|
36531
|
+
readShownBranchRef.current = () => collectBranches(childrenRef.current).activeBranch;
|
|
36532
|
+
}
|
|
36533
|
+
const parentPage = useContext(RoutePageContext);
|
|
36534
|
+
const pagesRef = useRef(new Map());
|
|
36535
|
+
const pageOf = branch => {
|
|
36536
|
+
let page = pagesRef.current.get(branch.id);
|
|
36537
|
+
if (!page) {
|
|
36538
|
+
page = createRoutePage(readShownBranchRef.current, parentPage);
|
|
36539
|
+
pagesRef.current.set(branch.id, page);
|
|
36540
|
+
}
|
|
36541
|
+
page.branchId = branch.id;
|
|
36542
|
+
// Never answering for another pane: two leaves rendering the same element
|
|
36543
|
+
// are two pages here, each in its pane, where a container carries one over.
|
|
36544
|
+
page.element = null;
|
|
36545
|
+
return page;
|
|
36546
|
+
};
|
|
36547
|
+
const shownPage = activeBranch ? pageOf(activeBranch) : null;
|
|
36548
|
+
const containerKey = useRef({}).current;
|
|
36549
|
+
useLayoutEffect(() => {
|
|
36550
|
+
publishRouteRender();
|
|
36551
|
+
reportContainerShows(containerKey, {
|
|
36552
|
+
parentPage,
|
|
36553
|
+
page: shownPage,
|
|
36554
|
+
branchType: activeBranch ? activeBranch.type : null,
|
|
36555
|
+
routePage: branchRoutePage(activeBranch)
|
|
36556
|
+
});
|
|
36557
|
+
});
|
|
36558
|
+
useLayoutEffect(() => {
|
|
36559
|
+
return () => {
|
|
36560
|
+
forgetContainer(containerKey);
|
|
36561
|
+
};
|
|
36562
|
+
}, []);
|
|
36563
|
+
if (activeBranch && activeBranch.type === "fallback") {
|
|
36564
|
+
// An address the row has no place for: nothing to scroll between.
|
|
36565
|
+
return jsx(RouteRowPane, {
|
|
36566
|
+
branch: activeBranch,
|
|
36567
|
+
page: shownPage,
|
|
36568
|
+
shown: true
|
|
36569
|
+
}, "fallback");
|
|
36570
|
+
}
|
|
36571
|
+
return leaves.map(leaf => jsx(RouteRowPane, {
|
|
36572
|
+
branch: leaf,
|
|
36573
|
+
page: pageOf(leaf),
|
|
36574
|
+
shown: activeBranch !== null && leaf.id === activeBranch.id
|
|
36575
|
+
}, leaf.id));
|
|
36576
|
+
};
|
|
36577
|
+
const RouteRowPane = ({
|
|
36578
|
+
branch,
|
|
36579
|
+
page,
|
|
36580
|
+
shown
|
|
36581
|
+
}) => {
|
|
36582
|
+
// The very vnode rendered last, while not on screen: Preact does not look into
|
|
36583
|
+
// it, and a page nobody can see costs nothing when the address moves.
|
|
36584
|
+
const contentRef = useRef(null);
|
|
36585
|
+
if (shown) {
|
|
36586
|
+
contentRef.current = h(RouteRowContext.Provider, {
|
|
36587
|
+
value: false
|
|
36588
|
+
}, h(RoutePageContext.Provider, {
|
|
36589
|
+
value: page
|
|
36590
|
+
}, branch.node));
|
|
36591
|
+
}
|
|
36592
|
+
useLayoutEffect(() => {
|
|
36593
|
+
page.ownLeavingSignal.value = !shown;
|
|
36594
|
+
}, [shown]);
|
|
36595
|
+
return jsx("div", {
|
|
36596
|
+
"data-route-row-pane": "",
|
|
36597
|
+
"data-built": contentRef.current ? "" : undefined,
|
|
36598
|
+
inert: !shown,
|
|
36599
|
+
children: contentRef.current
|
|
36600
|
+
});
|
|
36601
|
+
};
|
|
36602
|
+
|
|
36538
36603
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
36539
36604
|
// and renders only that branch — or the fallback if nothing matches.
|
|
36540
36605
|
// No contexts, no state of its own: it reads the route signals directly, and
|
|
@@ -36551,16 +36616,7 @@ const RouteContainer = ({
|
|
|
36551
36616
|
pages
|
|
36552
36617
|
} = collectBranches(children);
|
|
36553
36618
|
const fallbackPage = fallbackBranch ? fallbackBranch.page : null;
|
|
36554
|
-
|
|
36555
|
-
// Told to hold still: what is on screen stays on screen. Kept as the very
|
|
36556
|
-
// vnode that was rendered last time, which is how Preact is told there is
|
|
36557
|
-
// nothing to look at in that subtree.
|
|
36558
|
-
const frozen = routeRenderFrozenSignal.value >= 0 && routeRenderFrozen;
|
|
36559
|
-
const shownBranchRef = useRef(null);
|
|
36560
|
-
if (!frozen) {
|
|
36561
|
-
shownBranchRef.current = activeBranch;
|
|
36562
|
-
}
|
|
36563
|
-
const branch = shownBranchRef.current || activeBranch;
|
|
36619
|
+
const branch = activeBranch;
|
|
36564
36620
|
// What this container would show if it rendered now, by the same rule as
|
|
36565
36621
|
// the render above — asked from outside any render (see isShown below).
|
|
36566
36622
|
const childrenRef = useRef(null);
|
|
@@ -36568,9 +36624,6 @@ const RouteContainer = ({
|
|
|
36568
36624
|
const readShownBranchRef = useRef(null);
|
|
36569
36625
|
if (!readShownBranchRef.current) {
|
|
36570
36626
|
readShownBranchRef.current = () => {
|
|
36571
|
-
if (routeRenderFrozen) {
|
|
36572
|
-
return shownBranchRef.current;
|
|
36573
|
-
}
|
|
36574
36627
|
return collectBranches(childrenRef.current).activeBranch;
|
|
36575
36628
|
};
|
|
36576
36629
|
}
|
|
@@ -36865,7 +36918,7 @@ const branchRouteNode = branch => {
|
|
|
36865
36918
|
// Walk JSX children vnodes (without rendering) to build a branch list and
|
|
36866
36919
|
// find the active one in the same pass. Anything that is not a <Route> is read
|
|
36867
36920
|
// through and kept around the branch it holds (see below).
|
|
36868
|
-
// Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
|
|
36921
|
+
// Returns { matchingBranch, fallbackBranch, activeBranch, pages, leaves }.
|
|
36869
36922
|
//
|
|
36870
36923
|
// Each branch is given an id from where it is written among the children,
|
|
36871
36924
|
// which is what tells a container it changed page: the vnodes themselves are
|
|
@@ -36880,6 +36933,8 @@ const collectBranches = (children, path = "") => {
|
|
|
36880
36933
|
// matching or a leaf matching under it is enough to select it, and the
|
|
36881
36934
|
// fallback then stands down.
|
|
36882
36935
|
const pages = [];
|
|
36936
|
+
// Every leaf, wrappers kept, whichever matches: what a row has a pane for.
|
|
36937
|
+
const leaves = [];
|
|
36883
36938
|
const visit = child => {
|
|
36884
36939
|
if (!child || child === true || child === false) {
|
|
36885
36940
|
return;
|
|
@@ -36909,11 +36964,15 @@ const collectBranches = (children, path = "") => {
|
|
|
36909
36964
|
const {
|
|
36910
36965
|
matchingBranch: matchingInside,
|
|
36911
36966
|
fallbackBranch: fallbackInside,
|
|
36912
|
-
pages: pagesInside
|
|
36967
|
+
pages: pagesInside,
|
|
36968
|
+
leaves: leavesInside
|
|
36913
36969
|
} = collectBranches(wrapperChildren, `${id}.`);
|
|
36914
36970
|
for (const pageInside of pagesInside) {
|
|
36915
36971
|
pages.push(pageInside);
|
|
36916
36972
|
}
|
|
36973
|
+
for (const leafInside of leavesInside) {
|
|
36974
|
+
leaves.push(wrapBranch(leafInside, child));
|
|
36975
|
+
}
|
|
36917
36976
|
if (matchingInside && !matchingBranch) {
|
|
36918
36977
|
matchingBranch = wrapBranch(matchingInside, child);
|
|
36919
36978
|
}
|
|
@@ -36986,6 +37045,7 @@ const collectBranches = (children, path = "") => {
|
|
|
36986
37045
|
id,
|
|
36987
37046
|
node: child
|
|
36988
37047
|
};
|
|
37048
|
+
leaves.push(branch);
|
|
36989
37049
|
pages.push({
|
|
36990
37050
|
route,
|
|
36991
37051
|
params: routeParams
|
|
@@ -37014,7 +37074,8 @@ const collectBranches = (children, path = "") => {
|
|
|
37014
37074
|
matchingBranch,
|
|
37015
37075
|
fallbackBranch,
|
|
37016
37076
|
activeBranch,
|
|
37017
|
-
pages
|
|
37077
|
+
pages,
|
|
37078
|
+
leaves
|
|
37018
37079
|
};
|
|
37019
37080
|
};
|
|
37020
37081
|
const describeContainer = ({
|
|
@@ -37184,14 +37245,7 @@ const releaseTransitionDestination = (owner) => {
|
|
|
37184
37245
|
*
|
|
37185
37246
|
* A control therefore may ask to keep answering, and while a route transition
|
|
37186
37247
|
* plays the press is caught at the document and handed to it when it fell
|
|
37187
|
-
* inside its rectangle — which is where the hand thinks it pressed.
|
|
37188
|
-
* move route_travel.jsx makes for the box a finger reaches for mid-flight.
|
|
37189
|
-
*
|
|
37190
|
-
* A `RouteTravel` holds this too, for the bars it leaves live. A control there
|
|
37191
|
-
* gets its click on its own except in the first frames of the travel, when a
|
|
37192
|
-
* press falls to the document root like any other; the part of a bar photographed
|
|
37193
|
-
* for the travel (`data-navi-fixed-bar-overhang`, fixed_bar.jsx) is deaf
|
|
37194
|
-
* throughout.
|
|
37248
|
+
* inside its rectangle — which is where the hand thinks it pressed.
|
|
37195
37249
|
*
|
|
37196
37250
|
* ASKED FOR, never derived, and that is the whole safety of it: only the
|
|
37197
37251
|
* application knows the control does not move. One that travels with the pages
|
|
@@ -38078,7 +38132,7 @@ const holdTransitionWindow = (
|
|
|
38078
38132
|
) => {
|
|
38079
38133
|
const bandBefore = stateBefore.band;
|
|
38080
38134
|
const rectAfter = element.getBoundingClientRect();
|
|
38081
|
-
|
|
38135
|
+
readBand();
|
|
38082
38136
|
const outerScroll = readOuterScroll(element);
|
|
38083
38137
|
// The state being left, at where its box stands under the offset the box
|
|
38084
38138
|
// is measured under NOW: what scrolled outside the box between the two
|
|
@@ -38112,9 +38166,7 @@ const holdTransitionWindow = (
|
|
|
38112
38166
|
setTransitionValue(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
|
|
38113
38167
|
setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
|
|
38114
38168
|
setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
|
|
38115
|
-
const oldBand =
|
|
38116
|
-
? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
|
|
38117
|
-
: bandBefore;
|
|
38169
|
+
const oldBand = bandBefore;
|
|
38118
38170
|
setTransitionValue(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
|
|
38119
38171
|
setTransitionValue(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
|
|
38120
38172
|
setTransitionValue(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
|
|
@@ -38155,21 +38207,6 @@ const placeTransitionWindow = (owner, groupName) => {
|
|
|
38155
38207
|
document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
|
|
38156
38208
|
};
|
|
38157
38209
|
|
|
38158
|
-
/**
|
|
38159
|
-
* Whether a point of the window lies in a band the furniture keeps: the pictures
|
|
38160
|
-
* are cut at those bands, so what is seen there is never a page, however far the
|
|
38161
|
-
* box runs under it.
|
|
38162
|
-
*/
|
|
38163
|
-
const isPointInTransitionBand = (clientX, clientY) => {
|
|
38164
|
-
const band = readBand();
|
|
38165
|
-
return (
|
|
38166
|
-
clientX < band.left ||
|
|
38167
|
-
clientX > window.innerWidth - band.right ||
|
|
38168
|
-
clientY < band.top ||
|
|
38169
|
-
clientY > window.innerHeight - band.bottom
|
|
38170
|
-
);
|
|
38171
|
-
};
|
|
38172
|
-
|
|
38173
38210
|
// The live layout takes the box back. A discontinuity by construction — the
|
|
38174
38211
|
// window stands at the held height, the box is at its own — and an invisible
|
|
38175
38212
|
// one: the page arriving is fully in place, and the strip below it that the
|
|
@@ -38252,8 +38289,7 @@ const shiftRect = (rect, byTop, byLeft) => {
|
|
|
38252
38289
|
// What has scrolled OUTSIDE the box: the offsets of every scroller above the
|
|
38253
38290
|
// one the pages scroll in, the viewport's included. That nearest scroller is
|
|
38254
38291
|
// the pages' own — an offset there is a page scrolled, and it is not counted;
|
|
38255
|
-
// a hidden overflow counts as one
|
|
38256
|
-
// travel asks about (see pagesScrollTheDocument in route_travel.jsx). What is
|
|
38292
|
+
// a hidden overflow counts as one. What is
|
|
38257
38293
|
// pinned to the viewport (fixed, in the top layer) is not carried by the
|
|
38258
38294
|
// document's offset, and the walk stops under it.
|
|
38259
38295
|
const readOuterScroll = (element) => {
|
|
@@ -38321,40 +38357,6 @@ const readBand = () => {
|
|
|
38321
38357
|
};
|
|
38322
38358
|
};
|
|
38323
38359
|
|
|
38324
|
-
// How far the cut may be opened for the picture being left when the furniture
|
|
38325
|
-
// is LIVE under the pictures — the band its own state kept free, which is the
|
|
38326
|
-
// whole point of photographing it: furniture present in only one of the two
|
|
38327
|
-
// states is not the frame, it is part of what changes, and cutting the page
|
|
38328
|
-
// being left at a bar it never had shows its header being sliced instead of
|
|
38329
|
-
// leaving.
|
|
38330
|
-
//
|
|
38331
|
-
// Never past what the picture ARRIVING needs, though: the cut is one line for
|
|
38332
|
-
// both pictures, so opening it for one opens it for the other. A box that runs
|
|
38333
|
-
// under the live furniture it arrives beside — a page scrolled below a top bar
|
|
38334
|
-
// — would then be watched painting over that bar for the length of the
|
|
38335
|
-
// movement, which is what the cut exists to prevent. On such an edge the
|
|
38336
|
-
// arriving band stands, and the page being left is cut as it always was.
|
|
38337
|
-
//
|
|
38338
|
-
// With the furniture photographed (a route transition) none of this holds: a
|
|
38339
|
-
// bar only the arriving state has is a picture ordered over the page it
|
|
38340
|
-
// belongs to, and a page arriving under it paints nothing over it. The band
|
|
38341
|
-
// of the state being left is then published as it was, so the page being
|
|
38342
|
-
// left is never cut at a bar it never had.
|
|
38343
|
-
const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
|
|
38344
|
-
return {
|
|
38345
|
-
top: rectAfter.top < bandAfter.top ? bandAfter.top : bandBefore.top,
|
|
38346
|
-
right:
|
|
38347
|
-
rectAfter.right > window.innerWidth - bandAfter.right
|
|
38348
|
-
? bandAfter.right
|
|
38349
|
-
: bandBefore.right,
|
|
38350
|
-
bottom:
|
|
38351
|
-
rectAfter.bottom > window.innerHeight - bandAfter.bottom
|
|
38352
|
-
? bandAfter.bottom
|
|
38353
|
-
: bandBefore.bottom,
|
|
38354
|
-
left: rectAfter.left < bandAfter.left ? bandAfter.left : bandBefore.left,
|
|
38355
|
-
};
|
|
38356
|
-
};
|
|
38357
|
-
|
|
38358
38360
|
/**
|
|
38359
38361
|
* A view transition already playing, walked back to where it started.
|
|
38360
38362
|
*
|
|
@@ -38530,7 +38532,7 @@ const easedProgress = (x, [x1, y1, x2, y2]) => {
|
|
|
38530
38532
|
};
|
|
38531
38533
|
|
|
38532
38534
|
installImportMetaCssBuild(import.meta);
|
|
38533
|
-
const startViewTransition
|
|
38535
|
+
const startViewTransition = ensureDocumentStartViewTransition();
|
|
38534
38536
|
const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
|
|
38535
38537
|
const TRANSITION_TYPE_ATTRIBUTE = "data-navi-route-transition-type";
|
|
38536
38538
|
const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
|
|
@@ -38562,12 +38564,6 @@ const AREA_NAME = "navi-route-transition";
|
|
|
38562
38564
|
// the pages', or the document's own when the pages ARE the document (see
|
|
38563
38565
|
// findLeadAnimation).
|
|
38564
38566
|
const LEAD_NAMES = [AREA_NAME, "root"];
|
|
38565
|
-
// route_travel.jsx wears this on the root for the length of one of its
|
|
38566
|
-
// travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
|
|
38567
|
-
// name rather than imported: importing route_travel.jsx would pull the whole
|
|
38568
|
-
// travel machinery into an application that only defines transitions.
|
|
38569
|
-
const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
38570
|
-
|
|
38571
38567
|
// The same movements, written once and played on either target: the document,
|
|
38572
38568
|
// or the marked area. The guard keeps the two exclusive — with an area marked,
|
|
38573
38569
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
@@ -39379,10 +39375,10 @@ const rebuildWatcher = () => {
|
|
|
39379
39375
|
from,
|
|
39380
39376
|
to
|
|
39381
39377
|
} of relations) {
|
|
39382
|
-
if (from && pageIndexOf
|
|
39378
|
+
if (from && pageIndexOf(pages, from) === -1) {
|
|
39383
39379
|
pages.push(from);
|
|
39384
39380
|
}
|
|
39385
|
-
if (pageIndexOf
|
|
39381
|
+
if (pageIndexOf(pages, to) === -1) {
|
|
39386
39382
|
pages.push(to);
|
|
39387
39383
|
}
|
|
39388
39384
|
}
|
|
@@ -39470,7 +39466,7 @@ const rebuildWatcher = () => {
|
|
|
39470
39466
|
// What plays when no relation matched (see defineRouteDefaultTransition), what
|
|
39471
39467
|
// the navigation now landing asked for on its own (see readNavigationRequest),
|
|
39472
39468
|
// where it goes, and whether it found an answer already — a relation's
|
|
39473
|
-
// transition, a "none"
|
|
39469
|
+
// transition, a "none". The last three are read at the
|
|
39474
39470
|
// start of every navigation, so they are always about the latest one.
|
|
39475
39471
|
let defaultTransition = null;
|
|
39476
39472
|
let navigationRequest = null;
|
|
@@ -39721,14 +39717,6 @@ const beginTransition = ({
|
|
|
39721
39717
|
}) => {
|
|
39722
39718
|
navigationAnimated = true;
|
|
39723
39719
|
const documentElement = document.documentElement;
|
|
39724
|
-
// One navigation, one animator. A RouteTravel box already travelling this
|
|
39725
|
-
// change owns the document's transition — and possibly a finger; starting
|
|
39726
|
-
// one here on top would skip its pictures mid-slide. A pair of routes must
|
|
39727
|
-
// be animated by RouteTravel or by a route transition, never both.
|
|
39728
|
-
if (documentElement.hasAttribute(ROUTE_TRAVEL_ATTRIBUTE)) {
|
|
39729
|
-
console.warn("A RouteTravel is animating this navigation; the route transition defined between these routes is skipped. Animate a pair of routes with RouteTravel or defineRouteTransition, not both.");
|
|
39730
|
-
return;
|
|
39731
|
-
}
|
|
39732
39720
|
navigationDecision = {
|
|
39733
39721
|
type,
|
|
39734
39722
|
direction,
|
|
@@ -39813,7 +39801,7 @@ const beginTransition = ({
|
|
|
39813
39801
|
// calls that callback a frame later, and a navigation that has already
|
|
39814
39802
|
// been decided renders its page in between — a wait armed then waits for
|
|
39815
39803
|
// something that has already happened.
|
|
39816
|
-
const renderWait = armRouteRenderWait
|
|
39804
|
+
const renderWait = armRouteRenderWait();
|
|
39817
39805
|
// What the browser ACTUALLY captured, read once the pictures exist: it is
|
|
39818
39806
|
// the only place the silent misconfigurations show. They are all about the
|
|
39819
39807
|
// same thing — a movement playing on pictures that are not the pages.
|
|
@@ -39842,7 +39830,7 @@ const beginTransition = ({
|
|
|
39842
39830
|
break;
|
|
39843
39831
|
}
|
|
39844
39832
|
};
|
|
39845
|
-
const viewTransition = startViewTransition
|
|
39833
|
+
const viewTransition = startViewTransition(async () => {
|
|
39846
39834
|
// The picture the browser is about to take must be of the page that was
|
|
39847
39835
|
// asked for, and a route matching is not yet a page rendered. Whatever
|
|
39848
39836
|
// is awaited here must be able to resolve without a frame: the document
|
|
@@ -39922,7 +39910,7 @@ const beginTransition = ({
|
|
|
39922
39910
|
}
|
|
39923
39911
|
};
|
|
39924
39912
|
transition.viewTransition = viewTransition;
|
|
39925
|
-
viewTransition.ready.then(viewTransitionReady, ignoreSkipped
|
|
39913
|
+
viewTransition.ready.then(viewTransitionReady, ignoreSkipped);
|
|
39926
39914
|
viewTransition.finished.then(end, end);
|
|
39927
39915
|
};
|
|
39928
39916
|
|
|
@@ -40030,7 +40018,7 @@ const turnRunningTransitionRound = (fromUrl, url) => {
|
|
|
40030
40018
|
const absoluteUrl = url => url ? new URL(url, window.location.href).href : null;
|
|
40031
40019
|
|
|
40032
40020
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
40033
|
-
const ignoreSkipped
|
|
40021
|
+
const ignoreSkipped = () => {};
|
|
40034
40022
|
|
|
40035
40023
|
// The names the browser captured, read off the pictures themselves: what was
|
|
40036
40024
|
// asked for in CSS and what was taken are not the same question (see
|
|
@@ -40099,7 +40087,7 @@ const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
|
|
|
40099
40087
|
continue;
|
|
40100
40088
|
}
|
|
40101
40089
|
const unclipped = NESTED_GROUPS_SUPPORTED ? "" : " This browser has no nested groups (view-transition-group: contain), so it is not even cut at the pages' edge.";
|
|
40102
|
-
warnOnce("names-escaping-area", `"${viewTransitionName}" is a view-transition-name written inside the element marked ${TRANSITION_AREA_ATTRIBUTE}, and the page on the other side of the navigation has nothing of that name. A name lifts its element out of the pages' pictures, in every browser: with nothing to morph into, it stands where it was captured and fades on its own while the pages move.${unclipped} Give that name only for the length of the gesture it serves, or drop it while the pages move: :root
|
|
40090
|
+
warnOnce("names-escaping-area", `"${viewTransitionName}" is a view-transition-name written inside the element marked ${TRANSITION_AREA_ATTRIBUTE}, and the page on the other side of the navigation has nothing of that name. A name lifts its element out of the pages' pictures, in every browser: with nothing to morph into, it stands where it was captured and fades on its own while the pages move.${unclipped} Give that name only for the length of the gesture it serves, or drop it while the pages move: :root[${TRANSITION_ATTRIBUTE}] <that element> { view-transition-name: none } (with !important over an inline name).`);
|
|
40103
40091
|
return;
|
|
40104
40092
|
}
|
|
40105
40093
|
};
|
|
@@ -40120,7 +40108,7 @@ const warnOnce = (id, message) => {
|
|
|
40120
40108
|
// answer it is its own business, and the render is the moment the picture can
|
|
40121
40109
|
// be taken. Listening starts before the change, or a render landing while the
|
|
40122
40110
|
// change settles is a render nobody heard.
|
|
40123
|
-
const armRouteRenderWait
|
|
40111
|
+
const armRouteRenderWait = () => {
|
|
40124
40112
|
let stopListening;
|
|
40125
40113
|
const rendered = new Promise(resolve => {
|
|
40126
40114
|
stopListening = observeRouteRender(resolve);
|
|
@@ -40160,7 +40148,7 @@ const samePage$1 = (a, b) => {
|
|
|
40160
40148
|
}
|
|
40161
40149
|
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
40162
40150
|
};
|
|
40163
|
-
const pageIndexOf
|
|
40151
|
+
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage$1(candidate, page));
|
|
40164
40152
|
|
|
40165
40153
|
// The FIRST page that answers, and every page read all the same: a page that
|
|
40166
40154
|
// is not the current one today is the one that must wake the reader tomorrow.
|
|
@@ -40243,1451 +40231,720 @@ const installRouteTransitionCss = () => {
|
|
|
40243
40231
|
installTransitionFurnitureCss();
|
|
40244
40232
|
};
|
|
40245
40233
|
|
|
40246
|
-
|
|
40247
|
-
|
|
40248
|
-
|
|
40249
|
-
|
|
40250
|
-
|
|
40251
|
-
|
|
40252
|
-
|
|
40253
|
-
|
|
40254
|
-
|
|
40255
|
-
const
|
|
40256
|
-
const startViewTransition = ensureDocumentStartViewTransition();
|
|
40257
|
-
|
|
40258
|
-
// route_transition.js reads this attribute by name (not imported, see its
|
|
40259
|
-
// ROUTE_TRAVEL_ATTRIBUTE) to refuse animating a navigation a travel already
|
|
40260
|
-
// animates — keep the two in sync.
|
|
40261
|
-
const TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
40262
|
-
// Which way the pages move, said on the document: the pictures of a transition
|
|
40263
|
-
// hang off the root, not off the box that travels, so the box's own `axis` has
|
|
40264
|
-
// to be lent to the document for the length of the travel.
|
|
40265
|
-
const TRAVEL_AXIS_ATTRIBUTE = "data-navi-route-travel-axis";
|
|
40266
|
-
// While a finger holds the travel: the pictures stand still and go exactly
|
|
40267
|
-
// where it says (see the CSS, and scrubTravel).
|
|
40268
|
-
const HOLD_ATTRIBUTE = "data-navi-route-travel-held";
|
|
40269
|
-
// A travel a finger set off, for its whole life — including what plays out
|
|
40270
|
-
// after the finger is gone. It moves by another law than one asked for by a
|
|
40271
|
-
// press (see the CSS).
|
|
40272
|
-
const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
40273
|
-
// A travel that changed its mind about where it was going (see
|
|
40274
|
-
// redirectTravel). Only the pages can be aimed somewhere else: everything the
|
|
40275
|
-
// transition carries was measured once, at the start, against a destination
|
|
40276
|
-
// this travel is no longer going to.
|
|
40277
|
-
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
40278
|
-
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
40279
|
-
const TRAVEL_NAME = "navi-route-travel";
|
|
40280
|
-
const css$16 = /* css */`.navi_route_travel {
|
|
40281
|
-
position: relative;
|
|
40234
|
+
/**
|
|
40235
|
+
* Where a row of routes scrolled by the browser stands, for what is drawn about
|
|
40236
|
+
* it elsewhere: the bar under the tabs, in a <Nav> that sits outside the box and
|
|
40237
|
+
* knows nothing of it. A tab says which page it is through its address, so a
|
|
40238
|
+
* nav follows any row it holds tabs for, with nothing to wire.
|
|
40239
|
+
*
|
|
40240
|
+
* Said per scroll and not through a signal: a number written on one element is
|
|
40241
|
+
* all it takes, and a signal would make that a render.
|
|
40242
|
+
*/
|
|
40243
|
+
const followers = new Set();
|
|
40282
40244
|
|
|
40283
|
-
|
|
40284
|
-
|
|
40285
|
-
|
|
40245
|
+
const followRouteRows = (onMove) => {
|
|
40246
|
+
followers.add(onMove);
|
|
40247
|
+
return () => {
|
|
40248
|
+
followers.delete(onMove);
|
|
40249
|
+
};
|
|
40250
|
+
};
|
|
40286
40251
|
|
|
40287
|
-
|
|
40288
|
-
|
|
40252
|
+
/**
|
|
40253
|
+
* @param {{basePath: string, towardPath: string|null, progress: number}|null} move
|
|
40254
|
+
* - the two pages the row stands between, by the pathname of their address,
|
|
40255
|
+
* and how far past the first in the terms of --slide-travel-progress. `null`
|
|
40256
|
+
* once it has stopped.
|
|
40257
|
+
*/
|
|
40258
|
+
const publishRouteRowMove = (move) => {
|
|
40259
|
+
for (const follower of followers) {
|
|
40260
|
+
follower(move);
|
|
40289
40261
|
}
|
|
40290
|
-
}
|
|
40291
|
-
|
|
40292
|
-
:root[data-navi-route-travel] {
|
|
40293
|
-
view-transition-name: none;
|
|
40262
|
+
};
|
|
40294
40263
|
|
|
40295
|
-
|
|
40296
|
-
|
|
40297
|
-
|
|
40264
|
+
/**
|
|
40265
|
+
* A row of pages the browser scrolls itself, and what is left to read from it.
|
|
40266
|
+
*
|
|
40267
|
+
* The hand is not interpreted here: the browser follows it, on its compositor,
|
|
40268
|
+
* and it alone knows what the page cannot be told (a notch from a trackpad, the
|
|
40269
|
+
* end of a stroke from its momentum, which scroller a stream is latched on). What
|
|
40270
|
+
* remains is reading where the row stands and when it has come to rest.
|
|
40271
|
+
*
|
|
40272
|
+
* Rows are measured off their pages rather than by rank and width: a row that
|
|
40273
|
+
* ends early because a page is held leaves what lies beyond out (`data-off-row`),
|
|
40274
|
+
* and ranks would then lie about where the others are. An `offsetLeft` is rounded,
|
|
40275
|
+
* but by half a pixel at most whatever the rank, which a rank multiplied by a
|
|
40276
|
+
* rounded width is not. It also ignores a transform: a row inside a popup still
|
|
40277
|
+
* growing into place measures as it will stand.
|
|
40278
|
+
*
|
|
40279
|
+
* Each scroller must be the offsetParent of its pages.
|
|
40280
|
+
*/
|
|
40298
40281
|
|
|
40299
|
-
|
|
40300
|
-
|
|
40301
|
-
|
|
40302
|
-
|
|
40303
|
-
|
|
40304
|
-
|
|
40282
|
+
// What the rest of a scroll is worth while a page sits a rounding away from it.
|
|
40283
|
+
const AT_REST_DISTANCE = 1;
|
|
40284
|
+
// How long without a scroll event means the row stopped. The browser's word is
|
|
40285
|
+
// not enough: Firefox says `scrollend` once, when the wheel goes quiet, some 200ms
|
|
40286
|
+
// before its snap has finished and never again, and Safari before 26.2 does not
|
|
40287
|
+
// say it at all.
|
|
40288
|
+
const STOPPED_SILENCE = 150;
|
|
40305
40289
|
|
|
40306
|
-
|
|
40307
|
-
|
|
40308
|
-
left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
|
|
40309
|
-
width: auto;
|
|
40310
|
-
}
|
|
40290
|
+
const isOnRow = (page) =>
|
|
40291
|
+
page.offsetWidth > 0 && !page.hasAttribute("data-off-row");
|
|
40311
40292
|
|
|
40312
|
-
|
|
40313
|
-
|
|
40314
|
-
|
|
40315
|
-
width: auto;
|
|
40316
|
-
}
|
|
40293
|
+
const restsOn = (scroller, page) =>
|
|
40294
|
+
Math.abs(page.offsetLeft - scroller.scrollLeft) <= AT_REST_DISTANCE &&
|
|
40295
|
+
Math.abs(page.offsetTop - scroller.scrollTop) <= AT_REST_DISTANCE;
|
|
40317
40296
|
|
|
40318
|
-
|
|
40319
|
-
|
|
40320
|
-
|
|
40297
|
+
/**
|
|
40298
|
+
* @returns {Element|undefined} the page the row rests on, nothing while it stands
|
|
40299
|
+
* between two: a hand can pause midway, and an engine may call that the end of
|
|
40300
|
+
* a scroll.
|
|
40301
|
+
*/
|
|
40302
|
+
const pageAtRest = (scroller, pages) =>
|
|
40303
|
+
pages.find((page) => isOnRow(page) && restsOn(scroller, page));
|
|
40321
40304
|
|
|
40322
|
-
|
|
40323
|
-
|
|
40324
|
-
|
|
40325
|
-
|
|
40326
|
-
|
|
40327
|
-
|
|
40328
|
-
|
|
40329
|
-
|
|
40330
|
-
|
|
40331
|
-
|
|
40332
|
-
|
|
40333
|
-
|
|
40334
|
-
|
|
40335
|
-
|
|
40336
|
-
|
|
40337
|
-
|
|
40338
|
-
|
|
40339
|
-
|
|
40340
|
-
|
|
40341
|
-
|
|
40342
|
-
|
|
40343
|
-
|
|
40344
|
-
|
|
40345
|
-
|
|
40346
|
-
|
|
40347
|
-
|
|
40348
|
-
|
|
40349
|
-
|
|
40350
|
-
|
|
40351
|
-
|
|
40305
|
+
/**
|
|
40306
|
+
* Where the row stands between two neighbouring pages, on the side it has moved
|
|
40307
|
+
* to from `page`: a hand is not limited to the next one, so what it has passed
|
|
40308
|
+
* since is walked over. A map moves along one axis at a time, the one it has
|
|
40309
|
+
* moved furthest on.
|
|
40310
|
+
*
|
|
40311
|
+
* @returns {{base: Element, toward: Element|undefined, fraction: number, forward: boolean}}
|
|
40312
|
+
* the last page the row has passed (`page` itself until then), the one after,
|
|
40313
|
+
* and how far between the two from 0 to 1. Nothing `toward` past the last.
|
|
40314
|
+
*/
|
|
40315
|
+
const readRowStretch = (scroller, pages, page) => {
|
|
40316
|
+
const alongX =
|
|
40317
|
+
Math.abs(scroller.scrollLeft - page.offsetLeft) >=
|
|
40318
|
+
Math.abs(scroller.scrollTop - page.offsetTop);
|
|
40319
|
+
const position = alongX ? "offsetLeft" : "offsetTop";
|
|
40320
|
+
const cross = alongX ? "offsetTop" : "offsetLeft";
|
|
40321
|
+
const scrolled = alongX ? scroller.scrollLeft : scroller.scrollTop;
|
|
40322
|
+
const forward = scrolled > page[position];
|
|
40323
|
+
const step = forward ? 1 : -1;
|
|
40324
|
+
const line = pages
|
|
40325
|
+
.filter(
|
|
40326
|
+
(candidate) => isOnRow(candidate) && candidate[cross] === page[cross],
|
|
40327
|
+
)
|
|
40328
|
+
.sort((pageA, pageB) => pageA[position] - pageB[position]);
|
|
40329
|
+
let index = line.indexOf(page);
|
|
40330
|
+
let base = page;
|
|
40331
|
+
let toward = line[index + step];
|
|
40332
|
+
while (
|
|
40333
|
+
toward &&
|
|
40334
|
+
(forward ? toward[position] <= scrolled : toward[position] >= scrolled)
|
|
40335
|
+
) {
|
|
40336
|
+
index += step;
|
|
40337
|
+
base = toward;
|
|
40338
|
+
toward = line[index + step];
|
|
40352
40339
|
}
|
|
40340
|
+
const span = toward ? Math.abs(toward[position] - base[position]) : 0;
|
|
40341
|
+
const fraction = span ? Math.abs(scrolled - base[position]) / span : 0;
|
|
40342
|
+
return { base, toward, fraction, forward };
|
|
40343
|
+
};
|
|
40353
40344
|
|
|
40354
|
-
|
|
40355
|
-
|
|
40356
|
-
|
|
40357
|
-
|
|
40358
|
-
|
|
40359
|
-
|
|
40360
|
-
|
|
40345
|
+
/**
|
|
40346
|
+
* @param {Element} scroller
|
|
40347
|
+
* @param {object} options
|
|
40348
|
+
* @param {() => void} options.onScroll - the row moved.
|
|
40349
|
+
* @param {(event: Event|null) => void} options.onLand - the row may have stopped.
|
|
40350
|
+
* Read `pageAtRest` to know whether it stopped on a page. Heard more than once
|
|
40351
|
+
* for one landing, so what it does must find nothing to do the second time.
|
|
40352
|
+
* @returns {() => void} stop listening.
|
|
40353
|
+
*/
|
|
40354
|
+
const watchRowScroll = (scroller, { onScroll, onLand }) => {
|
|
40355
|
+
let stoppedTimeout = null;
|
|
40356
|
+
let touching = false;
|
|
40357
|
+
const landOnSilence = () => {
|
|
40358
|
+
// A finger holding the row still: nothing has stopped.
|
|
40359
|
+
if (!touching) {
|
|
40360
|
+
onLand(null);
|
|
40361
|
+
}
|
|
40362
|
+
};
|
|
40363
|
+
const armSilence = () => {
|
|
40364
|
+
clearTimeout(stoppedTimeout);
|
|
40365
|
+
stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
|
|
40366
|
+
};
|
|
40367
|
+
const onScrollEvent = () => {
|
|
40368
|
+
onScroll();
|
|
40369
|
+
armSilence();
|
|
40370
|
+
};
|
|
40371
|
+
const onTouchStart = () => {
|
|
40372
|
+
touching = true;
|
|
40373
|
+
clearTimeout(stoppedTimeout);
|
|
40374
|
+
};
|
|
40375
|
+
const onTouchEnd = (touchEvent) => {
|
|
40376
|
+
touching = touchEvent.touches.length > 0;
|
|
40377
|
+
if (!touching) {
|
|
40378
|
+
armSilence();
|
|
40379
|
+
}
|
|
40380
|
+
};
|
|
40381
|
+
scroller.addEventListener("scroll", onScrollEvent, { passive: true });
|
|
40382
|
+
scroller.addEventListener("scrollend", onLand);
|
|
40383
|
+
scroller.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
40384
|
+
scroller.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
40385
|
+
scroller.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
40386
|
+
return () => {
|
|
40387
|
+
clearTimeout(stoppedTimeout);
|
|
40388
|
+
scroller.removeEventListener("scroll", onScrollEvent);
|
|
40389
|
+
scroller.removeEventListener("scrollend", onLand);
|
|
40390
|
+
scroller.removeEventListener("touchstart", onTouchStart);
|
|
40391
|
+
scroller.removeEventListener("touchend", onTouchEnd);
|
|
40392
|
+
scroller.removeEventListener("touchcancel", onTouchEnd);
|
|
40393
|
+
};
|
|
40394
|
+
};
|
|
40361
40395
|
|
|
40362
|
-
|
|
40363
|
-
|
|
40364
|
-
|
|
40365
|
-
}
|
|
40396
|
+
installImportMetaCssBuild(import.meta);
|
|
40397
|
+
const css$16 = /* css */`.navi_route_travel {
|
|
40398
|
+
position: relative;
|
|
40366
40399
|
}
|
|
40367
40400
|
|
|
40368
|
-
|
|
40369
|
-
|
|
40370
|
-
|
|
40371
|
-
|
|
40372
|
-
|
|
40373
|
-
|
|
40374
|
-
|
|
40401
|
+
[data-route-travel-row] {
|
|
40402
|
+
overflow-anchor: none;
|
|
40403
|
+
overscroll-behavior-x: contain;
|
|
40404
|
+
scroll-snap-type: x mandatory;
|
|
40405
|
+
scrollbar-width: none;
|
|
40406
|
+
flex: auto;
|
|
40407
|
+
grid-template-rows: minmax(0, 1fr);
|
|
40408
|
+
grid-auto-columns: 100%;
|
|
40409
|
+
grid-auto-flow: column;
|
|
40410
|
+
min-width: 0;
|
|
40411
|
+
min-height: 0;
|
|
40412
|
+
display: grid;
|
|
40413
|
+
position: relative;
|
|
40414
|
+
overflow: auto hidden;
|
|
40375
40415
|
|
|
40376
|
-
|
|
40377
|
-
&::view-transition-group(*) {
|
|
40416
|
+
&::-webkit-scrollbar {
|
|
40378
40417
|
display: none;
|
|
40379
40418
|
}
|
|
40380
40419
|
|
|
40381
|
-
|
|
40382
|
-
|
|
40383
|
-
}
|
|
40384
|
-
}
|
|
40385
|
-
|
|
40386
|
-
:root[data-navi-route-travel-dragged] {
|
|
40387
|
-
&::view-transition-group(navi-route-travel), &::view-transition-old(navi-route-travel), &::view-transition-new(navi-route-travel) {
|
|
40388
|
-
animation-timing-function: linear;
|
|
40389
|
-
}
|
|
40390
|
-
}
|
|
40391
|
-
|
|
40392
|
-
:root[data-navi-route-travel="forward"] {
|
|
40393
|
-
&::view-transition-old(navi-route-travel) {
|
|
40394
|
-
animation-name: navi-route-travel-leave-towards-start;
|
|
40420
|
+
&[data-route-travel-row="code"] {
|
|
40421
|
+
overflow-x: hidden;
|
|
40395
40422
|
}
|
|
40396
40423
|
|
|
40397
|
-
|
|
40398
|
-
|
|
40399
|
-
}
|
|
40400
|
-
}
|
|
40401
|
-
|
|
40402
|
-
:root[data-navi-route-travel="back"] {
|
|
40403
|
-
&::view-transition-old(navi-route-travel) {
|
|
40404
|
-
animation-name: navi-route-travel-leave-towards-end;
|
|
40405
|
-
}
|
|
40406
|
-
|
|
40407
|
-
&::view-transition-new(navi-route-travel) {
|
|
40408
|
-
animation-name: navi-route-travel-enter-from-start;
|
|
40409
|
-
}
|
|
40410
|
-
}
|
|
40411
|
-
|
|
40412
|
-
:root[data-navi-route-travel-axis="y"] {
|
|
40413
|
-
&[data-navi-route-travel="forward"] {
|
|
40414
|
-
&::view-transition-old(navi-route-travel) {
|
|
40415
|
-
animation-name: navi-route-travel-leave-towards-top;
|
|
40416
|
-
}
|
|
40417
|
-
|
|
40418
|
-
&::view-transition-new(navi-route-travel) {
|
|
40419
|
-
animation-name: navi-route-travel-enter-from-bottom;
|
|
40420
|
-
}
|
|
40421
|
-
}
|
|
40422
|
-
|
|
40423
|
-
&[data-navi-route-travel="back"] {
|
|
40424
|
-
&::view-transition-old(navi-route-travel) {
|
|
40425
|
-
animation-name: navi-route-travel-leave-towards-bottom;
|
|
40426
|
-
}
|
|
40427
|
-
|
|
40428
|
-
&::view-transition-new(navi-route-travel) {
|
|
40429
|
-
animation-name: navi-route-travel-enter-from-top;
|
|
40430
|
-
}
|
|
40431
|
-
}
|
|
40432
|
-
}
|
|
40433
|
-
|
|
40434
|
-
@keyframes navi-route-travel-leave-towards-start {
|
|
40435
|
-
from {
|
|
40436
|
-
translate: 0;
|
|
40437
|
-
}
|
|
40438
|
-
|
|
40439
|
-
to {
|
|
40440
|
-
translate: -100%;
|
|
40441
|
-
}
|
|
40442
|
-
}
|
|
40443
|
-
|
|
40444
|
-
@keyframes navi-route-travel-enter-from-end {
|
|
40445
|
-
from {
|
|
40446
|
-
translate: 100%;
|
|
40447
|
-
}
|
|
40448
|
-
|
|
40449
|
-
to {
|
|
40450
|
-
translate: 0;
|
|
40451
|
-
}
|
|
40452
|
-
}
|
|
40453
|
-
|
|
40454
|
-
@keyframes navi-route-travel-leave-towards-end {
|
|
40455
|
-
from {
|
|
40456
|
-
translate: 0;
|
|
40457
|
-
}
|
|
40458
|
-
|
|
40459
|
-
to {
|
|
40460
|
-
translate: 100%;
|
|
40461
|
-
}
|
|
40462
|
-
}
|
|
40463
|
-
|
|
40464
|
-
@keyframes navi-route-travel-enter-from-start {
|
|
40465
|
-
from {
|
|
40466
|
-
translate: -100%;
|
|
40467
|
-
}
|
|
40468
|
-
|
|
40469
|
-
to {
|
|
40470
|
-
translate: 0;
|
|
40471
|
-
}
|
|
40472
|
-
}
|
|
40473
|
-
|
|
40474
|
-
@keyframes navi-route-travel-leave-towards-top {
|
|
40475
|
-
from {
|
|
40476
|
-
translate: 0;
|
|
40424
|
+
&[data-route-travel-row="still"] {
|
|
40425
|
+
overflow: visible;
|
|
40477
40426
|
}
|
|
40478
40427
|
|
|
40479
|
-
|
|
40480
|
-
|
|
40481
|
-
|
|
40482
|
-
|
|
40483
|
-
|
|
40484
|
-
|
|
40485
|
-
|
|
40486
|
-
|
|
40428
|
+
& > [data-route-row-pane] {
|
|
40429
|
+
scroll-snap-align: start;
|
|
40430
|
+
scroll-snap-stop: always;
|
|
40431
|
+
flex-direction: column;
|
|
40432
|
+
grid-row: 1;
|
|
40433
|
+
min-width: 0;
|
|
40434
|
+
min-height: 0;
|
|
40435
|
+
display: flex;
|
|
40487
40436
|
}
|
|
40488
40437
|
|
|
40489
|
-
|
|
40490
|
-
|
|
40438
|
+
&:not([data-moving]) > [data-route-row-pane][inert] {
|
|
40439
|
+
visibility: hidden;
|
|
40491
40440
|
}
|
|
40492
|
-
}
|
|
40493
40441
|
|
|
40494
|
-
|
|
40495
|
-
|
|
40496
|
-
|
|
40442
|
+
& > [data-route-row-pane][inert] {
|
|
40443
|
+
top: var(--navi-route-row-pin, 0);
|
|
40444
|
+
max-height: 100dvh;
|
|
40445
|
+
position: relative;
|
|
40446
|
+
overflow: clip;
|
|
40497
40447
|
}
|
|
40498
40448
|
|
|
40499
|
-
|
|
40500
|
-
|
|
40449
|
+
& > [data-route-row-live] {
|
|
40450
|
+
flex-direction: column;
|
|
40451
|
+
grid-row: 1;
|
|
40452
|
+
min-width: 0;
|
|
40453
|
+
min-height: 0;
|
|
40454
|
+
display: flex;
|
|
40455
|
+
position: relative;
|
|
40501
40456
|
}
|
|
40502
|
-
}
|
|
40503
40457
|
|
|
40504
|
-
|
|
40505
|
-
|
|
40506
|
-
translate: 0 -100%;
|
|
40458
|
+
& > [data-route-row-pictures] {
|
|
40459
|
+
display: contents;
|
|
40507
40460
|
}
|
|
40508
40461
|
|
|
40509
|
-
|
|
40510
|
-
|
|
40462
|
+
& > [data-route-row-pictures] > * {
|
|
40463
|
+
contain: layout paint;
|
|
40464
|
+
pointer-events: none;
|
|
40465
|
+
grid-row: 1;
|
|
40466
|
+
min-width: 0;
|
|
40511
40467
|
}
|
|
40512
40468
|
}
|
|
40513
40469
|
`;
|
|
40514
40470
|
|
|
40471
|
+
// How far a hand must have pulled before the address moves for a page that is not
|
|
40472
|
+
// there yet: past what a hand that only shook covers, so that a shake costs
|
|
40473
|
+
// neither a navigation nor the requests of a page.
|
|
40474
|
+
const SET_OFF_PULL = 16;
|
|
40475
|
+
|
|
40515
40476
|
/**
|
|
40516
|
-
*
|
|
40517
|
-
*
|
|
40477
|
+
* What the URL says while a hand holds the row is where it is heading, for a page
|
|
40478
|
+
* not built yet: it builds by the address moving. A counter, a title or a
|
|
40479
|
+
* breadcrumb around the box must not move for a pull that is let go of: it reads
|
|
40480
|
+
* this first. The page the reader is still on while that lasts, and `null` when
|
|
40481
|
+
* the row has come to rest, which is when `scrollsnapchange` fires and a pager's
|
|
40482
|
+
* `settledPage` changes. A travel no hand set off holds nothing: it goes where it
|
|
40483
|
+
* goes.
|
|
40518
40484
|
*
|
|
40519
|
-
*
|
|
40520
|
-
* the hand is heading. A counter, a title or a breadcrumb around the box must
|
|
40521
|
-
* not move for a pull that is let go of: it reads this first. It holds for as
|
|
40522
|
-
* long as the pictures move, whichever way they end up going, and is back to
|
|
40523
|
-
* `null` when they stop, which is when `scrollsnapchange` fires and a pager's
|
|
40524
|
-
* `settledPage` changes. A page walked whole under the hand has arrived too. A
|
|
40525
|
-
* travel no hand set off (a tab pressed, a wheel) holds nothing: it goes where
|
|
40526
|
-
* it goes.
|
|
40527
|
-
*
|
|
40528
|
-
* One for the document, as there is one view transition at a time. `route` says
|
|
40529
|
-
* which row, for a page holding several boxes.
|
|
40485
|
+
* One for the document. `route` says which row, for a page holding several boxes.
|
|
40530
40486
|
*/
|
|
40531
40487
|
const routeTravelSettledPageSignal = signal(null);
|
|
40532
|
-
// Which travel wrote it, the only one that may clear it: a travel ending after
|
|
40533
|
-
// another has taken over leaves that one's answer alone.
|
|
40534
|
-
let travelHoldingSettledPage = null;
|
|
40535
40488
|
|
|
40536
40489
|
/**
|
|
40537
40490
|
* @type {import("preact").FunctionComponent<{
|
|
40538
40491
|
* routes?: Array<object|{route: object, params?: object}>,
|
|
40539
|
-
* axis?: "x"|"y",
|
|
40540
40492
|
* travelByDrag?: boolean,
|
|
40541
40493
|
* onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
|
|
40542
40494
|
* }>}
|
|
40543
|
-
* @param {Array<object|{route: object, params?: object}>} [props.routes] -
|
|
40544
|
-
*
|
|
40545
|
-
*
|
|
40546
|
-
*
|
|
40547
|
-
*
|
|
40548
|
-
*
|
|
40549
|
-
*
|
|
40550
|
-
*
|
|
40551
|
-
*
|
|
40552
|
-
*
|
|
40553
|
-
*
|
|
40554
|
-
*
|
|
40555
|
-
*
|
|
40556
|
-
*
|
|
40557
|
-
*
|
|
40558
|
-
*
|
|
40559
|
-
*
|
|
40560
|
-
*
|
|
40561
|
-
*
|
|
40562
|
-
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
40563
|
-
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
40564
|
-
* page under six entries. A tab pressed goes the way its <Link> says —
|
|
40565
|
-
* `<Link replace>` for a row of tabs, so a press and a swipe towards the
|
|
40566
|
-
* same neighbour write the same history.
|
|
40567
|
-
*
|
|
40568
|
-
* Every other prop lands on the box itself (`id`, `data-testid`, `className`,
|
|
40569
|
-
* …), which is how one of these is named: a page can hold several — a row of
|
|
40570
|
-
* sections inside the box the whole application travels in — and the class they
|
|
40571
|
-
* share plus their axis are not enough to tell them apart from the outside.
|
|
40572
|
-
*
|
|
40573
|
-
* The pages are cut at the edge of this box while they travel, and at the app's
|
|
40574
|
-
* safe area the box runs under, which is written on the transition's own
|
|
40575
|
-
* pseudo-elements — no overflow of the document reaches pictures drawn in the
|
|
40576
|
-
* top layer. It needs nothing of the browser beyond view transitions
|
|
40577
|
-
* themselves: a browser without them navigates without the movement, and the
|
|
40578
|
-
* gesture applies its change on release instead of dragging a picture that does
|
|
40579
|
-
* not exist.
|
|
40580
|
-
*
|
|
40581
|
-
* While a travel plays, the rest of the page is NOT taken as a picture: the
|
|
40582
|
-
* root's name is dropped for that time (`view-transition-name: none`), so what
|
|
40583
|
-
* stands around the box — a tab row beside it — stays live and answers a press
|
|
40584
|
-
* mid-travel. The application's own rule for the root applies again the moment
|
|
40585
|
-
* the travel is over. What stands out of a `FixedBar` is the exception, when it
|
|
40586
|
-
* asks to stay over the pages (see `data-navi-fixed-bar-overhang` there).
|
|
40495
|
+
* @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
|
|
40496
|
+
* over something longer, in order. Read from the <Route> children by default,
|
|
40497
|
+
* in the order they are written. Given here, the children are one page rendered
|
|
40498
|
+
* once, whatever the address, and may be anything: use it when the pages are one
|
|
40499
|
+
* route with other params, or not children of this box. An entry is a route, or
|
|
40500
|
+
* `{ route, params }`.
|
|
40501
|
+
* @param {boolean} [props.travelByDrag=true] - whether a hand travels. Off where the
|
|
40502
|
+
* gesture belongs to the content; a tab pressed still does.
|
|
40503
|
+
* @param {(detail: {route: object, params: object|undefined, cause: "drag"}) => void|Promise<void>} [props.onTravel]
|
|
40504
|
+
* - how to go to a page a hand has brought in. The default REPLACES the current
|
|
40505
|
+
* history entry rather than pushing one: a swipe is how one browses a page, not a
|
|
40506
|
+
* place one aimed at, and three swipes back and forth must not bury the way out
|
|
40507
|
+
* of the page under six entries. A tab pressed goes the way its <Link> says —
|
|
40508
|
+
* `<Link replace>` for a row of tabs, so a press and a swipe towards the same
|
|
40509
|
+
* neighbour write the same history.
|
|
40510
|
+
*
|
|
40511
|
+
* Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
|
|
40512
|
+
* The box has to outlive every page of its row: around those <Route>s, or around
|
|
40513
|
+
* one <Route> whose params are the tabs, never inside one of the pages.
|
|
40587
40514
|
*/
|
|
40588
40515
|
const RouteTravel = ({
|
|
40589
40516
|
routes: routesProp,
|
|
40590
|
-
axis = "x",
|
|
40591
40517
|
travelByDrag = true,
|
|
40592
40518
|
onTravel = ({
|
|
40593
40519
|
route,
|
|
40594
40520
|
params
|
|
40595
|
-
}) => route.
|
|
40521
|
+
}) => route.navTo(params, {
|
|
40522
|
+
replace: true,
|
|
40523
|
+
routeTransition: "none"
|
|
40524
|
+
}),
|
|
40596
40525
|
className,
|
|
40597
40526
|
children,
|
|
40598
40527
|
...rest
|
|
40599
40528
|
}) => {
|
|
40600
40529
|
import.meta.css = [css$16, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
40601
|
-
|
|
40602
|
-
const
|
|
40603
|
-
const
|
|
40604
|
-
|
|
40605
|
-
|
|
40606
|
-
// browser has them ready. Null when no page is on its way anywhere.
|
|
40607
|
-
const travelRef = useRef(null);
|
|
40608
|
-
// The page this box has ASKED for and is still waiting to see arrive.
|
|
40609
|
-
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
40610
|
-
// travel decided anything about it — sometimes after the travel was undone —
|
|
40611
|
-
// and read back as "the page changed" it would start a second travel nobody
|
|
40612
|
-
// asked for, over pictures that are already showing something else.
|
|
40613
|
-
const pageAskedForRef = useRef(null);
|
|
40614
|
-
// The latest way to answer a gesture, for a watcher that outlives every
|
|
40615
|
-
// render (see the wheel effect below).
|
|
40616
|
-
const travelHandlersRef = useRef(null);
|
|
40617
|
-
const pointerDownRef = useRef(null);
|
|
40530
|
+
const rowRef = useRef();
|
|
40531
|
+
const liveRef = useRef();
|
|
40532
|
+
const picturesRef = useRef();
|
|
40533
|
+
const windowed = Boolean(routesProp);
|
|
40534
|
+
const [, render] = useReducer(count => count + 1, 0);
|
|
40618
40535
|
const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
|
|
40619
40536
|
const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
|
|
40620
|
-
|
|
40621
|
-
|
|
40622
|
-
|
|
40623
|
-
|
|
40624
|
-
|
|
40625
|
-
|
|
40537
|
+
// Held while the row moves: a window recentred under a hand would move the very
|
|
40538
|
+
// panes it is pushing. It is taken as soon as the row has stopped.
|
|
40539
|
+
const pagesAsked = pagesFromProp || pagesFromChildren;
|
|
40540
|
+
const movementRef = useRef(null);
|
|
40541
|
+
const pagesRef = useRef(pagesAsked);
|
|
40542
|
+
if (!movementRef.current) {
|
|
40543
|
+
pagesRef.current = pagesAsked;
|
|
40544
|
+
}
|
|
40545
|
+
const pages = pagesRef.current;
|
|
40626
40546
|
const currentIndex = currentPageIndex(pages);
|
|
40627
|
-
//
|
|
40628
|
-
//
|
|
40629
|
-
|
|
40630
|
-
|
|
40631
|
-
|
|
40632
|
-
|
|
40633
|
-
|
|
40634
|
-
const
|
|
40635
|
-
|
|
40636
|
-
|
|
40637
|
-
|
|
40638
|
-
|
|
40639
|
-
|
|
40640
|
-
|
|
40641
|
-
|
|
40642
|
-
|
|
40643
|
-
|
|
40644
|
-
|
|
40645
|
-
|
|
40646
|
-
|
|
40647
|
-
|
|
40648
|
-
|
|
40649
|
-
|
|
40650
|
-
|
|
40651
|
-
|
|
40652
|
-
|
|
40653
|
-
|
|
40654
|
-
|
|
40655
|
-
|
|
40656
|
-
|
|
40657
|
-
|
|
40658
|
-
|
|
40659
|
-
|
|
40660
|
-
|
|
40661
|
-
|
|
40662
|
-
|
|
40663
|
-
|
|
40664
|
-
|
|
40665
|
-
|
|
40666
|
-
|
|
40667
|
-
|
|
40668
|
-
|
|
40669
|
-
ended: false,
|
|
40670
|
-
// The box as it set off, kept rather than read from the ref: the
|
|
40671
|
-
// navigation a travel asks for can take the box down (see the update
|
|
40672
|
-
// callback), and the name it wears is still given back to it.
|
|
40673
|
-
element: elementRef.current
|
|
40547
|
+
// Where the page of a window stands: the address, or where it last was for an
|
|
40548
|
+
// address the row has no place for.
|
|
40549
|
+
const liveIndexRef = useRef(0);
|
|
40550
|
+
if (currentIndex !== -1) {
|
|
40551
|
+
liveIndexRef.current = currentIndex;
|
|
40552
|
+
}
|
|
40553
|
+
const liveIndex = liveIndexRef.current;
|
|
40554
|
+
const latestRef = useRef(null);
|
|
40555
|
+
latestRef.current = {
|
|
40556
|
+
pages,
|
|
40557
|
+
currentIndex,
|
|
40558
|
+
liveIndex,
|
|
40559
|
+
onTravel,
|
|
40560
|
+
travelByDrag
|
|
40561
|
+
};
|
|
40562
|
+
|
|
40563
|
+
// The page this box asked for: routing lands later, and read back as somebody
|
|
40564
|
+
// changing the address it would send the row where the hand already is.
|
|
40565
|
+
const askedPageRef = useRef(null);
|
|
40566
|
+
const panesOf = row => Array.from(row.children).filter(child => child.hasAttribute("data-route-row-pane"));
|
|
40567
|
+
const pathOf = ({
|
|
40568
|
+
route,
|
|
40569
|
+
params
|
|
40570
|
+
}) => new URL(route.buildUrl(params), window.location.href).pathname;
|
|
40571
|
+
|
|
40572
|
+
// One movement, whoever set it off, from the first thing that moves to the row
|
|
40573
|
+
// at rest. Null when nothing moves.
|
|
40574
|
+
const beginMovement = fields => {
|
|
40575
|
+
const row = rowRef.current;
|
|
40576
|
+
if (movementRef.current) {
|
|
40577
|
+
Object.assign(movementRef.current, fields);
|
|
40578
|
+
return movementRef.current;
|
|
40579
|
+
}
|
|
40580
|
+
row.setAttribute("data-moving", "");
|
|
40581
|
+
const movement = {
|
|
40582
|
+
travelling: false,
|
|
40583
|
+
pictures: new Map(),
|
|
40584
|
+
resumeScrollRecording: null,
|
|
40585
|
+
scrollsDocument: getScrollContainer(row.parentElement, {
|
|
40586
|
+
includeHidden: true
|
|
40587
|
+
}) === document.documentElement,
|
|
40588
|
+
...fields
|
|
40674
40589
|
};
|
|
40675
|
-
|
|
40676
|
-
|
|
40677
|
-
|
|
40678
|
-
|
|
40679
|
-
|
|
40680
|
-
|
|
40681
|
-
|
|
40682
|
-
|
|
40683
|
-
|
|
40684
|
-
|
|
40685
|
-
|
|
40686
|
-
|
|
40687
|
-
|
|
40590
|
+
movementRef.current = movement;
|
|
40591
|
+
if (movement.scrollsDocument) {
|
|
40592
|
+
// Whatever the browser does with the offset meanwhile (clamping it to a page
|
|
40593
|
+
// shorter than the trip) is not the reader scrolling, and the address it
|
|
40594
|
+
// would be written against may already be another page's.
|
|
40595
|
+
movement.resumeScrollRecording = suspendScrollRecording();
|
|
40596
|
+
row.style.setProperty("--navi-route-row-pin", `${pinOf(row)}px`);
|
|
40597
|
+
}
|
|
40598
|
+
return movement;
|
|
40599
|
+
};
|
|
40600
|
+
|
|
40601
|
+
// A clone of the page in the pane it stands in, before anything changes it. In
|
|
40602
|
+
// that pane and the same height: the row keeps its size, so the document its
|
|
40603
|
+
// offset, and the page one looked at is where it was.
|
|
40604
|
+
const leavePictureOfLive = movement => {
|
|
40605
|
+
const live = liveRef.current;
|
|
40606
|
+
const index = latestRef.current.liveIndex;
|
|
40607
|
+
const picture = live.cloneNode(true);
|
|
40608
|
+
picture.removeAttribute("data-route-row-live");
|
|
40609
|
+
picture.setAttribute("inert", "");
|
|
40610
|
+
picture.setAttribute("aria-hidden", "true");
|
|
40611
|
+
picture.style.gridColumn = String(index + 1);
|
|
40612
|
+
picture.style.top = "";
|
|
40613
|
+
movement.pictures.get(index)?.remove();
|
|
40614
|
+
movement.pictures.set(index, picture);
|
|
40615
|
+
// After the live page in the document: an id it holds still finds the page.
|
|
40616
|
+
picturesRef.current.append(picture);
|
|
40617
|
+
copyScrollPositions(live, picture);
|
|
40618
|
+
if (movement.scrollsDocument) {
|
|
40619
|
+
live.style.top = `${pinOf(rowRef.current)}px`;
|
|
40620
|
+
}
|
|
40621
|
+
};
|
|
40622
|
+
const askFor = page => {
|
|
40623
|
+
const {
|
|
40624
|
+
onTravel
|
|
40625
|
+
} = latestRef.current;
|
|
40626
|
+
askedPageRef.current = page;
|
|
40627
|
+
notePageChange();
|
|
40628
|
+
return onTravel({
|
|
40629
|
+
route: page.route,
|
|
40630
|
+
params: page.params,
|
|
40631
|
+
cause: "drag"
|
|
40688
40632
|
});
|
|
40689
|
-
|
|
40690
|
-
|
|
40691
|
-
|
|
40692
|
-
|
|
40693
|
-
|
|
40694
|
-
|
|
40695
|
-
|
|
40696
|
-
|
|
40697
|
-
|
|
40698
|
-
|
|
40699
|
-
|
|
40700
|
-
|
|
40701
|
-
|
|
40702
|
-
|
|
40703
|
-
|
|
40704
|
-
|
|
40705
|
-
|
|
40706
|
-
|
|
40707
|
-
|
|
40708
|
-
|
|
40709
|
-
|
|
40710
|
-
|
|
40711
|
-
|
|
40712
|
-
|
|
40713
|
-
|
|
40714
|
-
|
|
40715
|
-
|
|
40716
|
-
if (
|
|
40717
|
-
|
|
40718
|
-
|
|
40719
|
-
|
|
40720
|
-
|
|
40721
|
-
|
|
40722
|
-
|
|
40723
|
-
|
|
40724
|
-
|
|
40725
|
-
|
|
40726
|
-
|
|
40727
|
-
|
|
40728
|
-
|
|
40729
|
-
|
|
40730
|
-
|
|
40731
|
-
|
|
40732
|
-
|
|
40733
|
-
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
const renderWait = armRouteRenderWait();
|
|
40737
|
-
const viewTransition = startViewTransition(async () => {
|
|
40738
|
-
// Let go of too early before the browser got here, a frame or several on
|
|
40739
|
-
// a slow phone. Skipping a transition does not cancel this callback, and
|
|
40740
|
-
// the router asked from here would take the page anyway, with nobody left
|
|
40741
|
-
// to bring it back.
|
|
40742
|
-
if (travel.reverting) {
|
|
40743
|
-
renderWait.stop();
|
|
40744
|
-
releaseRendering();
|
|
40745
|
-
return;
|
|
40746
|
-
}
|
|
40747
|
-
travel.calledBack = true;
|
|
40748
|
-
// Whatever is awaited here must be able to resolve without the page being
|
|
40749
|
-
// rendered: the document is frozen for the whole of this callback, and a
|
|
40750
|
-
// frame never comes — waiting for one waits until the browser gives up on
|
|
40751
|
-
// the transition. And it stays frozen exactly this long, so this is also
|
|
40752
|
-
// the shortest thing there is to keep short.
|
|
40753
|
-
await whilePageRenders(page, async () => {
|
|
40754
|
-
releaseRendering();
|
|
40755
|
-
if (change) {
|
|
40756
|
-
await change();
|
|
40757
|
-
}
|
|
40758
|
-
}, renderWait);
|
|
40759
|
-
// Where the arriving tab was left, or its top when it has never been
|
|
40760
|
-
// read. A travel is an arrival — the tab pressed is another route, and
|
|
40761
|
-
// the replace it navigates by is the only thing about it that says
|
|
40762
|
-
// otherwise — and this is the one place that knows it. Read from the
|
|
40763
|
-
// travel rather than from the page it set off for: a travel aimed
|
|
40764
|
-
// somewhere else while it waited lands where it is aimed now.
|
|
40765
|
-
if (travel.scrollsDocument) {
|
|
40633
|
+
};
|
|
40634
|
+
const sayRowMoves = () => {
|
|
40635
|
+
const row = rowRef.current;
|
|
40636
|
+
const panes = panesOf(row);
|
|
40637
|
+
if (panes.length < 2) {
|
|
40638
|
+
return;
|
|
40639
|
+
}
|
|
40640
|
+
const {
|
|
40641
|
+
pages
|
|
40642
|
+
} = latestRef.current;
|
|
40643
|
+
const {
|
|
40644
|
+
base,
|
|
40645
|
+
toward,
|
|
40646
|
+
fraction
|
|
40647
|
+
} = readRowStretch(row, panes, panes[0]);
|
|
40648
|
+
publishRouteRowMove({
|
|
40649
|
+
basePath: pathOf(pages[panes.indexOf(base)]),
|
|
40650
|
+
towardPath: toward ? pathOf(pages[panes.indexOf(toward)]) : null,
|
|
40651
|
+
progress: -fraction
|
|
40652
|
+
});
|
|
40653
|
+
};
|
|
40654
|
+
|
|
40655
|
+
// The row has stopped, on the page of the address: everything held for the
|
|
40656
|
+
// movement is let go of in one go, before anything paints.
|
|
40657
|
+
const endMovement = () => {
|
|
40658
|
+
const row = rowRef.current;
|
|
40659
|
+
const movement = movementRef.current;
|
|
40660
|
+
if (!movement) {
|
|
40661
|
+
return;
|
|
40662
|
+
}
|
|
40663
|
+
movementRef.current = null;
|
|
40664
|
+
row.removeAttribute("data-moving");
|
|
40665
|
+
publishRouteRowMove(null);
|
|
40666
|
+
routeTravelSettledPageSignal.value = null;
|
|
40667
|
+
if (liveRef.current) {
|
|
40668
|
+
liveRef.current.style.top = "";
|
|
40669
|
+
}
|
|
40670
|
+
for (const picture of movement.pictures.values()) {
|
|
40671
|
+
picture.remove();
|
|
40672
|
+
}
|
|
40673
|
+
if (movement.scrollsDocument) {
|
|
40674
|
+
row.style.removeProperty("--navi-route-row-pin");
|
|
40675
|
+
const {
|
|
40676
|
+
pages,
|
|
40677
|
+
currentIndex
|
|
40678
|
+
} = latestRef.current;
|
|
40679
|
+
if (currentIndex !== -1) {
|
|
40766
40680
|
const {
|
|
40767
40681
|
route,
|
|
40768
40682
|
params
|
|
40769
|
-
} =
|
|
40683
|
+
} = pages[currentIndex];
|
|
40770
40684
|
arriveAtScrollPosition(route.buildUrl(params));
|
|
40771
40685
|
}
|
|
40772
|
-
|
|
40773
|
-
// row rather than around them, it went down with the page being left,
|
|
40774
|
-
// and there is no box left for the page arriving to be photographed in.
|
|
40775
|
-
// The navigation stands — it is what was asked for — and the movement
|
|
40776
|
-
// is dropped.
|
|
40777
|
-
if (!travel.element.isConnected) {
|
|
40778
|
-
travel.viewTransition.skipTransition();
|
|
40779
|
-
endTravel(travel);
|
|
40780
|
-
return;
|
|
40781
|
-
}
|
|
40782
|
-
// The page arriving is in the DOM and the transition has not started
|
|
40783
|
-
// playing: the one moment both boxes can be known. After the scroll,
|
|
40784
|
-
// which is what the box is measured through: the two states are at the
|
|
40785
|
-
// same place in the layout without being at the same place in the window
|
|
40786
|
-
// (see transition_window.js).
|
|
40787
|
-
// The bars stay live under the pictures for a travel: only the box is
|
|
40788
|
-
// photographed (see the TRAVEL_ATTRIBUTE css).
|
|
40789
|
-
holdTransitionWindow(travel, travel.element, stateBefore, {
|
|
40790
|
-
furnitureLive: true
|
|
40791
|
-
});
|
|
40792
|
-
});
|
|
40793
|
-
travel.viewTransition = viewTransition;
|
|
40794
|
-
// Subscribed first, so it reads before the scrub below writes anything.
|
|
40795
|
-
viewTransition.ready.then(() => {
|
|
40796
|
-
placeTransitionWindow(travel, TRAVEL_NAME);
|
|
40797
|
-
}, ignoreSkipped);
|
|
40798
|
-
if (scrub) {
|
|
40799
|
-
// Said only now: the release has to have something to let go of, and the
|
|
40800
|
-
// transition did not exist a line above.
|
|
40801
|
-
travel.dropHold = holdViewTransition(() => {
|
|
40802
|
-
viewTransition.skipTransition();
|
|
40803
|
-
endTravel(travel);
|
|
40804
|
-
});
|
|
40686
|
+
movement.resumeScrollRecording();
|
|
40805
40687
|
}
|
|
40806
|
-
//
|
|
40807
|
-
|
|
40808
|
-
// hold: the hold is written in CSS against whatever transition is running
|
|
40809
|
-
// (it has to be, so that everything named — a trait under a tab row —
|
|
40810
|
-
// follows the same finger), so left on it would take hold of the transition
|
|
40811
|
-
// that has just replaced ours. Born paused, with nobody holding it, that
|
|
40812
|
-
// one never finishes: its pictures stand over a page that cannot be
|
|
40813
|
-
// touched anymore.
|
|
40814
|
-
viewTransition.finished.catch(() => {
|
|
40815
|
-
// A transition that fails before it ever calls back leaves the page held:
|
|
40816
|
-
// whoever asked for the hold gives it back, here as everywhere else.
|
|
40817
|
-
renderWait.stop();
|
|
40818
|
-
releaseRendering();
|
|
40819
|
-
endTravel(travel);
|
|
40820
|
-
});
|
|
40821
|
-
if (!scrub) {
|
|
40822
|
-
// Nobody is holding it: it plays as any transition does, and is over when
|
|
40823
|
-
// the browser says so.
|
|
40824
|
-
viewTransition.finished.then(() => {
|
|
40825
|
-
endTravel(travel);
|
|
40826
|
-
}, ignoreSkipped);
|
|
40827
|
-
return travel;
|
|
40828
|
-
}
|
|
40829
|
-
// The pictures exist from `ready` on, and the finger may have moved a long
|
|
40830
|
-
// way by then — held at their start meanwhile, so this is where they catch
|
|
40831
|
-
// up with it rather than where they set off.
|
|
40832
|
-
viewTransition.ready.then(() => {
|
|
40833
|
-
if (travel === travelRef.current) {
|
|
40834
|
-
scrubTravel(travel, travel.ratio);
|
|
40835
|
-
}
|
|
40836
|
-
}, ignoreSkipped);
|
|
40837
|
-
return travel;
|
|
40688
|
+
// A window recentred meanwhile is taken now.
|
|
40689
|
+
render();
|
|
40838
40690
|
};
|
|
40839
40691
|
|
|
40840
|
-
//
|
|
40841
|
-
//
|
|
40842
|
-
//
|
|
40843
|
-
|
|
40844
|
-
//
|
|
40845
|
-
// Watched as a position in the row rather than route by route. A route
|
|
40846
|
-
// announces its own status, and the row's tabs can all be one route: the
|
|
40847
|
-
// announcement then says a section changed without saying which is on screen,
|
|
40848
|
-
// and it says it about things this row does not move for (a route that has
|
|
40849
|
-
// now been visited, a param of its own that is not a tab of this row). Worse,
|
|
40850
|
-
// a status is published from inside the routing and the params it carries are
|
|
40851
|
-
// the ones known at that instant — a section that lands as a signal settles
|
|
40852
|
-
// is announced late, or not at all. The signals ARE the position, so the
|
|
40853
|
-
// position is read from them: one computed over the whole row, notified once
|
|
40854
|
-
// per move, whichever route moved and whether by matching or by params.
|
|
40692
|
+
// The address and the row, in step. The opening and a row given again around the
|
|
40693
|
+
// page on screen have nothing to show for it; somebody else moving the address
|
|
40694
|
+
// (a tab pressed, the back button) is a travel.
|
|
40695
|
+
const answeredPageRef = useRef(null);
|
|
40855
40696
|
useLayoutEffect(() => {
|
|
40856
|
-
const
|
|
40857
|
-
const
|
|
40858
|
-
|
|
40859
|
-
|
|
40860
|
-
|
|
40861
|
-
|
|
40862
|
-
|
|
40863
|
-
|
|
40864
|
-
|
|
40865
|
-
|
|
40866
|
-
// for is here nonetheless, so the wait is called off.
|
|
40867
|
-
if (samePage(pageAskedForRef.current, page)) {
|
|
40868
|
-
pageAskedForRef.current = null;
|
|
40869
|
-
}
|
|
40870
|
-
return;
|
|
40871
|
-
}
|
|
40872
|
-
// Whoever asked for it: a tab of this row can be a param, and a move
|
|
40873
|
-
// between two of them is a page change the path does not show (see
|
|
40874
|
-
// page_change.js).
|
|
40875
|
-
notePageChange();
|
|
40876
|
-
// A page this box asked for itself — a travel's own navigation, or one
|
|
40877
|
-
// it had given up waiting on: what arrives here is the answer to a
|
|
40878
|
-
// question already answered, not somebody going somewhere.
|
|
40879
|
-
if (samePage(pageAskedForRef.current, page)) {
|
|
40880
|
-
pageAskedForRef.current = null;
|
|
40881
|
-
currentPageRef.current = page;
|
|
40882
|
-
return;
|
|
40883
|
-
}
|
|
40884
|
-
// Somewhere else arrived first: whatever this box was still waiting for
|
|
40885
|
-
// is not coming, or no longer means anything. Forgotten here rather
|
|
40886
|
-
// than kept, or the next press on that very tab would be taken for the
|
|
40887
|
-
// late answer to a question nobody remembers asking.
|
|
40888
|
-
pageAskedForRef.current = null;
|
|
40889
|
-
// Asked for a page while one was already on its way: the travel in
|
|
40890
|
-
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
40891
|
-
// top would leave this one's pictures to be dropped mid-slide.
|
|
40892
|
-
if (travelRef.current) {
|
|
40893
|
-
currentPageRef.current = page;
|
|
40894
|
-
retargetTravel(travelRef.current, page);
|
|
40895
|
-
return;
|
|
40896
|
-
}
|
|
40897
|
-
const fromIndex = pageIndexOf(pages, currentPageRef.current);
|
|
40898
|
-
currentPageRef.current = page;
|
|
40899
|
-
if (fromIndex === -1) {
|
|
40900
|
-
// Arriving from outside this row: there is no pair of pages to show,
|
|
40901
|
-
// so there is nothing to travel between.
|
|
40902
|
-
return;
|
|
40903
|
-
}
|
|
40904
|
-
beginTravel({
|
|
40905
|
-
page,
|
|
40906
|
-
fromPage: pages[fromIndex],
|
|
40907
|
-
direction: index > fromIndex ? "forward" : "back",
|
|
40908
|
-
scrub: false
|
|
40909
|
-
});
|
|
40910
|
-
};
|
|
40911
|
-
// `subscribe` rather than `effect`: it hands the value to a callback that
|
|
40912
|
-
// is NOT being tracked, and what this one does — navigate, ask the router
|
|
40913
|
-
// for another page — reads and writes the very signals the row is watched
|
|
40914
|
-
// through.
|
|
40915
|
-
return currentIndexSignal.subscribe(onRowMove);
|
|
40916
|
-
}, [pages]);
|
|
40917
|
-
|
|
40918
|
-
// Rendering is held for the length of a navigation, so that whatever picture
|
|
40919
|
-
// this box is about to take is of the page being LEFT (see
|
|
40920
|
-
// rendering_hold.js).
|
|
40921
|
-
useLayoutEffect(() => {
|
|
40922
|
-
const stopWatchingStart = observeBeforeRouting(holdRenderingForRouting);
|
|
40923
|
-
// Nobody may have had a picture to take: this navigation is not always one
|
|
40924
|
-
// this box travels. Whoever wanted the hold took it over (beginTravel)
|
|
40925
|
-
// while the change was being applied, and left nothing here.
|
|
40926
|
-
const stopWatchingEnd = observeAfterRouting(releaseRoutingRenderingHold);
|
|
40927
|
-
return () => {
|
|
40928
|
-
stopWatchingStart();
|
|
40929
|
-
stopWatchingEnd();
|
|
40930
|
-
};
|
|
40931
|
-
}, []);
|
|
40932
|
-
|
|
40933
|
-
// What the next announcement will compare itself against: written after the
|
|
40934
|
-
// render that shows it, so it is always the page one is looking at.
|
|
40935
|
-
useLayoutEffect(() => {
|
|
40936
|
-
currentPageRef.current = currentPage;
|
|
40937
|
-
}, [currentPage]);
|
|
40938
|
-
|
|
40939
|
-
// Let go of far enough: the movement carries on from under the finger, at its
|
|
40940
|
-
// own pace, to the end.
|
|
40941
|
-
const finishTravel = travel => {
|
|
40942
|
-
// Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
|
|
40943
|
-
// set them off: left standing after the release, this travel would go on
|
|
40944
|
-
// claiming a hand that is no longer there — and everything that asks
|
|
40945
|
-
// "is somebody holding this?" before touching it (a press on the tab one
|
|
40946
|
-
// came from, a wheel push) would be answered yes and do nothing, while the
|
|
40947
|
-
// pages carry on to a page the router has already left.
|
|
40948
|
-
travel.scrub = false;
|
|
40949
|
-
releaseHold();
|
|
40950
|
-
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
40951
|
-
};
|
|
40952
|
-
|
|
40953
|
-
// Let go of too early: the pages go back the way they came, and the URL with
|
|
40954
|
-
// them. The way back is not a travel of its own — the same animations are run
|
|
40955
|
-
// backwards, and the page that was left is put back under the picture BEFORE
|
|
40956
|
-
// the picture is dropped, so the two are the same thing at the moment they
|
|
40957
|
-
// are swapped and nothing is seen changing.
|
|
40958
|
-
const revertTravel = travel => {
|
|
40959
|
-
travel.reverting = true;
|
|
40960
|
-
// A revert somebody ASKED for — a press on the tab this travel came from —
|
|
40961
|
-
// has a problem the others do not: that page is already back, and the
|
|
40962
|
-
// picture being brought in is LIVE, so it shows the page one is going back
|
|
40963
|
-
// to. Both sides then show the same thing and the way back is invisible:
|
|
40964
|
-
// one presses, and one is simply there.
|
|
40965
|
-
//
|
|
40966
|
-
// So the pages are held where they are until the pictures have finished
|
|
40967
|
-
// going back. Only the pages: unlike the hold a navigation takes to have
|
|
40968
|
-
// its picture taken, nothing is being photographed here — everything else
|
|
40969
|
-
// may go on rendering, and a tab row beside the box keeps answering.
|
|
40970
|
-
const releaseRendering = freezeRouteRender();
|
|
40971
|
-
const animations = travelAnimations(travel);
|
|
40972
|
-
walkPicturesHome(animations, TRAVEL_LEAD_NAMES);
|
|
40973
|
-
releaseHold(travel);
|
|
40974
|
-
whenPicturesArrived(animations).then(async () => {
|
|
40975
|
-
try {
|
|
40976
|
-
pageAskedForRef.current = travel.fromPage;
|
|
40977
|
-
// The page that was left is put back UNDER the picture before the
|
|
40978
|
-
// picture is dropped, so the two are the same thing at the moment they
|
|
40979
|
-
// are swapped: that only holds once the page is really back.
|
|
40980
|
-
if (pageIsCurrent(travel.fromPage)) {
|
|
40981
|
-
// It never left: the press that set this revert off put it back
|
|
40982
|
-
// there, and the pages have been held where they were until now.
|
|
40983
|
-
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
40984
|
-
// render that never comes and a page frozen under its own pictures.
|
|
40985
|
-
releaseRendering();
|
|
40986
|
-
} else {
|
|
40987
|
-
await whilePageRenders(travel.fromPage, () => onTravel({
|
|
40988
|
-
route: travel.fromPage.route,
|
|
40989
|
-
params: travel.fromPage.params,
|
|
40990
|
-
cause: "revert"
|
|
40991
|
-
}));
|
|
40992
|
-
}
|
|
40993
|
-
travel.viewTransition.skipTransition();
|
|
40994
|
-
} finally {
|
|
40995
|
-
// Refused before the browser called back, nothing was swapped: the
|
|
40996
|
-
// offset is still the page's own, and no render is on its way to say so.
|
|
40997
|
-
if (travel.scrollsDocument && travel.calledBack) {
|
|
40998
|
-
// The page coming back was read somewhere else, and the offset
|
|
40999
|
-
// currently on the document is the one the page that came in put
|
|
41000
|
-
// there. Given back once the page is really back — the render is
|
|
41001
|
-
// what makes the document tall enough to hold that offset again —
|
|
41002
|
-
// and the recording stays deaf until then, over the clamp the swap
|
|
41003
|
-
// back makes on the way. Its own deafness rather than the travel's:
|
|
41004
|
-
// the travel ends here, and this outlives it by a render. Written
|
|
41005
|
-
// once the commit that says so has ended, as arriveAtScrollPosition
|
|
41006
|
-
// asks.
|
|
41007
|
-
const resumeScrollRecording = suspendScrollRecording();
|
|
41008
|
-
const stopWatchingRender = observeRouteRender(() => {
|
|
41009
|
-
stopWatchingRender();
|
|
41010
|
-
queueMicrotask(() => {
|
|
41011
|
-
const {
|
|
41012
|
-
route,
|
|
41013
|
-
params
|
|
41014
|
-
} = travel.fromPage;
|
|
41015
|
-
arriveAtScrollPosition(route.buildUrl(params));
|
|
41016
|
-
resumeScrollRecording();
|
|
41017
|
-
});
|
|
41018
|
-
});
|
|
41019
|
-
}
|
|
41020
|
-
releaseRendering();
|
|
41021
|
-
// A travel ENDS, whatever happened on the way back: put the state back,
|
|
41022
|
-
// fail to drop the picture, be interrupted by something else — the one
|
|
41023
|
-
// thing that must not happen is a travel that stays "in flight"
|
|
41024
|
-
// forever. Nothing would lift the hold, the pictures would stand where
|
|
41025
|
-
// they are over a page that cannot be touched, and every gesture after
|
|
41026
|
-
// this one would find the box busy.
|
|
41027
|
-
endTravel(travel);
|
|
40697
|
+
const row = rowRef.current;
|
|
40698
|
+
const page = currentIndex === -1 ? null : pages[currentIndex];
|
|
40699
|
+
const pane = panesOf(row)[currentIndex];
|
|
40700
|
+
const answered = answeredPageRef.current;
|
|
40701
|
+
if (samePage(answered, page)) {
|
|
40702
|
+
if (pane && !movementRef.current && !restsOn(row, pane)) {
|
|
40703
|
+
row.scrollTo({
|
|
40704
|
+
left: pane.offsetLeft,
|
|
40705
|
+
behavior: "instant"
|
|
40706
|
+
});
|
|
41028
40707
|
}
|
|
41029
|
-
});
|
|
41030
|
-
};
|
|
41031
|
-
|
|
41032
|
-
// Taken back in hand: the pictures stop where they are and answer the finger
|
|
41033
|
-
// again (see the CSS hold).
|
|
41034
|
-
const holdTravel = travel => {
|
|
41035
|
-
if (travel.ended || travel.reverting) {
|
|
41036
40708
|
return;
|
|
41037
40709
|
}
|
|
41038
|
-
|
|
41039
|
-
|
|
41040
|
-
// Whatever asks for a transition next takes ours away (there is one per
|
|
41041
|
-
// document): it must find this one already let go of, or it inherits a hold
|
|
41042
|
-
// nobody is holding.
|
|
41043
|
-
travel.dropHold = holdViewTransition(() => {
|
|
41044
|
-
travel.viewTransition?.skipTransition();
|
|
41045
|
-
endTravel(travel);
|
|
41046
|
-
});
|
|
41047
|
-
};
|
|
41048
|
-
|
|
41049
|
-
// The same travel, aimed at another page. The still it starts from does not
|
|
41050
|
-
// change — only what is being brought in against it, and that one is LIVE:
|
|
41051
|
-
// pointing the router elsewhere is all it takes for the picture to show that
|
|
41052
|
-
// page instead.
|
|
41053
|
-
const redirectTravel = (travel, page, direction) => {
|
|
41054
|
-
travel.page = page;
|
|
41055
|
-
// Everything the transition carries that is NOT the pages was measured
|
|
41056
|
-
// against a destination this travel is no longer going to (see the CSS).
|
|
41057
|
-
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
41058
|
-
if (direction === travel.direction) {
|
|
41059
|
-
// Same way, another page: the pictures in hand are already the right
|
|
41060
|
-
// pair, and nothing has to move.
|
|
40710
|
+
answeredPageRef.current = page;
|
|
40711
|
+
if (!page || !pane) {
|
|
41061
40712
|
return;
|
|
41062
40713
|
}
|
|
41063
|
-
|
|
41064
|
-
|
|
41065
|
-
|
|
41066
|
-
|
|
41067
|
-
|
|
41068
|
-
|
|
41069
|
-
|
|
41070
|
-
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
41071
|
-
};
|
|
41072
|
-
|
|
41073
|
-
// Somebody asked for a page while one was on its way. Where they asked for
|
|
41074
|
-
// decides what that means.
|
|
41075
|
-
const retargetTravel = (travel, page) => {
|
|
41076
|
-
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
41077
|
-
// A hand is holding the pages, or they are already on their way back:
|
|
41078
|
-
// either way this travel's end is decided by somebody else.
|
|
41079
|
-
return;
|
|
41080
|
-
}
|
|
41081
|
-
if (samePage(page, travel.page)) {
|
|
41082
|
-
// Already on its way there.
|
|
41083
|
-
return;
|
|
41084
|
-
}
|
|
41085
|
-
if (samePage(page, travel.fromPage)) {
|
|
41086
|
-
// Back where it set off from: that is not another travel, it is this one
|
|
41087
|
-
// undone — the same pictures, run backwards.
|
|
41088
|
-
revertTravel(travel);
|
|
40714
|
+
const asked = askedPageRef.current;
|
|
40715
|
+
if (asked && samePage(asked, page)) {
|
|
40716
|
+
askedPageRef.current = null;
|
|
40717
|
+
// The hand landed before the address did.
|
|
40718
|
+
if (movementRef.current?.landed && restsOn(row, pane)) {
|
|
40719
|
+
Promise.resolve().then(endMovement);
|
|
40720
|
+
}
|
|
41089
40721
|
return;
|
|
41090
40722
|
}
|
|
41091
|
-
|
|
41092
|
-
|
|
41093
|
-
|
|
40723
|
+
if (!answered || restsOn(row, pane)) {
|
|
40724
|
+
if (!restsOn(row, pane)) {
|
|
40725
|
+
row.scrollTo({
|
|
40726
|
+
left: pane.offsetLeft,
|
|
40727
|
+
behavior: "instant"
|
|
40728
|
+
});
|
|
40729
|
+
}
|
|
41094
40730
|
return;
|
|
41095
40731
|
}
|
|
41096
|
-
|
|
41097
|
-
|
|
40732
|
+
beginMovement({
|
|
40733
|
+
travelling: true
|
|
40734
|
+
});
|
|
40735
|
+
row.scrollTo({
|
|
40736
|
+
left: pane.offsetLeft,
|
|
40737
|
+
behavior: "smooth"
|
|
40738
|
+
});
|
|
40739
|
+
// In this very commit: the tab pressed already wears its bar, and the first
|
|
40740
|
+
// scroll event is a frame away.
|
|
40741
|
+
sayRowMoves();
|
|
40742
|
+
});
|
|
41098
40743
|
|
|
41099
|
-
//
|
|
41100
|
-
//
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
// simply arrives where it was going.
|
|
41105
|
-
const aimOnePageFurther = (travel, sign) => {
|
|
41106
|
-
const fromIndex = pageIndexOf(pages, travel.page);
|
|
41107
|
-
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
41108
|
-
if (fromIndex === -1 || !page) {
|
|
41109
|
-
return;
|
|
41110
|
-
}
|
|
41111
|
-
redirectTravel(travel, page, travel.direction);
|
|
41112
|
-
pageAskedForRef.current = page;
|
|
41113
|
-
travelTo(page, "drag");
|
|
41114
|
-
};
|
|
41115
|
-
const endTravel = travel => {
|
|
41116
|
-
if (travel.ended) {
|
|
41117
|
-
return;
|
|
40744
|
+
// Somebody else is about to move the address to another page of a window: its
|
|
40745
|
+
// picture has to be taken now, while it still shows the page being left.
|
|
40746
|
+
useLayoutEffect(() => {
|
|
40747
|
+
if (!windowed) {
|
|
40748
|
+
return undefined;
|
|
41118
40749
|
}
|
|
41119
|
-
|
|
41120
|
-
|
|
41121
|
-
travel.dropHold = null;
|
|
41122
|
-
if (travelHoldingSettledPage === travel) {
|
|
41123
|
-
travelHoldingSettledPage = null;
|
|
41124
|
-
routeTravelSettledPageSignal.value = null;
|
|
41125
|
-
}
|
|
41126
|
-
if (travelRef.current === travel) {
|
|
41127
|
-
document.documentElement.removeEventListener("touchmove", keepTouchRefusable);
|
|
41128
|
-
}
|
|
41129
|
-
// The offset on the scrollport belongs to a page again — the one that
|
|
41130
|
-
// arrived, or the one put back. A travel that never got as far as saying
|
|
41131
|
-
// which still has to give the recording back; the gesture's own
|
|
41132
|
-
// pseudo-travel never took it (see noPicture).
|
|
41133
|
-
travel.resumeScrollRecording?.();
|
|
41134
|
-
// Its own hold, always — whether or not this travel is still the current
|
|
41135
|
-
// one. Nobody else will lift it.
|
|
41136
|
-
releaseHold(travel);
|
|
41137
|
-
if (travelRef.current === travel) {
|
|
41138
|
-
travelRef.current = null;
|
|
41139
|
-
// Given back, so another box may wear it: kept, two of them on a page
|
|
41140
|
-
// would both answer to it and the browser refuses the whole transition
|
|
41141
|
-
// rather than pick.
|
|
41142
|
-
unnameAfterTravel(travel.element);
|
|
41143
|
-
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
41144
|
-
document.documentElement.removeAttribute(TRAVEL_AXIS_ATTRIBUTE);
|
|
41145
|
-
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
41146
|
-
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
41147
|
-
releaseTransitionWindow(travel);
|
|
41148
|
-
releaseTransitionDestination(travel);
|
|
41149
|
-
releaseTransitionPress(travel);
|
|
41150
|
-
}
|
|
41151
|
-
};
|
|
41152
|
-
|
|
41153
|
-
// What a gesture is about, whichever hand made it: a thumb dragging the page
|
|
41154
|
-
// and two fingers pushing it sideways on a trackpad ask for the same travel,
|
|
41155
|
-
// so they are answered by the same three callbacks and only the reading of
|
|
41156
|
-
// the input differs (see drag_to_travel.js).
|
|
41157
|
-
const boxSizeOnAxis = () => {
|
|
41158
|
-
const box = elementRef.current.getBoundingClientRect();
|
|
41159
|
-
return axis === "x" ? box.width : box.height;
|
|
41160
|
-
};
|
|
41161
|
-
const travelHandlers = {
|
|
41162
|
-
onStart: ({
|
|
41163
|
-
sign,
|
|
41164
|
-
target
|
|
40750
|
+
return observeBeforeRouting(({
|
|
40751
|
+
url
|
|
41165
40752
|
}) => {
|
|
41166
|
-
const
|
|
41167
|
-
|
|
41168
|
-
|
|
41169
|
-
|
|
41170
|
-
|
|
41171
|
-
|
|
41172
|
-
// another — which is what a hand reaching for a page still sliding is
|
|
41173
|
-
// asking for anyway. It is refused only when there is nothing to take
|
|
41174
|
-
// over (no picture at all).
|
|
41175
|
-
//
|
|
41176
|
-
// Never given up, even then: a gesture handed back to the browser is a
|
|
41177
|
-
// page that rocks under a travel that is already moving.
|
|
41178
|
-
// A travel being undone is not up for grabs either: it is already on
|
|
41179
|
-
// its way back and its end is decided. Held again mid-revert, its
|
|
41180
|
-
// animations would never finish — and the wait for them never resolves,
|
|
41181
|
-
// so the pictures stay where they are, over a page that cannot be
|
|
41182
|
-
// touched anymore.
|
|
41183
|
-
if (travelInFlight.noPicture || travelInFlight.ended || travelInFlight.reverting) {
|
|
41184
|
-
return {
|
|
41185
|
-
size,
|
|
41186
|
-
travelBack: false,
|
|
41187
|
-
travelOn: false
|
|
41188
|
-
};
|
|
41189
|
-
}
|
|
41190
|
-
// Stopped here, where it stands now: left playing under the press
|
|
41191
|
-
// until the hand said it holds it (see inFlight in onPointerDown).
|
|
41192
|
-
holdTravel(travelInFlight);
|
|
41193
|
-
// Where the pictures stand right now, said as a pull: what the finger
|
|
41194
|
-
// continues from, so nothing jumps when it takes them over.
|
|
41195
|
-
const ratio = ratioOfTravel(travelInFlight);
|
|
41196
|
-
travelInFlight.ratio = ratio;
|
|
41197
|
-
const pulledSign = travelInFlight.direction === "back" ? 1 : -1;
|
|
41198
|
-
return {
|
|
41199
|
-
size,
|
|
41200
|
-
slack: ratio * size * pulledSign,
|
|
41201
|
-
// One box, the one being travelled. Either of its ends can be walked
|
|
41202
|
-
// out of and the hand carries on into the next page, but that is a
|
|
41203
|
-
// travel of its own and the gesture asks for it when it gets there
|
|
41204
|
-
// (see onEdge) — from here there is one page on its way.
|
|
41205
|
-
travelBack: travelInFlight.direction === "back",
|
|
41206
|
-
travelOn: travelInFlight.direction === "forward"
|
|
41207
|
-
};
|
|
41208
|
-
}
|
|
41209
|
-
// Dragging the page towards the end of the axis brings in what is
|
|
41210
|
-
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
41211
|
-
const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
41212
|
-
if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
41213
|
-
return false;
|
|
41214
|
-
}
|
|
41215
|
-
if (!CAN_KEEP_PICTURE) {
|
|
41216
|
-
travelRef.current = {
|
|
41217
|
-
page,
|
|
41218
|
-
noPicture: true,
|
|
41219
|
-
ended: false
|
|
41220
|
-
};
|
|
41221
|
-
return {
|
|
41222
|
-
size,
|
|
41223
|
-
travelBack: sign > 0,
|
|
41224
|
-
travelOn: sign < 0
|
|
41225
|
-
};
|
|
41226
|
-
}
|
|
41227
|
-
return {
|
|
41228
|
-
size,
|
|
41229
|
-
travelBack: sign > 0,
|
|
41230
|
-
travelOn: sign < 0,
|
|
41231
|
-
// A page change costs what a slide does not, whatever the hand does
|
|
41232
|
-
// next: a navigation, a render, the requests of the page arriving, and
|
|
41233
|
-
// those of the page coming back. So nothing is asked of the router
|
|
41234
|
-
// until the hand has really pulled, and the neighbour is read then:
|
|
41235
|
-
// turned around before that, there is nothing to undo.
|
|
41236
|
-
setOff: ({
|
|
41237
|
-
sign: signNow
|
|
41238
|
-
}) => {
|
|
41239
|
-
const pageThatWay = signNow > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
41240
|
-
if (!pageThatWay || travelRef.current) {
|
|
41241
|
-
return false;
|
|
41242
|
-
}
|
|
41243
|
-
beginTravel({
|
|
41244
|
-
page: pageThatWay,
|
|
41245
|
-
fromPage: pages[currentIndex],
|
|
41246
|
-
direction: signNow > 0 ? "back" : "forward",
|
|
41247
|
-
scrub: true,
|
|
41248
|
-
change: () => travelTo(pageThatWay, "drag")
|
|
41249
|
-
});
|
|
41250
|
-
return {
|
|
41251
|
-
travelBack: signNow > 0,
|
|
41252
|
-
travelOn: signNow < 0
|
|
41253
|
-
};
|
|
41254
|
-
}
|
|
41255
|
-
};
|
|
41256
|
-
},
|
|
41257
|
-
onPull: ({
|
|
41258
|
-
progress
|
|
41259
|
-
}) => {
|
|
41260
|
-
const travel = travelRef.current;
|
|
41261
|
-
if (!travel || travel.noPicture) {
|
|
40753
|
+
const {
|
|
40754
|
+
pages,
|
|
40755
|
+
currentIndex
|
|
40756
|
+
} = latestRef.current;
|
|
40757
|
+
const asked = askedPageRef.current;
|
|
40758
|
+
if (asked || currentIndex === -1) {
|
|
41262
40759
|
return;
|
|
41263
40760
|
}
|
|
41264
|
-
|
|
41265
|
-
|
|
41266
|
-
|
|
41267
|
-
const ratio = travel.direction === "back" ? progress : -progress;
|
|
41268
|
-
travel.ratio = ratio > 0 ? ratio : 0;
|
|
41269
|
-
scrubTravel(travel, travel.ratio);
|
|
41270
|
-
},
|
|
41271
|
-
// A page walked all the way to one of its ends and the finger still going:
|
|
41272
|
-
// what it asks for is the page past that end, and it has said so by not
|
|
41273
|
-
// stopping. Which end decides how it is answered, and the two are not the
|
|
41274
|
-
// same amount of work.
|
|
41275
|
-
onEdge: ({
|
|
41276
|
-
sign,
|
|
41277
|
-
thrown
|
|
41278
|
-
}) => {
|
|
41279
|
-
const travel = travelRef.current;
|
|
41280
|
-
if (thrown) {
|
|
41281
|
-
// Caught, then thrown on before its end: the page after is reached by
|
|
41282
|
-
// aiming the travel in hand further, and let go of it arrives there.
|
|
41283
|
-
// There is no box to hand over — a new one would mean jumping these
|
|
41284
|
-
// pictures the rest of the way first, a page seen leaping — so none
|
|
41285
|
-
// is answered, and the gesture lets the travel go as it is.
|
|
41286
|
-
if (travel && !travel.noPicture && !travel.ended && !travel.reverting) {
|
|
41287
|
-
aimOnePageFurther(travel, sign);
|
|
41288
|
-
}
|
|
41289
|
-
return false;
|
|
40761
|
+
const path = new URL(url, window.location.href).pathname;
|
|
40762
|
+
if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
|
|
40763
|
+
return;
|
|
41290
40764
|
}
|
|
41291
|
-
|
|
41292
|
-
|
|
40765
|
+
const movement = beginMovement({
|
|
40766
|
+
travelling: true
|
|
40767
|
+
});
|
|
40768
|
+
leavePictureOfLive(movement);
|
|
40769
|
+
});
|
|
40770
|
+
}, [windowed]);
|
|
40771
|
+
useLayoutEffect(() => {
|
|
40772
|
+
const row = rowRef.current;
|
|
40773
|
+
const onScroll = () => {
|
|
40774
|
+
const {
|
|
40775
|
+
pages,
|
|
40776
|
+
currentIndex,
|
|
40777
|
+
travelByDrag
|
|
40778
|
+
} = latestRef.current;
|
|
40779
|
+
const panes = panesOf(row);
|
|
40780
|
+
if (panes.length < 2) {
|
|
40781
|
+
return;
|
|
41293
40782
|
}
|
|
41294
|
-
|
|
41295
|
-
|
|
41296
|
-
|
|
41297
|
-
|
|
41298
|
-
|
|
41299
|
-
// Walked whole. What the pictures show is the page that has arrived,
|
|
41300
|
-
// and the one leaving is gone: there is no pair left to travel with, so
|
|
41301
|
-
// the next travel needs pictures of its own — a navigation, a render, a
|
|
41302
|
-
// snapshot, and for those few frames the page does not follow the
|
|
41303
|
-
// finger before catching up with it (see `ready` in beginTravel). At
|
|
41304
|
-
// the start of a gesture that gap is invisible, the hand has barely
|
|
41305
|
-
// moved; here the hand is at full speed, and this is what it costs.
|
|
41306
|
-
//
|
|
41307
|
-
// Where we are is what THIS travel was bringing in — the URL changed as
|
|
41308
|
-
// it set off, so `currentIndex` belongs to a render this gesture is
|
|
41309
|
-
// older than.
|
|
41310
|
-
const fromIndex = pageIndexOf(pages, travel.page);
|
|
41311
|
-
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
41312
|
-
if (fromIndex === -1 || !page) {
|
|
41313
|
-
return false;
|
|
41314
|
-
}
|
|
41315
|
-
// At their very end before they are let go of: what ends the travel in
|
|
41316
|
-
// hand is the next transition starting (there is one per document, and
|
|
41317
|
-
// the funnel skips ours), and a picture skipped short of its end is a
|
|
41318
|
-
// page seen jumping the last few pixels.
|
|
41319
|
-
scrubTravel(travel, 1);
|
|
41320
|
-
travel.ratio = 1;
|
|
41321
|
-
beginTravel({
|
|
41322
|
-
page,
|
|
41323
|
-
fromPage: travel.page,
|
|
41324
|
-
direction,
|
|
41325
|
-
scrub: true,
|
|
41326
|
-
change: () => travelTo(page, "drag")
|
|
41327
|
-
});
|
|
41328
|
-
return {
|
|
41329
|
-
size: boxSizeOnAxis(),
|
|
41330
|
-
travelBack: sign > 0,
|
|
41331
|
-
travelOn: sign < 0
|
|
41332
|
-
};
|
|
40783
|
+
const moving = movementRef.current;
|
|
40784
|
+
const pane = panes[currentIndex];
|
|
40785
|
+
if (!moving && pane && restsOn(row, pane)) {
|
|
40786
|
+
// Written by this box, at rest: nobody is moving anything.
|
|
40787
|
+
return;
|
|
41333
40788
|
}
|
|
41334
|
-
|
|
41335
|
-
|
|
41336
|
-
|
|
41337
|
-
|
|
41338
|
-
// other neighbour is enough for it to show that one instead. The travel
|
|
41339
|
-
// turns around where it stands, on the same transition and under the same
|
|
41340
|
-
// hand, and there is no gap at all.
|
|
41341
|
-
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
41342
|
-
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
41343
|
-
if (fromIndex === -1 || !page) {
|
|
41344
|
-
return false;
|
|
40789
|
+
const movement = moving || beginMovement({});
|
|
40790
|
+
sayRowMoves();
|
|
40791
|
+
if (movement.travelling || !travelByDrag || !pane) {
|
|
40792
|
+
return;
|
|
41345
40793
|
}
|
|
41346
|
-
|
|
41347
|
-
|
|
41348
|
-
|
|
41349
|
-
return {
|
|
41350
|
-
size: boxSizeOnAxis(),
|
|
41351
|
-
travelBack: sign > 0,
|
|
41352
|
-
travelOn: sign < 0
|
|
41353
|
-
};
|
|
41354
|
-
},
|
|
41355
|
-
// The next swipe over a page still arriving: the page after it, and the
|
|
41356
|
-
// travel in flight goes there — the same pictures, the router pointed one
|
|
41357
|
-
// page further, exactly what a tab pressed while it plays does (see
|
|
41358
|
-
// retargetTravel). Nothing is held and nothing restarts.
|
|
41359
|
-
onPushOn: ({
|
|
41360
|
-
sign
|
|
41361
|
-
}) => {
|
|
41362
|
-
const travel = travelRef.current;
|
|
41363
|
-
if (!travel || travel.noPicture || travel.ended || travel.reverting || travel.scrub) {
|
|
41364
|
-
return false;
|
|
40794
|
+
const pulled = row.scrollLeft - pane.offsetLeft;
|
|
40795
|
+
if (Math.abs(pulled) < SET_OFF_PULL) {
|
|
40796
|
+
return;
|
|
41365
40797
|
}
|
|
41366
|
-
|
|
41367
|
-
|
|
41368
|
-
|
|
41369
|
-
|
|
41370
|
-
travels
|
|
41371
|
-
}) => {
|
|
41372
|
-
gestureRef.current = null;
|
|
41373
|
-
const travel = travelRef.current;
|
|
41374
|
-
if (!travel) {
|
|
41375
|
-
// The travel this gesture was holding ended under it. Nothing left to
|
|
41376
|
-
// decide, but the hold is this gesture's own doing and nobody else will
|
|
41377
|
-
// take it off — a hold nobody lifts is a page nobody can touch.
|
|
41378
|
-
releaseHold();
|
|
40798
|
+
const index = pulled > 0 ? currentIndex + 1 : currentIndex - 1;
|
|
40799
|
+
const paneTowards = panes[index];
|
|
40800
|
+
const page = pages[index];
|
|
40801
|
+
if (!paneTowards || samePage(askedPageRef.current, page)) {
|
|
41379
40802
|
return;
|
|
41380
40803
|
}
|
|
41381
|
-
if (
|
|
41382
|
-
|
|
41383
|
-
|
|
41384
|
-
travelTo(travel.page, "drag");
|
|
40804
|
+
if (windowed) {
|
|
40805
|
+
if (movement.pictures.has(index)) {
|
|
40806
|
+
return;
|
|
41385
40807
|
}
|
|
40808
|
+
leavePictureOfLive(movement);
|
|
40809
|
+
} else if (paneTowards.hasAttribute("data-built")) {
|
|
41386
40810
|
return;
|
|
41387
40811
|
}
|
|
41388
|
-
if (
|
|
41389
|
-
|
|
41390
|
-
return;
|
|
40812
|
+
if (routeTravelSettledPageSignal.peek() === null) {
|
|
40813
|
+
routeTravelSettledPageSignal.value = pages[currentIndex];
|
|
41391
40814
|
}
|
|
41392
|
-
|
|
41393
|
-
}
|
|
41394
|
-
};
|
|
41395
|
-
const onPointerDown = pointerDownEvent => {
|
|
41396
|
-
if (!travelByDrag || gestureRef.current) {
|
|
41397
|
-
return;
|
|
41398
|
-
}
|
|
41399
|
-
// A travel playing under the press is left playing: a hand landing on a
|
|
41400
|
-
// page still sliding is far more often the next swipe than a catch, and
|
|
41401
|
-
// stopping the pages at the press is what that swipe feels as a stall. The
|
|
41402
|
-
// gesture listens to the hand first and says which it was — the next page
|
|
41403
|
-
// (onPushOn) or this one, held (onStart) — see inFlight in
|
|
41404
|
-
// drag_to_travel.js. Either way the press is not refused: a gesture given
|
|
41405
|
-
// back to the browser is a page that rocks under a travel already moving.
|
|
41406
|
-
const travelInFlight = travelRef.current;
|
|
41407
|
-
const leftPlaying = travelInFlight && !travelInFlight.noPicture && !travelInFlight.ended && !travelInFlight.reverting && !travelInFlight.scrub;
|
|
41408
|
-
if (currentIndex === -1) {
|
|
41409
|
-
return;
|
|
41410
|
-
}
|
|
41411
|
-
const giveUp = () => {
|
|
41412
|
-
gestureRef.current = null;
|
|
40815
|
+
askFor(page);
|
|
41413
40816
|
};
|
|
41414
|
-
const
|
|
41415
|
-
|
|
41416
|
-
|
|
41417
|
-
inFlight: leftPlaying ? {
|
|
41418
|
-
axis,
|
|
41419
|
-
// The way a pull bringing in the page on its way would go:
|
|
41420
|
-
// dragging towards the end of the axis brings in what is BEFORE.
|
|
41421
|
-
way: travelInFlight.direction === "back" ? 1 : -1
|
|
41422
|
-
} : false,
|
|
41423
|
-
...travelHandlers,
|
|
41424
|
-
onGiveUp: giveUp
|
|
41425
|
-
});
|
|
41426
|
-
if (!gesture) {
|
|
41427
|
-
// Not a press this box can be about — something that reads the pointer
|
|
41428
|
-
// itself, a box below it that travels the same way.
|
|
41429
|
-
giveUp();
|
|
41430
|
-
return;
|
|
41431
|
-
}
|
|
41432
|
-
gestureRef.current = gesture;
|
|
41433
|
-
};
|
|
41434
|
-
|
|
41435
|
-
// A press aimed at a page while a travel is playing does not always reach the
|
|
41436
|
-
// box: the browser's transition covers the page and the press can be delivered
|
|
41437
|
-
// to the document root instead, whatever the pictures are told about pointer
|
|
41438
|
-
// events. So while a travel plays, the press is caught at the document and
|
|
41439
|
-
// handed to the box when it fell where a page is SEEN, which is where the hand
|
|
41440
|
-
// thinks it pressed.
|
|
41441
|
-
//
|
|
41442
|
-
// Decided by the position and never by the target: in the first frames of a
|
|
41443
|
-
// travel a press on a live bar falls to the root like any other, and what lies
|
|
41444
|
-
// under a picture is not what the hand is aiming at.
|
|
41445
|
-
pointerDownRef.current = onPointerDown;
|
|
41446
|
-
useLayoutEffect(() => {
|
|
41447
|
-
const onDocumentPointerDown = pointerDownEvent => {
|
|
41448
|
-
if (!travelRef.current) {
|
|
40817
|
+
const onLand = () => {
|
|
40818
|
+
const movement = movementRef.current;
|
|
40819
|
+
if (!movement) {
|
|
41449
40820
|
return;
|
|
41450
40821
|
}
|
|
41451
|
-
const
|
|
41452
|
-
|
|
41453
|
-
|
|
40822
|
+
const panes = panesOf(row);
|
|
40823
|
+
const pane = pageAtRest(row, panes);
|
|
40824
|
+
if (!pane) {
|
|
41454
40825
|
return;
|
|
41455
40826
|
}
|
|
40827
|
+
movement.landed = true;
|
|
41456
40828
|
const {
|
|
41457
|
-
|
|
41458
|
-
|
|
41459
|
-
|
|
41460
|
-
|
|
41461
|
-
|
|
41462
|
-
|
|
41463
|
-
|
|
41464
|
-
clientY
|
|
41465
|
-
} = pointerDownEvent;
|
|
41466
|
-
if (clientX < left || clientX > right || clientY < top || clientY > bottom) {
|
|
40829
|
+
pages
|
|
40830
|
+
} = latestRef.current;
|
|
40831
|
+
const page = pages[panes.indexOf(pane)];
|
|
40832
|
+
if (!page || pageIsCurrent(page)) {
|
|
40833
|
+
if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
|
|
40834
|
+
endMovement();
|
|
40835
|
+
}
|
|
41467
40836
|
return;
|
|
41468
40837
|
}
|
|
41469
|
-
|
|
41470
|
-
// the pictures are cut at that bar: the press is aimed at the bar.
|
|
41471
|
-
if (isPointInTransitionBand(clientX, clientY)) {
|
|
40838
|
+
if (samePage(askedPageRef.current, page)) {
|
|
41472
40839
|
return;
|
|
41473
40840
|
}
|
|
41474
|
-
|
|
40841
|
+
if (windowed && !movement.pictures.has(panes.indexOf(pane))) {
|
|
40842
|
+
leavePictureOfLive(movement);
|
|
40843
|
+
}
|
|
40844
|
+
Promise.resolve(askFor(page)).then(() => {
|
|
40845
|
+
if (pageIsCurrent(page)) {
|
|
40846
|
+
return;
|
|
40847
|
+
}
|
|
40848
|
+
// Refused, or sent elsewhere: the row says where the address is.
|
|
40849
|
+
askedPageRef.current = null;
|
|
40850
|
+
const {
|
|
40851
|
+
currentIndex
|
|
40852
|
+
} = latestRef.current;
|
|
40853
|
+
const paneCurrent = panesOf(row)[currentIndex];
|
|
40854
|
+
if (paneCurrent) {
|
|
40855
|
+
movement.travelling = true;
|
|
40856
|
+
row.scrollTo({
|
|
40857
|
+
left: paneCurrent.offsetLeft,
|
|
40858
|
+
behavior: "smooth"
|
|
40859
|
+
});
|
|
40860
|
+
}
|
|
40861
|
+
});
|
|
41475
40862
|
};
|
|
41476
|
-
|
|
40863
|
+
const stopWatching = watchRowScroll(row, {
|
|
40864
|
+
onScroll,
|
|
40865
|
+
onLand
|
|
40866
|
+
});
|
|
41477
40867
|
return () => {
|
|
41478
|
-
|
|
40868
|
+
stopWatching();
|
|
40869
|
+
movementRef.current?.resumeScrollRecording?.();
|
|
40870
|
+
movementRef.current = null;
|
|
40871
|
+
publishRouteRowMove(null);
|
|
41479
40872
|
};
|
|
41480
|
-
}, []);
|
|
40873
|
+
}, [windowed]);
|
|
41481
40874
|
|
|
41482
|
-
// A
|
|
41483
|
-
//
|
|
41484
|
-
|
|
41485
|
-
|
|
41486
|
-
|
|
41487
|
-
if (travelInFlight?.scrub) {
|
|
41488
|
-
// A hand is holding the pages. They are its until it lets go.
|
|
41489
|
-
return;
|
|
41490
|
-
}
|
|
41491
|
-
// Where the box is going, which is not where it is: a step asked for while
|
|
41492
|
-
// a travel plays is the page after the one on its way.
|
|
41493
|
-
const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
|
|
41494
|
-
const fromIndex = pageIndexOf(pages, fromPage);
|
|
41495
|
-
if (fromIndex === -1) {
|
|
41496
|
-
return;
|
|
41497
|
-
}
|
|
41498
|
-
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
41499
|
-
if (!page) {
|
|
40875
|
+
// A window's page lands on a pane as soon as the address does. The row has come
|
|
40876
|
+
// to rest on it or will: when it already has, nothing else will say so.
|
|
40877
|
+
useLayoutEffect(() => {
|
|
40878
|
+
const movement = movementRef.current;
|
|
40879
|
+
if (!movement?.landed || currentIndex === -1) {
|
|
41500
40880
|
return;
|
|
41501
40881
|
}
|
|
41502
|
-
|
|
41503
|
-
|
|
41504
|
-
|
|
41505
|
-
// page seen jumping the last few pixels.
|
|
41506
|
-
scrubTravel(travelInFlight, 1);
|
|
41507
|
-
travelInFlight.ratio = 1;
|
|
41508
|
-
}
|
|
41509
|
-
beginTravel({
|
|
41510
|
-
page,
|
|
41511
|
-
fromPage,
|
|
41512
|
-
direction: sign > 0 ? "back" : "forward",
|
|
41513
|
-
scrub: false,
|
|
41514
|
-
change: () => travelTo(page, "wheel")
|
|
41515
|
-
});
|
|
41516
|
-
};
|
|
41517
|
-
|
|
41518
|
-
// Reached through a ref, and the watcher is never rebuilt for it: a travel
|
|
41519
|
-
// CHANGES the current page, so anything listening on `currentIndex` would be
|
|
41520
|
-
// torn down halfway through the very gesture that is moving it.
|
|
41521
|
-
travelHandlersRef.current = travelHandlers;
|
|
41522
|
-
const travelOneStepRef = useRef(null);
|
|
41523
|
-
travelOneStepRef.current = travelOneStep;
|
|
41524
|
-
useLayoutEffect(() => {
|
|
41525
|
-
if (!travelByDrag) {
|
|
41526
|
-
return undefined;
|
|
40882
|
+
const pane = panesOf(rowRef.current)[currentIndex];
|
|
40883
|
+
if (pane && restsOn(rowRef.current, pane)) {
|
|
40884
|
+
Promise.resolve().then(endMovement);
|
|
41527
40885
|
}
|
|
41528
|
-
|
|
41529
|
-
|
|
41530
|
-
onStep: ({
|
|
41531
|
-
sign
|
|
41532
|
-
}) => travelOneStepRef.current(sign)
|
|
41533
|
-
});
|
|
41534
|
-
}, [travelByDrag, axis]);
|
|
40886
|
+
}, [currentIndex]);
|
|
40887
|
+
const rowKind = pages.length < 2 && windowed ? "still" : travelByDrag ? "" : "code";
|
|
41535
40888
|
return jsx("div", {
|
|
41536
40889
|
...rest,
|
|
41537
|
-
ref: elementRef,
|
|
41538
40890
|
className: className ? `navi_route_travel ${className}` : "navi_route_travel",
|
|
41539
|
-
"
|
|
41540
|
-
|
|
41541
|
-
|
|
41542
|
-
|
|
41543
|
-
|
|
41544
|
-
|
|
41545
|
-
|
|
41546
|
-
|
|
41547
|
-
|
|
41548
|
-
|
|
41549
|
-
|
|
41550
|
-
|
|
41551
|
-
|
|
41552
|
-
|
|
41553
|
-
|
|
41554
|
-
|
|
41555
|
-
|
|
41556
|
-
|
|
41557
|
-
|
|
41558
|
-
|
|
41559
|
-
|
|
40891
|
+
children: jsx("div", {
|
|
40892
|
+
ref: rowRef,
|
|
40893
|
+
"data-route-travel-row": rowKind
|
|
40894
|
+
// What a box around this one and a control acting on the press read: this
|
|
40895
|
+
// axis is taken here (see drag_to_travel.js).
|
|
40896
|
+
,
|
|
40897
|
+
"data-travel-by-drag": travelByDrag && rowKind !== "still" ? "x" : undefined,
|
|
40898
|
+
children: windowed ? jsxs(Fragment, {
|
|
40899
|
+
children: [pages.map((page, index) => jsx("div", {
|
|
40900
|
+
"data-route-row-pane": "",
|
|
40901
|
+
style: {
|
|
40902
|
+
gridColumn: index + 1
|
|
40903
|
+
}
|
|
40904
|
+
}, index)), jsx("div", {
|
|
40905
|
+
ref: liveRef,
|
|
40906
|
+
"data-route-row-live": "",
|
|
40907
|
+
style: {
|
|
40908
|
+
gridColumn: liveIndex + 1
|
|
40909
|
+
},
|
|
40910
|
+
children: children
|
|
40911
|
+
}), jsx("div", {
|
|
40912
|
+
ref: picturesRef,
|
|
40913
|
+
"data-route-row-pictures": ""
|
|
40914
|
+
})]
|
|
40915
|
+
}) : jsx(RouteRowContext.Provider, {
|
|
40916
|
+
value: true,
|
|
40917
|
+
children: children
|
|
40918
|
+
})
|
|
40919
|
+
})
|
|
41560
40920
|
});
|
|
41561
40921
|
};
|
|
41562
40922
|
|
|
41563
|
-
//
|
|
41564
|
-
//
|
|
41565
|
-
|
|
41566
|
-
|
|
41567
|
-
|
|
41568
|
-
|
|
41569
|
-
|
|
41570
|
-
// pictures nobody is holding.
|
|
41571
|
-
let travelHoldingPictures = null;
|
|
41572
|
-
const holdPictures = travel => {
|
|
41573
|
-
travelHoldingPictures = travel;
|
|
41574
|
-
document.documentElement.setAttribute(HOLD_ATTRIBUTE, "");
|
|
41575
|
-
};
|
|
41576
|
-
const releaseHold = travel => {
|
|
41577
|
-
if (travel && travelHoldingPictures !== travel) {
|
|
41578
|
-
return;
|
|
41579
|
-
}
|
|
41580
|
-
travelHoldingPictures = null;
|
|
41581
|
-
document.documentElement.removeAttribute(HOLD_ATTRIBUTE);
|
|
41582
|
-
};
|
|
41583
|
-
|
|
41584
|
-
// The animations of the pictures, asked for again until there are some: they
|
|
41585
|
-
// come into existence with the transition, several frames after it was asked
|
|
41586
|
-
// for, and the gesture has already begun by then. Kept once found — the set
|
|
41587
|
-
// does not change for the length of one travel.
|
|
41588
|
-
const travelAnimations = travel => {
|
|
41589
|
-
if (travel.animations) {
|
|
41590
|
-
return travel.animations;
|
|
41591
|
-
}
|
|
41592
|
-
const animations = viewTransitionAnimations();
|
|
41593
|
-
if (animations.length) {
|
|
41594
|
-
travel.animations = animations;
|
|
41595
|
-
}
|
|
41596
|
-
return animations;
|
|
41597
|
-
};
|
|
41598
|
-
|
|
41599
|
-
// How far a travel has come, read off the pictures themselves rather than off
|
|
41600
|
-
// what the last gesture wrote: one let go of is still moving, and a hand
|
|
41601
|
-
// reaching for it must find it where it IS.
|
|
41602
|
-
const ratioOfTravel = travel => {
|
|
41603
|
-
const animations = travelAnimations(travel);
|
|
41604
|
-
const animation = findLeadAnimation(animations, TRAVEL_LEAD_NAMES) || animations[0];
|
|
41605
|
-
if (!animation) {
|
|
41606
|
-
return travel.ratio;
|
|
41607
|
-
}
|
|
41608
|
-
const timing = animation.effect.getComputedTiming();
|
|
41609
|
-
const duration = timing.delay + timing.activeDuration;
|
|
41610
|
-
if (!duration) {
|
|
41611
|
-
return travel.ratio;
|
|
41612
|
-
}
|
|
41613
|
-
return animation.currentTime / duration;
|
|
41614
|
-
};
|
|
41615
|
-
|
|
41616
|
-
// Where the two pictures stand, said as a moment in the movement they would
|
|
41617
|
-
// have played on their own: the browser knows how they move (it is written in
|
|
41618
|
-
// CSS), so the finger only has to say how far in. They are held still by CSS
|
|
41619
|
-
// while this lasts, so a time written here is a place they stay at.
|
|
41620
|
-
const scrubTravel = (travel, ratio) => {
|
|
41621
|
-
for (const animation of travelAnimations(travel)) {
|
|
41622
|
-
const timing = animation.effect.getComputedTiming();
|
|
41623
|
-
const duration = timing.delay + timing.activeDuration;
|
|
41624
|
-
animation.currentTime = ratio * duration;
|
|
41625
|
-
}
|
|
40923
|
+
// How far down the row the reader is looking, for a page brought in: a page on
|
|
40924
|
+
// screen scrolls with the document, and one arriving has to start from there.
|
|
40925
|
+
const pinOf = row => {
|
|
40926
|
+
const {
|
|
40927
|
+
top
|
|
40928
|
+
} = row.getBoundingClientRect();
|
|
40929
|
+
return top < 0 ? -top : 0;
|
|
41626
40930
|
};
|
|
41627
40931
|
|
|
41628
|
-
//
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41637
|
-
|
|
41638
|
-
|
|
41639
|
-
|
|
41640
|
-
// because the two do not always happen at the same moment: a view transition
|
|
41641
|
-
// calls its update callback a frame after it is started, and the render can
|
|
41642
|
-
// land in that gap — see beginTravel, which arms this the moment the travel is
|
|
41643
|
-
// decided and hands it over.
|
|
41644
|
-
const armRouteRenderWait = () => {
|
|
41645
|
-
let stopListening;
|
|
41646
|
-
const rendered = new Promise(resolve => {
|
|
41647
|
-
stopListening = observeRouteRender(resolve);
|
|
41648
|
-
});
|
|
41649
|
-
return {
|
|
41650
|
-
rendered,
|
|
41651
|
-
stop: () => stopListening()
|
|
41652
|
-
};
|
|
41653
|
-
};
|
|
41654
|
-
const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
|
|
41655
|
-
try {
|
|
41656
|
-
await change();
|
|
41657
|
-
if (pageIsCurrent(page)) {
|
|
41658
|
-
await wait.rendered;
|
|
40932
|
+
// What a clone does not take: where what scrolls inside was left.
|
|
40933
|
+
const copyScrollPositions = (from, to) => {
|
|
40934
|
+
const fromElements = from.querySelectorAll("*");
|
|
40935
|
+
const toElements = to.querySelectorAll("*");
|
|
40936
|
+
for (let i = 0; i < fromElements.length; i++) {
|
|
40937
|
+
const {
|
|
40938
|
+
scrollTop,
|
|
40939
|
+
scrollLeft
|
|
40940
|
+
} = fromElements[i];
|
|
40941
|
+
if (scrollTop || scrollLeft) {
|
|
40942
|
+
toElements[i].scrollTop = scrollTop;
|
|
40943
|
+
toElements[i].scrollLeft = scrollLeft;
|
|
41659
40944
|
}
|
|
41660
|
-
} finally {
|
|
41661
|
-
wait.stop();
|
|
41662
40945
|
}
|
|
41663
40946
|
};
|
|
41664
40947
|
|
|
41665
|
-
// Whether the document's offset belongs to the tabs. The pages of a row scroll
|
|
41666
|
-
// the document when nothing between the box and the viewport is a scroll
|
|
41667
|
-
// container: the tab on screen is then what makes the document tall, and the
|
|
41668
|
-
// offset on it is that tab's — it has to be given back with the tab, and the
|
|
41669
|
-
// browser's clamping of it while pages are swapped has to be ignored.
|
|
41670
|
-
//
|
|
41671
|
-
// A box that lives inside a scroller of its own — a frame in an article, a
|
|
41672
|
-
// panel beside other content — shares nothing with the document: the offset
|
|
41673
|
-
// there is the surrounding page's, the reader never left it, and a travel has
|
|
41674
|
-
// no business moving it. Each of its pages brings its own scrollport, which
|
|
41675
|
-
// goes away with the page and has nothing to restore.
|
|
41676
|
-
//
|
|
41677
|
-
// includeHidden, because an `overflow: hidden` ancestor is one of those
|
|
41678
|
-
// scrollers: it shows no scrollbar and still holds an offset of its own, and
|
|
41679
|
-
// what is under it never reaches the document. A wrapper that merely clips
|
|
41680
|
-
// (`overflow: clip`) holds nothing and is walked straight through — see
|
|
41681
|
-
// is_scrollable.js.
|
|
41682
|
-
const pagesScrollTheDocument = element => {
|
|
41683
|
-
// What window.scrollTo moves. A body whose overflow is the viewport's is
|
|
41684
|
-
// walked through to it (see overflowGoesToViewport in @jsenv/dom); a body
|
|
41685
|
-
// the walk stops on scrolls itself, out of the viewport's reach.
|
|
41686
|
-
return getScrollContainer(element, {
|
|
41687
|
-
includeHidden: true
|
|
41688
|
-
}) === document.documentElement;
|
|
41689
|
-
};
|
|
41690
|
-
|
|
41691
40948
|
// A page of the row: a route, and the params that say which of its tabs when
|
|
41692
40949
|
// several of them share it. Written as a bare route by a caller whose tabs are
|
|
41693
40950
|
// routes of their own — which is the same page with nothing to tell apart.
|
|
@@ -41708,8 +40965,6 @@ const samePage = (a, b) => {
|
|
|
41708
40965
|
}
|
|
41709
40966
|
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
41710
40967
|
};
|
|
41711
|
-
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
|
|
41712
|
-
|
|
41713
40968
|
// The FIRST page that answers, as with the branches of a <Route>: several
|
|
41714
40969
|
// routes match at once — a literal one and the parameterized one it is a case of
|
|
41715
40970
|
// ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
|
|
@@ -41730,20 +40985,6 @@ const currentPageIndex = pages => {
|
|
|
41730
40985
|
return currentIndex;
|
|
41731
40986
|
};
|
|
41732
40987
|
|
|
41733
|
-
// A transition skipped by another one starting is an outcome, not a failure.
|
|
41734
|
-
const ignoreSkipped = () => {};
|
|
41735
|
-
|
|
41736
|
-
// The name is lent to the box that is travelling and taken back afterwards.
|
|
41737
|
-
// There is one transition in a document at a time, so one box wears it at a
|
|
41738
|
-
// time — and the others, unnamed, are simply not captured: they stay live
|
|
41739
|
-
// under the pictures rather than being frozen with the page.
|
|
41740
|
-
const nameForTravel = element => {
|
|
41741
|
-
element.style.viewTransitionName = TRAVEL_NAME;
|
|
41742
|
-
};
|
|
41743
|
-
const unnameAfterTravel = element => {
|
|
41744
|
-
element.style.viewTransitionName = "";
|
|
41745
|
-
};
|
|
41746
|
-
|
|
41747
40988
|
/**
|
|
41748
40989
|
* What covers the box a route movement plays in, from INSIDE the document: a
|
|
41749
40990
|
* sticky row of tabs above a `<RouteTravel>`, a header pinned to the top of a
|
|
@@ -56662,9 +55903,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
|
56662
55903
|
* press on it is aimed at what the reader can see. Say it there, and only
|
|
56663
55904
|
* there: a control that TRAVELS with the pages has a rectangle where it will
|
|
56664
55905
|
* be, not where it is seen, and a press handed to it fires something nobody
|
|
56665
|
-
* aimed at.
|
|
56666
|
-
* the part it photographs (`data-navi-fixed-bar-overhang`) is deaf for the
|
|
56667
|
-
* whole travel, and the rest for its first frames.
|
|
55906
|
+
* aimed at.
|
|
56668
55907
|
* @param {boolean} [props.replace] - Go to the destination by TAKING THE PLACE
|
|
56669
55908
|
* of the current history entry instead of stacking onto it: the link stays a
|
|
56670
55909
|
* link (an address, a middle click, the keyboard, `aria-current`), only the
|
|
@@ -57153,6 +56392,10 @@ const SlideContainerContext = createContext(null);
|
|
|
57153
56392
|
// ([data-slide-ways~="right"]).
|
|
57154
56393
|
const SLIDE_CURRENT_ATTRIBUTE = "data-slide-current";
|
|
57155
56394
|
const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
|
|
56395
|
+
// The slide --slide-travel-progress counts from, when that is not the current
|
|
56396
|
+
// one: a hand on a row the browser scrolls can carry it past several slides, and
|
|
56397
|
+
// what follows it goes on between the two it is standing between.
|
|
56398
|
+
const SLIDE_BASE_ATTRIBUTE = "data-slide-travel-base";
|
|
57156
56399
|
// Every way out that would DO something right now, in the words the commands
|
|
57157
56400
|
// use: a direction ("left", "right", "up", "down") when there is a slide that
|
|
57158
56401
|
// way, plus "first" and "last" when one is not already standing at that end of
|
|
@@ -57390,11 +56633,15 @@ const css$_ = /* css */`@layer navi {
|
|
|
57390
56633
|
}
|
|
57391
56634
|
}
|
|
57392
56635
|
|
|
56636
|
+
.navi_nav[data-nav-following-row] .navi_current_indicator {
|
|
56637
|
+
visibility: hidden;
|
|
56638
|
+
}
|
|
56639
|
+
|
|
57393
56640
|
.navi_nav .navi_link[data-href-current] .navi_current_indicator {
|
|
57394
56641
|
view-transition-name: var(--nav-indicator-name);
|
|
57395
56642
|
}
|
|
57396
56643
|
|
|
57397
|
-
:root
|
|
56644
|
+
:root[data-navi-route-transition] .navi_nav:not([data-nav-between-tabs]) .navi_current_indicator {
|
|
57398
56645
|
view-transition-name: none;
|
|
57399
56646
|
}
|
|
57400
56647
|
|
|
@@ -57525,6 +56772,64 @@ const NavStyleCSSVars = {
|
|
|
57525
56772
|
currentIndicatorColor: "--nav-current-indicator-color",
|
|
57526
56773
|
currentIndicatorSize: ["--nav-current-indicator-size", "height"]
|
|
57527
56774
|
};
|
|
56775
|
+
|
|
56776
|
+
// Where the bar is, where it is headed and in which colour at each end: the
|
|
56777
|
+
// numbers the CSS above interpolates between. Measured off the DOM (offsets force
|
|
56778
|
+
// a layout, the colours a style resolve), so asked only when the two tabs change.
|
|
56779
|
+
const writeIndicatorGeometry = (navElement, {
|
|
56780
|
+
baseTabElement,
|
|
56781
|
+
towardTabElement,
|
|
56782
|
+
towardIsAfter,
|
|
56783
|
+
vertical
|
|
56784
|
+
}) => {
|
|
56785
|
+
const measure = tabElement => vertical ? {
|
|
56786
|
+
position: tabElement.offsetTop,
|
|
56787
|
+
length: tabElement.offsetHeight
|
|
56788
|
+
} : {
|
|
56789
|
+
position: tabElement.offsetLeft,
|
|
56790
|
+
length: tabElement.offsetWidth
|
|
56791
|
+
};
|
|
56792
|
+
// The colour a tab asks the bar to be under it. Read from the tab and not
|
|
56793
|
+
// from the row, so a tab that says nothing gives the row's value back —
|
|
56794
|
+
// it inherits it (see the css above) — and one that says something gives
|
|
56795
|
+
// its own. Empty when neither resolves to anything: the css then falls
|
|
56796
|
+
// back on its own, rather than being handed a colour that is not one.
|
|
56797
|
+
const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
|
|
56798
|
+
const writeColor = (property, color) => {
|
|
56799
|
+
if (color) {
|
|
56800
|
+
navElement.style.setProperty(property, color);
|
|
56801
|
+
} else {
|
|
56802
|
+
navElement.style.removeProperty(property);
|
|
56803
|
+
}
|
|
56804
|
+
};
|
|
56805
|
+
const baseMeasure = measure(baseTabElement);
|
|
56806
|
+
let positionDelta = 0;
|
|
56807
|
+
let lengthDelta = 0;
|
|
56808
|
+
let towardSign = 0;
|
|
56809
|
+
let towardColor = "";
|
|
56810
|
+
if (towardTabElement) {
|
|
56811
|
+
const towardMeasure = measure(towardTabElement);
|
|
56812
|
+
// What one box of travel is worth in pixels of this row, signed so that
|
|
56813
|
+
// the trait is exactly on the other tab when the progress is at its own
|
|
56814
|
+
// end: the container counts +1 when the picture leans on a slide sitting
|
|
56815
|
+
// BEFORE the current one and -1 when it sits after.
|
|
56816
|
+
const sign = towardIsAfter ? -1 : 1;
|
|
56817
|
+
positionDelta = (towardMeasure.position - baseMeasure.position) * sign;
|
|
56818
|
+
lengthDelta = (towardMeasure.length - baseMeasure.length) * sign;
|
|
56819
|
+
towardSign = sign;
|
|
56820
|
+
towardColor = colorOf(towardTabElement);
|
|
56821
|
+
}
|
|
56822
|
+
const {
|
|
56823
|
+
style
|
|
56824
|
+
} = navElement;
|
|
56825
|
+
style.setProperty("--nav-indicator-position", baseMeasure.position);
|
|
56826
|
+
style.setProperty("--nav-indicator-length", baseMeasure.length);
|
|
56827
|
+
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
56828
|
+
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
56829
|
+
style.setProperty("--nav-indicator-toward-sign", towardSign);
|
|
56830
|
+
writeColor("--nav-indicator-color", colorOf(baseTabElement));
|
|
56831
|
+
writeColor("--nav-indicator-color-toward", towardColor);
|
|
56832
|
+
};
|
|
57528
56833
|
const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
57529
56834
|
if (currentIndicator === true) {
|
|
57530
56835
|
return vertical ? "left" : "bottom";
|
|
@@ -57597,6 +56902,8 @@ const Nav = ({
|
|
|
57597
56902
|
const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
|
|
57598
56903
|
const slideContainerElementRef = useRef(null);
|
|
57599
56904
|
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
56905
|
+
// A row of routes draws that same bar only while it follows a row scrolling.
|
|
56906
|
+
const rowIndicatorPosition = slideContainer ? null : positionOfCurrentIndicator(currentIndicator, vertical);
|
|
57600
56907
|
|
|
57601
56908
|
// Which tab of this row is the one being shown: the slide the container says
|
|
57602
56909
|
// it is on, or, for a row of routes, the link the browser is on.
|
|
@@ -57609,7 +56916,9 @@ const Nav = ({
|
|
|
57609
56916
|
if (!containerElement) {
|
|
57610
56917
|
return navElement.querySelector("[data-href-current]");
|
|
57611
56918
|
}
|
|
57612
|
-
|
|
56919
|
+
// The tab the trait counts from: the current one, unless a hand has carried
|
|
56920
|
+
// the row past it.
|
|
56921
|
+
const currentArea = containerElement.getAttribute(SLIDE_BASE_ATTRIBUTE) ?? containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
57613
56922
|
if (currentArea === null) {
|
|
57614
56923
|
return null;
|
|
57615
56924
|
}
|
|
@@ -57669,56 +56978,13 @@ const Nav = ({
|
|
|
57669
56978
|
tabCount: tabElements.length,
|
|
57670
56979
|
vertical
|
|
57671
56980
|
};
|
|
57672
|
-
const
|
|
57673
|
-
|
|
57674
|
-
|
|
57675
|
-
|
|
57676
|
-
|
|
57677
|
-
|
|
57678
|
-
};
|
|
57679
|
-
// The colour a tab asks the bar to be under it. Read from the tab and not
|
|
57680
|
-
// from the row, so a tab that says nothing gives the row's value back —
|
|
57681
|
-
// it inherits it (see the css above) — and one that says something gives
|
|
57682
|
-
// its own. Empty when neither resolves to anything: the css then falls
|
|
57683
|
-
// back on its own, rather than being handed a colour that is not one.
|
|
57684
|
-
const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
|
|
57685
|
-
const writeColor = (property, color) => {
|
|
57686
|
-
if (color) {
|
|
57687
|
-
navElement.style.setProperty(property, color);
|
|
57688
|
-
} else {
|
|
57689
|
-
navElement.style.removeProperty(property);
|
|
57690
|
-
}
|
|
57691
|
-
};
|
|
57692
|
-
const currentMeasure = measure(tabElements[currentIndex]);
|
|
57693
|
-
const towardArea = towardAreaNow;
|
|
57694
|
-
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
57695
|
-
let positionDelta = 0;
|
|
57696
|
-
let lengthDelta = 0;
|
|
57697
|
-
let towardSign = 0;
|
|
57698
|
-
let towardColor = "";
|
|
57699
|
-
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
57700
|
-
const towardTabElement = tabElements[towardIndex];
|
|
57701
|
-
const towardMeasure = measure(towardTabElement);
|
|
57702
|
-
// What one box of travel is worth in pixels of this row, signed so that
|
|
57703
|
-
// the trait is exactly on the other tab when the progress is at its own
|
|
57704
|
-
// end: the container counts +1 when the picture leans on a slide sitting
|
|
57705
|
-
// BEFORE the current one and -1 when it sits after.
|
|
57706
|
-
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
57707
|
-
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
57708
|
-
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
57709
|
-
towardSign = sign;
|
|
57710
|
-
towardColor = colorOf(towardTabElement);
|
|
57711
|
-
}
|
|
57712
|
-
const {
|
|
57713
|
-
style
|
|
57714
|
-
} = navElement;
|
|
57715
|
-
style.setProperty("--nav-indicator-position", currentMeasure.position);
|
|
57716
|
-
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
57717
|
-
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
57718
|
-
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
57719
|
-
style.setProperty("--nav-indicator-toward-sign", towardSign);
|
|
57720
|
-
writeColor("--nav-indicator-color", colorOf(tabElements[currentIndex]));
|
|
57721
|
-
writeColor("--nav-indicator-color-toward", towardColor);
|
|
56981
|
+
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardAreaNow);
|
|
56982
|
+
writeIndicatorGeometry(navElement, {
|
|
56983
|
+
baseTabElement: tabElements[currentIndex],
|
|
56984
|
+
towardTabElement: towardIndex !== -1 && towardIndex !== currentIndex ? tabElements[towardIndex] : null,
|
|
56985
|
+
towardIsAfter: towardIndex > currentIndex,
|
|
56986
|
+
vertical
|
|
56987
|
+
});
|
|
57722
56988
|
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
57723
56989
|
};
|
|
57724
56990
|
// Reached through a ref by everything watching the DOM below: those watchers
|
|
@@ -57726,6 +56992,60 @@ const Nav = ({
|
|
|
57726
56992
|
// about the row as it is now.
|
|
57727
56993
|
const paintIndicatorGeometryRef = useRef(null);
|
|
57728
56994
|
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
56995
|
+
|
|
56996
|
+
// The bar following a row of routes the browser scrolls, between the two tabs
|
|
56997
|
+
// of the pages it stands between. A row this nav holds no tab for is another
|
|
56998
|
+
// nav's to draw.
|
|
56999
|
+
useLayoutEffect(() => {
|
|
57000
|
+
if (!rowIndicatorPosition) {
|
|
57001
|
+
return undefined;
|
|
57002
|
+
}
|
|
57003
|
+
let paintedBase = null;
|
|
57004
|
+
let paintedToward = null;
|
|
57005
|
+
const letGo = () => {
|
|
57006
|
+
const navElement = navRef.current;
|
|
57007
|
+
paintedBase = null;
|
|
57008
|
+
paintedToward = null;
|
|
57009
|
+
navElement?.removeAttribute("data-nav-following-row");
|
|
57010
|
+
navElement?.removeAttribute("data-nav-indicator-measured");
|
|
57011
|
+
navElement?.style.removeProperty("--slide-travel-progress");
|
|
57012
|
+
};
|
|
57013
|
+
const stopFollowing = followRouteRows(move => {
|
|
57014
|
+
const navElement = navRef.current;
|
|
57015
|
+
if (!navElement) {
|
|
57016
|
+
return;
|
|
57017
|
+
}
|
|
57018
|
+
if (!move) {
|
|
57019
|
+
letGo();
|
|
57020
|
+
return;
|
|
57021
|
+
}
|
|
57022
|
+
const tabElements = Array.from(navElement.querySelectorAll(".navi_link"));
|
|
57023
|
+
const tabOf = path => tabElements.find(tabElement => tabElement.pathname === path);
|
|
57024
|
+
const baseTabElement = tabOf(move.basePath);
|
|
57025
|
+
if (!baseTabElement) {
|
|
57026
|
+
letGo();
|
|
57027
|
+
return;
|
|
57028
|
+
}
|
|
57029
|
+
const towardTabElement = move.towardPath ? tabOf(move.towardPath) : null;
|
|
57030
|
+
if (baseTabElement !== paintedBase || towardTabElement !== paintedToward) {
|
|
57031
|
+
paintedBase = baseTabElement;
|
|
57032
|
+
paintedToward = towardTabElement;
|
|
57033
|
+
writeIndicatorGeometry(navElement, {
|
|
57034
|
+
baseTabElement,
|
|
57035
|
+
towardTabElement,
|
|
57036
|
+
towardIsAfter: tabElements.indexOf(towardTabElement) > tabElements.indexOf(baseTabElement),
|
|
57037
|
+
vertical
|
|
57038
|
+
});
|
|
57039
|
+
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
57040
|
+
navElement.setAttribute("data-nav-following-row", "");
|
|
57041
|
+
}
|
|
57042
|
+
navElement.style.setProperty("--slide-travel-progress", move.progress);
|
|
57043
|
+
});
|
|
57044
|
+
return () => {
|
|
57045
|
+
stopFollowing();
|
|
57046
|
+
letGo();
|
|
57047
|
+
};
|
|
57048
|
+
}, [rowIndicatorPosition, vertical]);
|
|
57729
57049
|
// What the last paint measured for — see the memo at the top of
|
|
57730
57050
|
// paintIndicatorGeometry.
|
|
57731
57051
|
const indicatorPaintedForRef = useRef(null);
|
|
@@ -58084,7 +57404,7 @@ const Nav = ({
|
|
|
58084
57404
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
58085
57405
|
"data-vertical": vertical ? "" : undefined,
|
|
58086
57406
|
"data-panel-position": panelPosition,
|
|
58087
|
-
"data-nav-indicator": indicatorPosition ?? undefined
|
|
57407
|
+
"data-nav-indicator": indicatorPosition ?? rowIndicatorPosition ?? undefined
|
|
58088
57408
|
// "write your travel here too": a custom property cannot be read across
|
|
58089
57409
|
// the DOM, so the container paints its progress onto this element and the
|
|
58090
57410
|
// trait follows in CSS alone (see SlideContainer's followerElements).
|
|
@@ -58108,7 +57428,7 @@ const Nav = ({
|
|
|
58108
57428
|
"--nav-indicator-name": indicatorNameRef.current
|
|
58109
57429
|
} : props.style,
|
|
58110
57430
|
styleCSSVars: NavStyleCSSVars,
|
|
58111
|
-
children: [indicatorPosition && jsx("span", {
|
|
57431
|
+
children: [(indicatorPosition || rowIndicatorPosition) && jsx("span", {
|
|
58112
57432
|
className: "navi_nav_indicator"
|
|
58113
57433
|
}), jsx(NavContext.Provider, {
|
|
58114
57434
|
value: navContextValue,
|
|
@@ -59302,17 +58622,6 @@ const css$Y = /* css */`@layer navi {
|
|
|
59302
58622
|
left: calc(-1 * var(--navi-fixed-bar-border-width));
|
|
59303
58623
|
clip-path: inset(0 0 0 -100vw);
|
|
59304
58624
|
}
|
|
59305
|
-
|
|
59306
|
-
@supports (view-transition-name: match-element) {
|
|
59307
|
-
:root[data-navi-route-travel] .navi_fixed_bar_shadow, :root[data-navi-route-travel] [data-navi-fixed-bar-overhang] {
|
|
59308
|
-
view-transition-name: match-element;
|
|
59309
|
-
view-transition-class: navi_fixed_bar_over_pages;
|
|
59310
|
-
}
|
|
59311
|
-
}
|
|
59312
|
-
|
|
59313
|
-
:root[data-navi-route-travel-turned]::view-transition-group(.navi_fixed_bar_over_pages) {
|
|
59314
|
-
display: block;
|
|
59315
|
-
}
|
|
59316
58625
|
`;
|
|
59317
58626
|
const FixedBarStyleCSSVars = {
|
|
59318
58627
|
// width/height go through the vars the size calc reads: written as plain
|
|
@@ -59364,16 +58673,7 @@ const FixedBarStyleCSSVars = {
|
|
|
59364
58673
|
* `height`/`width`.
|
|
59365
58674
|
* @param {string} [props.shadow] - What the bar casts over the content, in
|
|
59366
58675
|
* `box-shadow` syntax, cut at the bar's box on every side but the content's.
|
|
59367
|
-
* Takes no room.
|
|
59368
|
-
* are covered by the pages while a `RouteTravel` plays, this one stays over
|
|
59369
|
-
* them.
|
|
59370
|
-
*
|
|
59371
|
-
* A child standing out of the bar's box (a raised button, the notch around it)
|
|
59372
|
-
* wears `data-navi-fixed-bar-overhang`: unmarked, the pages of a `RouteTravel`
|
|
59373
|
-
* cover it wherever they have paint behind it. Marked, it stays over them, and
|
|
59374
|
-
* like anything photographed it is deaf for the length of the travel —
|
|
59375
|
-
* `pressableDuringRouteTransition` on a control inside it keeps that control
|
|
59376
|
-
* answering. The rest of the bar stays live.
|
|
58676
|
+
* Takes no room.
|
|
59377
58677
|
*/
|
|
59378
58678
|
const FixedBar = ({
|
|
59379
58679
|
children,
|
|
@@ -62627,9 +61927,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
62627
61927
|
* button in a bar the two states share, standing exactly where its picture is
|
|
62628
61928
|
* drawn — must still close it. What `<Link pressableDuringRouteTransition>`
|
|
62629
61929
|
* says, for the other half of a toggle. Only for a control that does not
|
|
62630
|
-
* travel with the pages.
|
|
62631
|
-
* a `FixedBar`: the part it photographs (`data-navi-fixed-bar-overhang`) is
|
|
62632
|
-
* deaf for the whole travel, and the rest for its first frames.
|
|
61930
|
+
* travel with the pages.
|
|
62633
61931
|
* @param {Function} [action] What the press runs: a button holds no value, so
|
|
62634
61932
|
* every press is one call. Inside a `<Form>` or a group it carries the value
|
|
62635
61933
|
* around it, merged over the action's bound params — a value that is not an
|
|
@@ -64772,6 +64070,16 @@ const css$M = /* css */`@layer navi {
|
|
|
64772
64070
|
}
|
|
64773
64071
|
}
|
|
64774
64072
|
|
|
64073
|
+
@media (pointer: coarse) {
|
|
64074
|
+
& .navi_control_input {
|
|
64075
|
+
overflow: clip;
|
|
64076
|
+
|
|
64077
|
+
&::-webkit-slider-thumb {
|
|
64078
|
+
transform: scaleX(3);
|
|
64079
|
+
}
|
|
64080
|
+
}
|
|
64081
|
+
}
|
|
64082
|
+
|
|
64775
64083
|
& .navi_input_range_accent_probe {
|
|
64776
64084
|
background-color: var(--accent-color);
|
|
64777
64085
|
visibility: hidden;
|
|
@@ -66539,7 +65847,9 @@ const InputPseudoElements = ["::-navi-loader"];
|
|
|
66539
65847
|
* only: nothing moves and nothing is drawn. It overflows the range, centered
|
|
66540
65848
|
* on its line, over whatever is next to it and takes the presses aimed there,
|
|
66541
65849
|
* so leave it the room it needs (`marginY`). Different for a mouse and a
|
|
66542
|
-
* finger: `--press-height` in a `@media (pointer: coarse)` rule.
|
|
65850
|
+
* finger: `--press-height` in a `@media (pointer: coarse)` rule. On Safari for
|
|
65851
|
+
* iOS a drag only starts close to the thumb, within three times its width; a
|
|
65852
|
+
* tap further along the line moves the value.
|
|
66543
65853
|
*/
|
|
66544
65854
|
const Input = props => {
|
|
66545
65855
|
resolveInputProps(props);
|
|
@@ -68108,26 +67418,47 @@ const css$I = /* css */`@property --slide-travel-progress {
|
|
|
68108
67418
|
|
|
68109
67419
|
.navi_slide_container {
|
|
68110
67420
|
border-radius: inherit;
|
|
67421
|
+
overflow-anchor: none;
|
|
67422
|
+
scroll-snap-type: both mandatory;
|
|
67423
|
+
scrollbar-width: none;
|
|
68111
67424
|
flex: 0 auto;
|
|
68112
67425
|
min-width: 0;
|
|
68113
67426
|
min-height: 0;
|
|
68114
67427
|
display: grid;
|
|
68115
67428
|
position: relative;
|
|
68116
|
-
overflow:
|
|
67429
|
+
overflow: hidden;
|
|
67430
|
+
|
|
67431
|
+
&::-webkit-scrollbar {
|
|
67432
|
+
display: none;
|
|
67433
|
+
}
|
|
67434
|
+
|
|
67435
|
+
&[data-slide-travelling] {
|
|
67436
|
+
scroll-snap-type: none;
|
|
67437
|
+
}
|
|
67438
|
+
|
|
67439
|
+
&[data-slide-hand*="x"] {
|
|
67440
|
+
overscroll-behavior-x: contain;
|
|
67441
|
+
overflow-x: auto;
|
|
67442
|
+
}
|
|
67443
|
+
|
|
67444
|
+
&[data-slide-hand*="y"] {
|
|
67445
|
+
overscroll-behavior-y: contain;
|
|
67446
|
+
overflow-y: auto;
|
|
67447
|
+
}
|
|
68117
67448
|
|
|
68118
67449
|
&:focus {
|
|
68119
67450
|
outline: none;
|
|
68120
67451
|
}
|
|
68121
67452
|
|
|
68122
|
-
&[data-
|
|
67453
|
+
&[data-slide-no-touch="x"] {
|
|
68123
67454
|
touch-action: pan-y;
|
|
68124
67455
|
}
|
|
68125
67456
|
|
|
68126
|
-
&[data-
|
|
67457
|
+
&[data-slide-no-touch="y"] {
|
|
68127
67458
|
touch-action: pan-x;
|
|
68128
67459
|
}
|
|
68129
67460
|
|
|
68130
|
-
&[data-
|
|
67461
|
+
&[data-slide-no-touch="xy"] {
|
|
68131
67462
|
touch-action: none;
|
|
68132
67463
|
}
|
|
68133
67464
|
|
|
@@ -68147,15 +67478,19 @@ const css$I = /* css */`@property --slide-travel-progress {
|
|
|
68147
67478
|
display: grid;
|
|
68148
67479
|
|
|
68149
67480
|
& > [data-slide] {
|
|
68150
|
-
|
|
68151
|
-
|
|
68152
|
-
|
|
67481
|
+
top: var(--slide-top, 0);
|
|
67482
|
+
left: var(--slide-left, 0);
|
|
67483
|
+
scroll-snap-align: start;
|
|
67484
|
+
scroll-snap-stop: always;
|
|
68153
67485
|
border-radius: 0;
|
|
68154
67486
|
outline: none;
|
|
68155
67487
|
grid-area: 1 / 1;
|
|
67488
|
+
min-width: 0;
|
|
67489
|
+
min-height: 0;
|
|
67490
|
+
position: relative;
|
|
68156
67491
|
}
|
|
68157
67492
|
|
|
68158
|
-
& > [data-slide][data-slide-offstage] {
|
|
67493
|
+
& > [data-slide][data-slide-offstage], & > [data-slide][data-off-row] {
|
|
68159
67494
|
visibility: hidden;
|
|
68160
67495
|
}
|
|
68161
67496
|
}
|
|
@@ -68185,22 +67520,15 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
|
|
|
68185
67520
|
// Where the track stands right now, in pixels, read off the box it draws in
|
|
68186
67521
|
// rather than off the value that moves it: mid-animation the browser reports
|
|
68187
67522
|
// that value as a calc() of a percentage and a length ("calc(-43% - 119px)"),
|
|
68188
|
-
// which no simple parse survives
|
|
68189
|
-
//
|
|
67523
|
+
// which no simple parse survives. Against the box the track rests in, past the
|
|
67524
|
+
// border (clientLeft/clientTop), and plus what the box has scrolled, which the
|
|
67525
|
+
// track's rectangle carries and its translate does not.
|
|
68190
67526
|
const trackOffsetPx = (track, containerElement) => {
|
|
68191
67527
|
const trackRect = track.getBoundingClientRect();
|
|
68192
67528
|
const containerRect = containerElement.getBoundingClientRect();
|
|
68193
|
-
// Against the box the track RESTS in — the content box, past the border
|
|
68194
|
-
// (clientLeft/clientTop are the border widths; the box never has a
|
|
68195
|
-
// scrollbar nor padding, see the top-of-file comment on padding). Measured
|
|
68196
|
-
// against the border box, a border reads as that many pixels of translate
|
|
68197
|
-
// nothing ever asked for — and it COMPOUNDS: every travel interrupted
|
|
68198
|
-
// mid-flight re-measures the track and carries the error into its "from",
|
|
68199
|
-
// so rapid back-and-forth gestures stack a 1px border into a visible
|
|
68200
|
-
// vertical drift of the whole track.
|
|
68201
67529
|
return {
|
|
68202
|
-
x: trackRect.x - containerRect.x - containerElement.clientLeft,
|
|
68203
|
-
y: trackRect.y - containerRect.y - containerElement.clientTop
|
|
67530
|
+
x: trackRect.x - containerRect.x - containerElement.clientLeft + containerElement.scrollLeft,
|
|
67531
|
+
y: trackRect.y - containerRect.y - containerElement.clientTop + containerElement.scrollTop
|
|
68204
67532
|
};
|
|
68205
67533
|
};
|
|
68206
67534
|
const offsetToPx = (offset, box) => {
|
|
@@ -68377,7 +67705,7 @@ const causeOfEvent = event => {
|
|
|
68377
67705
|
const {
|
|
68378
67706
|
type
|
|
68379
67707
|
} = event;
|
|
68380
|
-
if (type === "
|
|
67708
|
+
if (type === "scrollend") {
|
|
68381
67709
|
return "drag";
|
|
68382
67710
|
}
|
|
68383
67711
|
if (type === "keydown" || type === "keyup") {
|
|
@@ -68602,19 +67930,19 @@ const areaHeldOn = slideElement => {
|
|
|
68602
67930
|
* go in it), so the surface says `data-slide-container-follows={id}` and the
|
|
68603
67931
|
* arrows keep walking wherever the keyboard is in it. See the paragraph above
|
|
68604
67932
|
* about what is drawn AROUND the travel.
|
|
68605
|
-
* @param {boolean|"x"|"y"|"xy"} [props.travelByDrag=true] - whether a
|
|
68606
|
-
*
|
|
68607
|
-
*
|
|
68608
|
-
*
|
|
68609
|
-
*
|
|
68610
|
-
* @param {boolean|"x"|"y"|"xy"} [props.travelByScroll="x"] - whether a wheel
|
|
68611
|
-
* pushing the box travels
|
|
68612
|
-
*
|
|
68613
|
-
*
|
|
68614
|
-
*
|
|
68615
|
-
*
|
|
68616
|
-
*
|
|
68617
|
-
*
|
|
67933
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByDrag=true] - whether a finger or
|
|
67934
|
+
* a pen pushing the slides travels: the browser pans them, a mouse does not.
|
|
67935
|
+
* Off where the gesture belongs to the content (a canvas one draws on, a map
|
|
67936
|
+
* one pans), or where the slides are steps of a form rather than a row one
|
|
67937
|
+
* browses.
|
|
67938
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByScroll="x"] - whether a wheel or a
|
|
67939
|
+
* trackpad pushing the box travels. Sideways only by default, because a page is
|
|
67940
|
+
* hardly ever scrollable that way: a sideways scroll over the box can only have
|
|
67941
|
+
* been meant for the box. Down the page it is the page's OWN gesture, handed on
|
|
67942
|
+
* to what scrolls around the box, so a map that travels vertically has to be
|
|
67943
|
+
* told (`travelByScroll` / `"y"` / `"xy"`) before a wheel moves it.
|
|
67944
|
+
* @param {string} [props.duration="300ms"] - how long a slide change asked for
|
|
67945
|
+
* takes: a tab, a key, a command, a state. A hand goes at its own pace.
|
|
68618
67946
|
* @param {"largest"|"frozen"} [props.sizing="largest"] - what decides the size
|
|
68619
67947
|
* of the box. "largest": the largest slide, at every moment — the box follows
|
|
68620
67948
|
* whatever the slides do. "frozen": the box is measured once and kept at that
|
|
@@ -68733,10 +68061,6 @@ const SlideContainer = ({
|
|
|
68733
68061
|
// one being travelled TO while the track moves. What the next travel departs
|
|
68734
68062
|
// from, because it is what one is looking at.
|
|
68735
68063
|
const drawnAreaRef = useRef(undefined);
|
|
68736
|
-
// The movement the track is playing, while it plays: the slide it is leaving
|
|
68737
|
-
// and the slide it is going to. A hand landing on the box takes it over from
|
|
68738
|
-
// its origin (see onStart in createTravelHandlers).
|
|
68739
|
-
const movingRef = useRef(null);
|
|
68740
68064
|
// Which way the travel about to be drawn goes, when whatever asked for it
|
|
68741
68065
|
// knows: a window stepping off its last slide comes back on its first, and
|
|
68742
68066
|
// only the press says that is a step forward — the map, read between those
|
|
@@ -68750,17 +68074,6 @@ const SlideContainer = ({
|
|
|
68750
68074
|
// A focus transfer read off the interaction that asked for the travel, kept
|
|
68751
68075
|
// until the slide it is meant for holds its final DOM (see handOverFocus).
|
|
68752
68076
|
const focusHandOverRef = useRef(null);
|
|
68753
|
-
// The gesture in hand, from the pointer that started it to the slides it
|
|
68754
|
-
// brought alongside. Null when no finger is on the box.
|
|
68755
|
-
const dragRef = useRef(null);
|
|
68756
|
-
// Where the travel about to be drawn departs from, when that is not where the
|
|
68757
|
-
// track rests: a slide let go of halfway carries on from under the finger.
|
|
68758
|
-
// Read and dropped by the layout effect, which is the one drawing it.
|
|
68759
|
-
const travelFromRef = useRef(null);
|
|
68760
|
-
// The same fact for the indicator (--slide-travel-progress): how far the
|
|
68761
|
-
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
68762
|
-
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
68763
|
-
const travelProgressFromRef = useRef(null);
|
|
68764
68077
|
// One per element painting the progress: the box, plus everything following
|
|
68765
68078
|
// it (see followerElementsRef). All started together and with the same
|
|
68766
68079
|
// options, so they are one movement said in several places.
|
|
@@ -68771,6 +68084,23 @@ const SlideContainer = ({
|
|
|
68771
68084
|
// draw in CSS alone, at the pace of the travel and under the finger, with
|
|
68772
68085
|
// nothing measured per frame.
|
|
68773
68086
|
const followerElementsRef = useRef([]);
|
|
68087
|
+
// Whether a hand has the row away from the current slide. A render meanwhile
|
|
68088
|
+
// must not send it back.
|
|
68089
|
+
const handMovingRef = useRef(false);
|
|
68090
|
+
// The slide a hand has just landed on, for the render it asks for: it is
|
|
68091
|
+
// already there, so there is nothing to play.
|
|
68092
|
+
const handLandedRef = useRef(null);
|
|
68093
|
+
// Where the box rests on the row, and what the row starts from: a slide that
|
|
68094
|
+
// holds on to the user ends the row on that side, and a scroll range starts at
|
|
68095
|
+
// its first reachable slide (see writeRowAtRest).
|
|
68096
|
+
const restPlaceRef = useRef({
|
|
68097
|
+
x: 0,
|
|
68098
|
+
y: 0
|
|
68099
|
+
});
|
|
68100
|
+
const originRef = useRef({
|
|
68101
|
+
x: 0,
|
|
68102
|
+
y: 0
|
|
68103
|
+
});
|
|
68774
68104
|
const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
|
|
68775
68105
|
const current = rollingArea ?? provisionalArea ?? currentAreaState;
|
|
68776
68106
|
// The area of this box a value held outside is shown on: the slide of that
|
|
@@ -69022,10 +68352,10 @@ const SlideContainer = ({
|
|
|
69022
68352
|
// (areaTowards knows the holes and a loop's two ends).
|
|
69023
68353
|
const areasAlongside = area => {
|
|
69024
68354
|
const alongside = [];
|
|
69025
|
-
if (
|
|
68355
|
+
if (travelAxes?.includes("x")) {
|
|
69026
68356
|
alongside.push(areaTowards(-1, 0, area), areaTowards(1, 0, area));
|
|
69027
68357
|
}
|
|
69028
|
-
if (
|
|
68358
|
+
if (travelAxes?.includes("y")) {
|
|
69029
68359
|
alongside.push(areaTowards(0, -1, area), areaTowards(0, 1, area));
|
|
69030
68360
|
}
|
|
69031
68361
|
return alongside;
|
|
@@ -69054,13 +68384,87 @@ const SlideContainer = ({
|
|
|
69054
68384
|
return added.length ? [...previous, ...added] : previous;
|
|
69055
68385
|
});
|
|
69056
68386
|
};
|
|
69057
|
-
|
|
69058
|
-
|
|
69059
|
-
|
|
69060
|
-
|
|
69061
|
-
|
|
69062
|
-
|
|
69063
|
-
|
|
68387
|
+
const writeSlidePlace = (slideElement, {
|
|
68388
|
+
x,
|
|
68389
|
+
y
|
|
68390
|
+
}) => {
|
|
68391
|
+
slideElement.style.setProperty("--slide-left", `${x * 100}%`);
|
|
68392
|
+
slideElement.style.setProperty("--slide-top", `${y * 100}%`);
|
|
68393
|
+
};
|
|
68394
|
+
// Every slide where the map says it is, except what the slide on screen holds
|
|
68395
|
+
// the user from: stacked under it, so the row ends there on that side. The same
|
|
68396
|
+
// rule as the gate in goToArea. The row starts at its first reachable slide,
|
|
68397
|
+
// since a scroll range starts at 0 and what lies before cannot be scrolled to.
|
|
68398
|
+
const writeRowAtRest = (slideElements, placeOf, currentElement) => {
|
|
68399
|
+
const currentPlace = placeOf.get(readArea(currentElement)) || {
|
|
68400
|
+
x: 0,
|
|
68401
|
+
y: 0
|
|
68402
|
+
};
|
|
68403
|
+
const holdsNext = currentElement.hasAttribute("data-prevent-nav-next");
|
|
68404
|
+
const holdsPrevious = currentElement.hasAttribute("data-prevent-nav-previous");
|
|
68405
|
+
const isOutOfReach = ({
|
|
68406
|
+
x,
|
|
68407
|
+
y
|
|
68408
|
+
}) => {
|
|
68409
|
+
if (x === currentPlace.x && y === currentPlace.y) {
|
|
68410
|
+
return false;
|
|
68411
|
+
}
|
|
68412
|
+
const forward = y === currentPlace.y ? x > currentPlace.x : y > currentPlace.y;
|
|
68413
|
+
return forward ? holdsNext : holdsPrevious;
|
|
68414
|
+
};
|
|
68415
|
+
const origin = {
|
|
68416
|
+
x: currentPlace.x,
|
|
68417
|
+
y: currentPlace.y
|
|
68418
|
+
};
|
|
68419
|
+
for (const slideElement of slideElements) {
|
|
68420
|
+
const place = placeOf.get(readArea(slideElement)) || {
|
|
68421
|
+
x: 0,
|
|
68422
|
+
y: 0
|
|
68423
|
+
};
|
|
68424
|
+
if (isOutOfReach(place)) {
|
|
68425
|
+
continue;
|
|
68426
|
+
}
|
|
68427
|
+
if (place.x < origin.x) {
|
|
68428
|
+
origin.x = place.x;
|
|
68429
|
+
}
|
|
68430
|
+
if (place.y < origin.y) {
|
|
68431
|
+
origin.y = place.y;
|
|
68432
|
+
}
|
|
68433
|
+
}
|
|
68434
|
+
const rest = {
|
|
68435
|
+
x: currentPlace.x - origin.x,
|
|
68436
|
+
y: currentPlace.y - origin.y
|
|
68437
|
+
};
|
|
68438
|
+
for (const slideElement of slideElements) {
|
|
68439
|
+
const place = placeOf.get(readArea(slideElement)) || {
|
|
68440
|
+
x: 0,
|
|
68441
|
+
y: 0
|
|
68442
|
+
};
|
|
68443
|
+
const outOfReach = isOutOfReach(place);
|
|
68444
|
+
slideElement.toggleAttribute("data-off-row", outOfReach);
|
|
68445
|
+
writeSlidePlace(slideElement, outOfReach ? rest : {
|
|
68446
|
+
x: place.x - origin.x,
|
|
68447
|
+
y: place.y - origin.y
|
|
68448
|
+
});
|
|
68449
|
+
}
|
|
68450
|
+
originRef.current = origin;
|
|
68451
|
+
restPlaceRef.current = rest;
|
|
68452
|
+
};
|
|
68453
|
+
const sendRowToRest = currentElement => {
|
|
68454
|
+
const containerEl = containerRef.current;
|
|
68455
|
+
if (!restsOn(containerEl, currentElement)) {
|
|
68456
|
+
containerEl.scrollTo({
|
|
68457
|
+
left: currentElement.offsetLeft,
|
|
68458
|
+
top: currentElement.offsetTop,
|
|
68459
|
+
behavior: "instant"
|
|
68460
|
+
});
|
|
68461
|
+
}
|
|
68462
|
+
};
|
|
68463
|
+
// The travel is over: the stage is struck, every slide goes back where the map
|
|
68464
|
+
// says it is and the scroll position takes over from the track. Nothing is seen
|
|
68465
|
+
// moving for it, the slide on screen standing at the same place either way, as
|
|
68466
|
+
// long as it is all written in one go, which is why this is done here rather
|
|
68467
|
+
// than left to a render.
|
|
69064
68468
|
const settleTravel = () => {
|
|
69065
68469
|
const track = trackRef.current;
|
|
69066
68470
|
if (!track) {
|
|
@@ -69076,33 +68480,19 @@ const SlideContainer = ({
|
|
|
69076
68480
|
}
|
|
69077
68481
|
stageRef.current = null;
|
|
69078
68482
|
trackAnimationRef.current = null;
|
|
69079
|
-
movingRef.current = null;
|
|
69080
68483
|
// At rest the picture IS the current slide, whatever the last gesture wrote
|
|
69081
68484
|
// there: an indicator has nothing left to lean towards.
|
|
69082
|
-
paintTravelProgress(
|
|
68485
|
+
paintTravelProgress();
|
|
68486
|
+
writeRowAtRest(slideElements, placeOf, currentElement);
|
|
69083
68487
|
for (const slideElement of slideElements) {
|
|
69084
|
-
|
|
69085
|
-
x,
|
|
69086
|
-
y
|
|
69087
|
-
} = placeOf.get(readArea(slideElement)) || {
|
|
69088
|
-
x: 0,
|
|
69089
|
-
y: 0
|
|
69090
|
-
};
|
|
69091
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
69092
|
-
slideElement.toggleAttribute("data-slide-offstage", slideElement !== currentElement);
|
|
68488
|
+
slideElement.removeAttribute("data-slide-offstage");
|
|
69093
68489
|
}
|
|
69094
68490
|
const currentArea = readArea(currentElement);
|
|
69095
68491
|
drawnAreaRef.current = currentArea;
|
|
69096
|
-
|
|
69097
|
-
|
|
69098
|
-
|
|
69099
|
-
|
|
69100
|
-
x: 0,
|
|
69101
|
-
y: 0
|
|
69102
|
-
};
|
|
69103
|
-
const offset = `${-x * 100}% ${-y * 100}%`;
|
|
69104
|
-
offsetRef.current = offset;
|
|
69105
|
-
track.style.setProperty("--slide-container-offset", offset);
|
|
68492
|
+
offsetRef.current = "0% 0%";
|
|
68493
|
+
track.style.setProperty("--slide-container-offset", "0% 0%");
|
|
68494
|
+
containerRef.current.removeAttribute("data-slide-travelling");
|
|
68495
|
+
sendRowToRest(currentElement);
|
|
69106
68496
|
renderNear(currentArea, {
|
|
69107
68497
|
alongside: true
|
|
69108
68498
|
});
|
|
@@ -69155,11 +68545,12 @@ const SlideContainer = ({
|
|
|
69155
68545
|
if (landsWithPage && trackAnimationRef.current) {
|
|
69156
68546
|
trackAnimationRef.current.cancel();
|
|
69157
68547
|
trackAnimationRef.current = null;
|
|
69158
|
-
movingRef.current = null;
|
|
69159
68548
|
cancelTravelProgressAnimation();
|
|
69160
|
-
paintTravelProgress(
|
|
68549
|
+
paintTravelProgress();
|
|
69161
68550
|
}
|
|
69162
|
-
const
|
|
68551
|
+
const handLanded = handLandedRef.current === currentArea;
|
|
68552
|
+
handLandedRef.current = null;
|
|
68553
|
+
const still = noTravel || landsWithPage || handLanded;
|
|
69163
68554
|
// Nothing is travelling, so nothing is staged: the picture to paint is the
|
|
69164
68555
|
// map itself, and a stage left over from a travel that has just been given
|
|
69165
68556
|
// up on would be painted instead of it.
|
|
@@ -69225,19 +68616,30 @@ const SlideContainer = ({
|
|
|
69225
68616
|
// travel someone asked for has its own transfer waiting (focusHandOverRef),
|
|
69226
68617
|
// and that one knows what was pressed.
|
|
69227
68618
|
const focusIsLeaving = !focusHandOverRef.current && slideElements.some(slideElement => slideElement !== currentElement && slideElement.contains(document.activeElement));
|
|
68619
|
+
// Read before the row is written: what the slide holds decides where it ends.
|
|
68620
|
+
if (!stage) {
|
|
68621
|
+
writeRowAtRest(slideElements, placeOf, currentElement);
|
|
68622
|
+
}
|
|
68623
|
+
const onRow = ({
|
|
68624
|
+
x,
|
|
68625
|
+
y
|
|
68626
|
+
}) => ({
|
|
68627
|
+
x: x - originRef.current.x,
|
|
68628
|
+
y: y - originRef.current.y
|
|
68629
|
+
});
|
|
69228
68630
|
for (const slideElement of slideElements) {
|
|
69229
68631
|
const area = readArea(slideElement);
|
|
69230
|
-
const {
|
|
69231
|
-
x,
|
|
69232
|
-
y
|
|
69233
|
-
} = placeOfArea(area);
|
|
69234
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
69235
68632
|
const isCurrent = slideElement === currentElement;
|
|
69236
68633
|
slideElement.toggleAttribute("data-current", isCurrent);
|
|
69237
68634
|
slideElement.toggleAttribute("data-slide-displaced", !isCurrent);
|
|
69238
|
-
//
|
|
69239
|
-
//
|
|
69240
|
-
|
|
68635
|
+
// Off stage only while a travel asked for plays: at rest a hand can bring in
|
|
68636
|
+
// any of them, and it would find them unpainted.
|
|
68637
|
+
const onStage = Boolean(stage) && stage.placeByArea.has(area);
|
|
68638
|
+
slideElement.toggleAttribute("data-slide-offstage", Boolean(stage) && !onStage);
|
|
68639
|
+
if (onStage) {
|
|
68640
|
+
slideElement.removeAttribute("data-off-row");
|
|
68641
|
+
writeSlidePlace(slideElement, onRow(stage.placeByArea.get(area)));
|
|
68642
|
+
}
|
|
69241
68643
|
if (isCurrent) {
|
|
69242
68644
|
// Reachable again first, so the focus below has somewhere to land: an
|
|
69243
68645
|
// inert element cannot take it.
|
|
@@ -69245,12 +68647,14 @@ const SlideContainer = ({
|
|
|
69245
68647
|
}
|
|
69246
68648
|
}
|
|
69247
68649
|
const track = trackRef.current;
|
|
69248
|
-
|
|
68650
|
+
containerRef.current.toggleAttribute("data-slide-travelling", Boolean(stage));
|
|
68651
|
+
// How far the track stands from where the scroll position rests: none at rest.
|
|
68652
|
+
const rowPlace = onRow(currentPlace);
|
|
68653
|
+
const restPlace = restPlaceRef.current;
|
|
68654
|
+
const offset = `${(restPlace.x - rowPlace.x) * 100}% ${(restPlace.y - rowPlace.y) * 100}%`;
|
|
69249
68655
|
// The travel that was ASKED for is still one box, whatever is left of it to
|
|
69250
|
-
// cover:
|
|
69251
|
-
// the duration rather than taking a full one over a few pixels.
|
|
68656
|
+
// cover: what is left takes what is left of the duration.
|
|
69252
68657
|
const offsetTargetBefore = offsetRef.current;
|
|
69253
|
-
const targetMoves = offset !== offsetTargetBefore;
|
|
69254
68658
|
// Where the track IS, read before anything is written: a travel asked for
|
|
69255
68659
|
// while another is still playing must carry on from what is on screen. The
|
|
69256
68660
|
// previous TARGET is where the last travel was going, not where it got to —
|
|
@@ -69269,35 +68673,16 @@ const SlideContainer = ({
|
|
|
69269
68673
|
const onScreenPx = trackOffsetPx(track, containerRef.current);
|
|
69270
68674
|
offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
|
|
69271
68675
|
}
|
|
69272
|
-
|
|
69273
|
-
// said by the gesture that put it there: the track is at rest as far as any
|
|
69274
|
-
// animation is concerned, so nothing else could tell. Taken by the render
|
|
69275
|
-
// that moves the track and by no other: a `current` held outside catches up
|
|
69276
|
-
// with the gesture a render late (a parent's state, set from a handler that
|
|
69277
|
-
// runs after the address is written and read back), and a render still
|
|
69278
|
-
// standing on the slide being left would otherwise carry the finger's
|
|
69279
|
-
// offset back to it — a travel home nobody asked for.
|
|
69280
|
-
const offsetDragged = targetMoves ? travelFromRef.current : null;
|
|
69281
|
-
if (targetMoves) {
|
|
69282
|
-
travelFromRef.current = null;
|
|
69283
|
-
}
|
|
69284
|
-
const offsetBefore = offsetDragged ?? offsetOnScreen ?? offsetRef.current;
|
|
68676
|
+
const offsetBefore = offsetOnScreen ?? offsetRef.current;
|
|
69285
68677
|
offsetRef.current = offset;
|
|
69286
|
-
|
|
69287
|
-
// there, and when it is over this is what holds. Except under a hand-off
|
|
69288
|
-
// still waiting for its travel: the track stays where the finger left it,
|
|
69289
|
-
// until the render that moves it or the frame after which the gesture gives
|
|
69290
|
-
// up on it (see settleTowards in onEnd).
|
|
69291
|
-
if (!travelFromRef.current) {
|
|
69292
|
-
track.style.setProperty("--slide-container-offset", offset);
|
|
69293
|
-
}
|
|
68678
|
+
track.style.setProperty("--slide-container-offset", offset);
|
|
69294
68679
|
// A travel to this very offset is already playing: left to play. Same
|
|
69295
68680
|
// courtesy as the stage above — a re-render in the middle of a travel (a
|
|
69296
68681
|
// signal read higher up answering) changes nothing here, and setting off
|
|
69297
68682
|
// again from the on-screen position would replay what is left over a full
|
|
69298
68683
|
// duration (the ask is zero, so ratioOfOneTravel answers 1), slowing the
|
|
69299
68684
|
// travel at every re-render.
|
|
69300
|
-
const sameTravelPlaying = travelInFlight &&
|
|
68685
|
+
const sameTravelPlaying = travelInFlight && offsetTargetBefore === offset;
|
|
69301
68686
|
const travels = !sameTravelPlaying && !still && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
69302
68687
|
if (travels) {
|
|
69303
68688
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
@@ -69307,10 +68692,9 @@ const SlideContainer = ({
|
|
|
69307
68692
|
// this only ever shortens it (a longer travel is not made slower, which
|
|
69308
68693
|
// would make a two-box move drag).
|
|
69309
68694
|
const travelRatio = ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
|
|
69310
|
-
// Already moving
|
|
69311
|
-
//
|
|
69312
|
-
|
|
69313
|
-
const easing = travelInFlight || offsetDragged ? "ease-out" : "ease";
|
|
68695
|
+
// Already moving, so there is no ease-in to play: it would stall the track
|
|
68696
|
+
// for an instant right where the eye is following it.
|
|
68697
|
+
const easing = travelInFlight ? "ease-out" : "ease";
|
|
69314
68698
|
// Cancelled rather than layered: two animations on the same property
|
|
69315
68699
|
// would blend, and what one sees then is neither of the two moves.
|
|
69316
68700
|
trackAnimationRef.current?.cancel();
|
|
@@ -69322,14 +68706,8 @@ const SlideContainer = ({
|
|
|
69322
68706
|
duration: durationMs * travelRatio,
|
|
69323
68707
|
easing
|
|
69324
68708
|
});
|
|
69325
|
-
|
|
69326
|
-
|
|
69327
|
-
to: currentArea
|
|
69328
|
-
};
|
|
69329
|
-
// The trait travels with the slides: from where the gesture left it when
|
|
69330
|
-
// there was one, from a whole box away when the travel was asked for.
|
|
69331
|
-
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
69332
|
-
travelProgressFromRef.current = null;
|
|
68709
|
+
// The trait travels with the slides, from a whole box away.
|
|
68710
|
+
const progressFrom = travelStep ? travelStep.x || travelStep.y : 0;
|
|
69333
68711
|
// The slide the picture leans on for the length of it is the one being
|
|
69334
68712
|
// LEFT: it is the second one in the frame until the travel is over.
|
|
69335
68713
|
animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
|
|
@@ -69346,12 +68724,11 @@ const SlideContainer = ({
|
|
|
69346
68724
|
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
69347
68725
|
// cancelled by the next travel — that one carries the stage on
|
|
69348
68726
|
});
|
|
69349
|
-
} else if (stage &&
|
|
68727
|
+
} else if (stage && trackAnimationRef.current?.playState !== "running") {
|
|
69350
68728
|
// Staged with nothing left to play: a travel that was drawn and then had
|
|
69351
68729
|
// its animation taken away (a duration set to 0, a re-render landing
|
|
69352
68730
|
// between the two). Struck at once rather than left standing, since the
|
|
69353
|
-
// thing it was standing for is over.
|
|
69354
|
-
// standing for something — the finger still holding it.
|
|
68731
|
+
// thing it was standing for is over.
|
|
69355
68732
|
settleTravel();
|
|
69356
68733
|
}
|
|
69357
68734
|
// A window waiting for its travel to be over (see goToArea's own loop
|
|
@@ -69411,6 +68788,11 @@ const SlideContainer = ({
|
|
|
69411
68788
|
renderNear(settlingArea, {
|
|
69412
68789
|
alongside: !stageRef.current
|
|
69413
68790
|
});
|
|
68791
|
+
// Something moved the row under it at rest: an opening, a page changing, a
|
|
68792
|
+
// slide added before it. Under a hand it stays where the hand has it.
|
|
68793
|
+
if (!stageRef.current && !handMovingRef.current) {
|
|
68794
|
+
sendRowToRest(currentElement);
|
|
68795
|
+
}
|
|
69414
68796
|
// Said last, and after data-current has been written: what a travel would do
|
|
69415
68797
|
// is read off the map from the slide that is now current, and off the locks
|
|
69416
68798
|
// that slide is wearing in this very commit.
|
|
@@ -69419,9 +68801,6 @@ const SlideContainer = ({
|
|
|
69419
68801
|
// — a single announcement for a render that may have changed several of the
|
|
69420
68802
|
// facts at once.
|
|
69421
68803
|
announceState();
|
|
69422
|
-
// The finger has the last word: everything above drew the map at rest, and
|
|
69423
|
-
// where the track actually is right now is where the gesture put it.
|
|
69424
|
-
paintDrag();
|
|
69425
68804
|
});
|
|
69426
68805
|
|
|
69427
68806
|
/**
|
|
@@ -69434,7 +68813,8 @@ const SlideContainer = ({
|
|
|
69434
68813
|
value,
|
|
69435
68814
|
released,
|
|
69436
68815
|
dx = 0,
|
|
69437
|
-
dy = 0
|
|
68816
|
+
dy = 0,
|
|
68817
|
+
landed
|
|
69438
68818
|
} = {}) => {
|
|
69439
68819
|
// A window mid-roll cannot travel yet: it is on its way somewhere and the
|
|
69440
68820
|
// content that goes with it has not moved, so a second travel would be
|
|
@@ -69562,7 +68942,7 @@ const SlideContainer = ({
|
|
|
69562
68942
|
// A caller writing the change somewhere that keeps a trace — the URL, a
|
|
69563
68943
|
// history — needs it to know whether a place was aimed at or browsed past.
|
|
69564
68944
|
const cause = causeOfEvent(event);
|
|
69565
|
-
if (commit === "rest") {
|
|
68945
|
+
if (commit === "rest" && !landed) {
|
|
69566
68946
|
// The travel first, the change once it is over: a caller putting it
|
|
69567
68947
|
// somewhere expensive or visible (the URL, the address bar, a server)
|
|
69568
68948
|
// must not be asked for it sixty times a second, and the picture must not
|
|
@@ -69947,22 +69327,152 @@ const SlideContainer = ({
|
|
|
69947
69327
|
return area;
|
|
69948
69328
|
};
|
|
69949
69329
|
|
|
69950
|
-
// Where
|
|
69951
|
-
//
|
|
69952
|
-
|
|
69953
|
-
|
|
69954
|
-
|
|
69955
|
-
|
|
69956
|
-
|
|
69957
|
-
|
|
69330
|
+
// Where a hand has the row, in the terms of --slide-travel-progress: from the
|
|
69331
|
+
// current slide while it stands next to it, and past that between the two it
|
|
69332
|
+
// stands between, as far as the hand carries it.
|
|
69333
|
+
const paintNativeRow = () => {
|
|
69334
|
+
if (trackAnimationRef.current?.playState === "running") {
|
|
69335
|
+
return;
|
|
69336
|
+
}
|
|
69337
|
+
const containerEl = containerRef.current;
|
|
69338
|
+
const slideElements = Array.from(trackRef.current.children);
|
|
69339
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
69340
|
+
if (!currentElement) {
|
|
69341
|
+
return;
|
|
69342
|
+
}
|
|
69343
|
+
if (restsOn(containerEl, currentElement)) {
|
|
69344
|
+
if (handMovingRef.current) {
|
|
69345
|
+
paintTravelProgress();
|
|
69346
|
+
}
|
|
69347
|
+
return;
|
|
69348
|
+
}
|
|
69349
|
+
handMovingRef.current = true;
|
|
69350
|
+
const {
|
|
69351
|
+
base,
|
|
69352
|
+
toward,
|
|
69353
|
+
fraction,
|
|
69354
|
+
forward
|
|
69355
|
+
} = readRowStretch(containerEl, slideElements, currentElement);
|
|
69356
|
+
const baseArea = readArea(base);
|
|
69357
|
+
for (const element of travelPainters()) {
|
|
69358
|
+
if (base === currentElement) {
|
|
69359
|
+
element.removeAttribute(SLIDE_BASE_ATTRIBUTE);
|
|
69360
|
+
} else {
|
|
69361
|
+
element.setAttribute(SLIDE_BASE_ATTRIBUTE, baseArea);
|
|
69362
|
+
}
|
|
69363
|
+
element.style.setProperty("--slide-travel-progress", forward ? -fraction : fraction);
|
|
69364
|
+
}
|
|
69365
|
+
paintTravelToward(toward ? readArea(toward) : null);
|
|
69366
|
+
// What the hand reaches ahead of the neighbours built at rest is asked for on
|
|
69367
|
+
// the way: an empty slide goes by otherwise.
|
|
69368
|
+
renderNear(baseArea, {
|
|
69369
|
+
alongside: true
|
|
69370
|
+
});
|
|
69371
|
+
};
|
|
69372
|
+
const landNativeRow = scrollEndEvent => {
|
|
69373
|
+
if (!handMovingRef.current) {
|
|
69374
|
+
return;
|
|
69375
|
+
}
|
|
69376
|
+
const containerEl = containerRef.current;
|
|
69377
|
+
const {
|
|
69378
|
+
slideElements,
|
|
69379
|
+
placeOf
|
|
69380
|
+
} = readMap();
|
|
69381
|
+
const landedElement = pageAtRest(containerEl, slideElements);
|
|
69382
|
+
if (!landedElement) {
|
|
69383
|
+
return;
|
|
69384
|
+
}
|
|
69385
|
+
handMovingRef.current = false;
|
|
69386
|
+
paintTravelProgress();
|
|
69387
|
+
const area = readArea(landedElement);
|
|
69388
|
+
const currentArea = containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
69389
|
+
if (area === currentArea) {
|
|
69390
|
+
settleTravel();
|
|
69391
|
+
return;
|
|
69392
|
+
}
|
|
69393
|
+
const event = scrollEndEvent ?? new Event("scrollend");
|
|
69394
|
+
if (loop) {
|
|
69395
|
+
// A window does not change slide, it rolls: back at rest in the render the
|
|
69396
|
+
// content moves in, the two being the same picture.
|
|
69397
|
+
const from = placeOf.get(currentArea) || {
|
|
69398
|
+
x: 0,
|
|
69399
|
+
y: 0
|
|
69400
|
+
};
|
|
69401
|
+
const to = placeOf.get(area) || {
|
|
69402
|
+
x: 0,
|
|
69403
|
+
y: 0
|
|
69404
|
+
};
|
|
69405
|
+
setNoTravel(true);
|
|
69406
|
+
onLoop?.({
|
|
69407
|
+
area,
|
|
69408
|
+
dx: Math.sign(to.x - from.x),
|
|
69409
|
+
dy: Math.sign(to.y - from.y),
|
|
69410
|
+
event
|
|
69411
|
+
});
|
|
69412
|
+
return;
|
|
69413
|
+
}
|
|
69414
|
+
handLandedRef.current = area;
|
|
69415
|
+
if (goToArea(area, {
|
|
69416
|
+
event,
|
|
69417
|
+
landed: true
|
|
69418
|
+
})) {
|
|
69958
69419
|
return;
|
|
69959
69420
|
}
|
|
69960
|
-
|
|
69961
|
-
|
|
69962
|
-
|
|
69963
|
-
|
|
69964
|
-
|
|
69421
|
+
// Refused: the row is put back in view of the slide that stays.
|
|
69422
|
+
handLandedRef.current = null;
|
|
69423
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
69424
|
+
containerEl.scrollTo({
|
|
69425
|
+
left: currentElement.offsetLeft,
|
|
69426
|
+
top: currentElement.offsetTop,
|
|
69427
|
+
behavior: "smooth"
|
|
69428
|
+
});
|
|
69429
|
+
};
|
|
69430
|
+
|
|
69431
|
+
// Reached through a ref, and the listeners are never rebuilt for it: a landing
|
|
69432
|
+
// changes the current slide, and one is often already on its way when it does.
|
|
69433
|
+
const nativeRowHandlersRef = useRef(null);
|
|
69434
|
+
nativeRowHandlersRef.current = {
|
|
69435
|
+
paintNativeRow,
|
|
69436
|
+
landNativeRow
|
|
69965
69437
|
};
|
|
69438
|
+
useLayoutEffect(() => {
|
|
69439
|
+
return watchRowScroll(containerRef.current, {
|
|
69440
|
+
onScroll: () => nativeRowHandlersRef.current.paintNativeRow(),
|
|
69441
|
+
onLand: event => nativeRowHandlersRef.current.landNativeRow(event)
|
|
69442
|
+
});
|
|
69443
|
+
}, []);
|
|
69444
|
+
|
|
69445
|
+
// A wheel on an axis only a finger travels (travelByScroll) is not this box's,
|
|
69446
|
+
// though a scroller takes it all the same: it goes to what scrolls inside, or
|
|
69447
|
+
// on to what scrolls around, which is where it was headed.
|
|
69448
|
+
const wheelRefusedAxes = Array.from(dragAxes ?? "").filter(axis => !scrollAxes?.includes(axis)).join("");
|
|
69449
|
+
useLayoutEffect(() => {
|
|
69450
|
+
if (!wheelRefusedAxes) {
|
|
69451
|
+
return undefined;
|
|
69452
|
+
}
|
|
69453
|
+
const containerEl = containerRef.current;
|
|
69454
|
+
const onWheel = wheelEvent => {
|
|
69455
|
+
const axis = Math.abs(wheelEvent.deltaX) > Math.abs(wheelEvent.deltaY) ? "x" : "y";
|
|
69456
|
+
if (!wheelRefusedAxes.includes(axis)) {
|
|
69457
|
+
return;
|
|
69458
|
+
}
|
|
69459
|
+
const delta = axis === "x" ? wheelEvent.deltaX : wheelEvent.deltaY;
|
|
69460
|
+
if (!delta || scrollRoomTowards(wheelEvent.target, containerEl, axis, delta > 0 ? -1 : 1)) {
|
|
69461
|
+
return;
|
|
69462
|
+
}
|
|
69463
|
+
wheelEvent.preventDefault();
|
|
69464
|
+
getScrollContainer(containerEl.parentElement)?.scrollBy({
|
|
69465
|
+
[axis === "x" ? "left" : "top"]: delta,
|
|
69466
|
+
behavior: "instant"
|
|
69467
|
+
});
|
|
69468
|
+
};
|
|
69469
|
+
containerEl.addEventListener("wheel", onWheel, {
|
|
69470
|
+
passive: false
|
|
69471
|
+
});
|
|
69472
|
+
return () => {
|
|
69473
|
+
containerEl.removeEventListener("wheel", onWheel);
|
|
69474
|
+
};
|
|
69475
|
+
}, [wheelRefusedAxes]);
|
|
69966
69476
|
|
|
69967
69477
|
// Everything that draws this travel: the box itself and whoever follows it.
|
|
69968
69478
|
const travelPainters = () => {
|
|
@@ -70095,13 +69605,12 @@ const SlideContainer = ({
|
|
|
70095
69605
|
// current slide changes under it.
|
|
70096
69606
|
const paintTravelProgress = (progress, area) => {
|
|
70097
69607
|
for (const element of travelPainters()) {
|
|
70098
|
-
|
|
70099
|
-
element.style.setProperty("--slide-travel-progress", progress);
|
|
70100
|
-
} else {
|
|
69608
|
+
{
|
|
70101
69609
|
element.style.removeProperty("--slide-travel-progress");
|
|
69610
|
+
element.removeAttribute(SLIDE_BASE_ATTRIBUTE);
|
|
70102
69611
|
}
|
|
70103
69612
|
}
|
|
70104
|
-
paintTravelToward(
|
|
69613
|
+
paintTravelToward(null);
|
|
70105
69614
|
};
|
|
70106
69615
|
const cancelTravelProgressAnimation = () => {
|
|
70107
69616
|
for (const animation of progressAnimationsRef.current) {
|
|
@@ -70121,7 +69630,7 @@ const SlideContainer = ({
|
|
|
70121
69630
|
// draw is the one about to start.
|
|
70122
69631
|
followerElementsRef.current = readFollowerElements(containerRef.current);
|
|
70123
69632
|
cancelTravelProgressAnimation();
|
|
70124
|
-
paintTravelProgress(
|
|
69633
|
+
paintTravelProgress();
|
|
70125
69634
|
const painters = travelPainters();
|
|
70126
69635
|
if (!painters.length || !from || !durationMs) {
|
|
70127
69636
|
return;
|
|
@@ -70142,598 +69651,8 @@ const SlideContainer = ({
|
|
|
70142
69651
|
// cancelled by the next travel — that one says where the trait goes
|
|
70143
69652
|
});
|
|
70144
69653
|
};
|
|
70145
|
-
|
|
70146
|
-
// The two slides the gesture can bring in, placed one box either side of the
|
|
70147
|
-
// one being dragged — the same stage a travel builds, except that both ends
|
|
70148
|
-
// are set up at once because the finger has not said yet which way it goes.
|
|
70149
|
-
const stageDrag = (drag, currentArea) => {
|
|
70150
|
-
const {
|
|
70151
|
-
slideElements,
|
|
70152
|
-
placeOf
|
|
70153
|
-
} = readMap();
|
|
70154
|
-
const step = drag.axis === "x" ? {
|
|
70155
|
-
x: 1,
|
|
70156
|
-
y: 0
|
|
70157
|
-
} : {
|
|
70158
|
-
x: 0,
|
|
70159
|
-
y: 1
|
|
70160
|
-
};
|
|
70161
|
-
const placeByArea = new Map();
|
|
70162
|
-
placeByArea.set(drag.area, drag.basePlace);
|
|
70163
|
-
if (drag.areaBack) {
|
|
70164
|
-
placeByArea.set(drag.areaBack, {
|
|
70165
|
-
x: drag.basePlace.x - step.x,
|
|
70166
|
-
y: drag.basePlace.y - step.y
|
|
70167
|
-
});
|
|
70168
|
-
}
|
|
70169
|
-
if (drag.areaOn) {
|
|
70170
|
-
placeByArea.set(drag.areaOn, {
|
|
70171
|
-
x: drag.basePlace.x + step.x,
|
|
70172
|
-
y: drag.basePlace.y + step.y
|
|
70173
|
-
});
|
|
70174
|
-
}
|
|
70175
|
-
stageRef.current = {
|
|
70176
|
-
placeByArea,
|
|
70177
|
-
area: currentArea
|
|
70178
|
-
};
|
|
70179
|
-
for (const slideElement of slideElements) {
|
|
70180
|
-
const area = readArea(slideElement);
|
|
70181
|
-
const {
|
|
70182
|
-
x,
|
|
70183
|
-
y
|
|
70184
|
-
} = placeByArea.get(area) || placeOf.get(area) || {
|
|
70185
|
-
x: 0,
|
|
70186
|
-
y: 0
|
|
70187
|
-
};
|
|
70188
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
70189
|
-
slideElement.toggleAttribute("data-slide-offstage", !placeByArea.has(area));
|
|
70190
|
-
}
|
|
70191
|
-
};
|
|
70192
|
-
|
|
70193
|
-
// Let go of, and the slides go where the gesture leaves them: back on the
|
|
70194
|
-
// slide being dragged when nothing travels, on to the slide arriving when a
|
|
70195
|
-
// travel caught in flight is let carry on. Over the distance left to cover,
|
|
70196
|
-
// so a slide barely moved snaps back and one dragged most of the way there
|
|
70197
|
-
// takes its time.
|
|
70198
|
-
const settleTowards = (drag, area) => {
|
|
70199
|
-
const track = trackRef.current;
|
|
70200
|
-
if (!track) {
|
|
70201
|
-
return;
|
|
70202
|
-
}
|
|
70203
|
-
const stage = stageRef.current;
|
|
70204
|
-
const {
|
|
70205
|
-
placeOf
|
|
70206
|
-
} = readMap();
|
|
70207
|
-
const place = stage?.placeByArea.get(area) || placeOf.get(area) || {
|
|
70208
|
-
x: 0,
|
|
70209
|
-
y: 0
|
|
70210
|
-
};
|
|
70211
|
-
const restPx = {
|
|
70212
|
-
x: -place.x * drag.box.width,
|
|
70213
|
-
y: -place.y * drag.box.height
|
|
70214
|
-
};
|
|
70215
|
-
const nowPx = {
|
|
70216
|
-
x: drag.baseOffset.x + drag.pull.x,
|
|
70217
|
-
y: drag.baseOffset.y + drag.pull.y
|
|
70218
|
-
};
|
|
70219
|
-
const restOffset = `${restPx.x}px ${restPx.y}px`;
|
|
70220
|
-
const durationMs = durationToMs(duration);
|
|
70221
|
-
const size = drag.axis === "x" ? drag.box.width : drag.box.height;
|
|
70222
|
-
const left = Math.abs(restPx[drag.axis] - nowPx[drag.axis]);
|
|
70223
|
-
trackAnimationRef.current?.cancel();
|
|
70224
|
-
trackAnimationRef.current = null;
|
|
70225
|
-
track.style.setProperty("--slide-container-offset", restOffset);
|
|
70226
|
-
if (!durationMs || !left) {
|
|
70227
|
-
paintTravelProgress(0);
|
|
70228
|
-
settleTravel();
|
|
70229
|
-
return;
|
|
70230
|
-
}
|
|
70231
|
-
// Which other slide the picture leans on while it arrives: the one being
|
|
70232
|
-
// pulled in when it comes back on the dragged slide, the dragged slide
|
|
70233
|
-
// itself when it goes on to the one arriving — and the trait is then a
|
|
70234
|
-
// box short of that one, which is what it has to close.
|
|
70235
|
-
const arrivesOnBase = area === drag.area;
|
|
70236
|
-
const sign = drag.pull[drag.axis] > 0 ? 1 : -1;
|
|
70237
|
-
const progressFrom = arrivesOnBase ? drag.progress : drag.progress - sign;
|
|
70238
|
-
const otherArea = arrivesOnBase ? drag.areaPulled : drag.area;
|
|
70239
|
-
movingRef.current = {
|
|
70240
|
-
from: otherArea,
|
|
70241
|
-
to: area
|
|
70242
|
-
};
|
|
70243
|
-
animateTravelProgress(progressFrom, durationMs * (left / size), "ease-out", otherArea);
|
|
70244
|
-
const animation = track.animate([{
|
|
70245
|
-
translate: `${nowPx.x}px ${nowPx.y}px`
|
|
70246
|
-
}, {
|
|
70247
|
-
translate: restOffset
|
|
70248
|
-
}], {
|
|
70249
|
-
duration: durationMs * (left / size),
|
|
70250
|
-
easing: "ease-out"
|
|
70251
|
-
});
|
|
70252
|
-
trackAnimationRef.current = animation;
|
|
70253
|
-
animation.finished.then(settleTravel, () => {
|
|
70254
|
-
// cancelled by a travel asked for since — that one carries the stage on
|
|
70255
|
-
});
|
|
70256
|
-
};
|
|
70257
|
-
|
|
70258
|
-
// A travel that is playing when a gesture takes hold of it is STOPPED where it
|
|
70259
|
-
// stands, at the moment the hand holds it (see readTravelInFlight for when
|
|
70260
|
-
// that is): the track stays exactly under the eye, and the hand answers it
|
|
70261
|
-
// from there.
|
|
70262
|
-
// A gesture that turns out to be nothing lets the travel carry on from where
|
|
70263
|
-
// it was caught (see onGiveUp).
|
|
70264
|
-
const catchTravelInFlight = () => {
|
|
70265
|
-
const trackElement = trackRef.current;
|
|
70266
|
-
const travelCaught = trackAnimationRef.current;
|
|
70267
|
-
if (!travelCaught || travelCaught.playState !== "running") {
|
|
70268
|
-
return null;
|
|
70269
|
-
}
|
|
70270
|
-
const onScreenPx = trackOffsetPx(trackElement, containerRef.current);
|
|
70271
|
-
const offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
|
|
70272
|
-
travelCaught.cancel();
|
|
70273
|
-
// Where it was, held: cancelling an animation puts the track back on the
|
|
70274
|
-
// value underneath it, which is the far end of the travel — the very jump
|
|
70275
|
-
// this is about.
|
|
70276
|
-
trackElement.style.setProperty("--slide-container-offset", offsetOnScreen);
|
|
70277
|
-
const moving = movingRef.current || {};
|
|
70278
|
-
return {
|
|
70279
|
-
trackElement,
|
|
70280
|
-
onScreenPx,
|
|
70281
|
-
offsetOnScreen,
|
|
70282
|
-
offsetTarget: offsetRef.current,
|
|
70283
|
-
from: moving.from,
|
|
70284
|
-
to: moving.to
|
|
70285
|
-
};
|
|
70286
|
-
};
|
|
70287
|
-
// Which way a travel still playing is going, read without touching it: a
|
|
70288
|
-
// hand landing on something moving has no axis left to decide (a first pixel
|
|
70289
|
-
// of tremor read as one gives the gesture up), and which WAY along it the
|
|
70290
|
-
// travel goes is what tells the next swipe from a catch. `way` is the sign a
|
|
70291
|
-
// pull going the same way would have.
|
|
70292
|
-
const readTravelInFlight = () => {
|
|
70293
|
-
const trackElement = trackRef.current;
|
|
70294
|
-
const animation = trackAnimationRef.current;
|
|
70295
|
-
if (!animation || animation.playState !== "running") {
|
|
70296
|
-
return null;
|
|
70297
|
-
}
|
|
70298
|
-
const onScreenPx = trackOffsetPx(trackElement, containerRef.current);
|
|
70299
|
-
const targetPx = offsetToPx(offsetRef.current, trackElement.getBoundingClientRect());
|
|
70300
|
-
const towardsX = targetPx.x - onScreenPx.x;
|
|
70301
|
-
const towardsY = targetPx.y - onScreenPx.y;
|
|
70302
|
-
const axis = Math.abs(towardsX) >= Math.abs(towardsY) ? "x" : "y";
|
|
70303
|
-
const towards = axis === "x" ? towardsX : towardsY;
|
|
70304
|
-
if (Math.abs(towards) < 1) {
|
|
70305
|
-
// On its last pixel: nothing left to catch or to push.
|
|
70306
|
-
return null;
|
|
70307
|
-
}
|
|
70308
|
-
return {
|
|
70309
|
-
axis,
|
|
70310
|
-
way: towards > 0 ? 1 : -1
|
|
70311
|
-
};
|
|
70312
|
-
};
|
|
70313
|
-
|
|
70314
|
-
// What a travel gesture does to the slides, whoever asked for it: a pointer
|
|
70315
|
-
// dragging the box and a wheel pushing it sideways ask for the same travel,
|
|
70316
|
-
// so they are answered by the same callbacks and only the reading of the
|
|
70317
|
-
// input differs (see drag_to_travel.js). The rules of the gesture are read
|
|
70318
|
-
// there, the geometry of the slides here.
|
|
70319
|
-
const createTravelHandlers = caughtAtStart => {
|
|
70320
|
-
let caughtTravel = caughtAtStart;
|
|
70321
|
-
// The travel in hand, as the slides see it: where the one being dragged
|
|
70322
|
-
// stands, what is either side of it, and how far the gesture has taken it.
|
|
70323
|
-
const drag = {
|
|
70324
|
-
axis: null,
|
|
70325
|
-
area: null,
|
|
70326
|
-
areaBack: null,
|
|
70327
|
-
areaOn: null,
|
|
70328
|
-
areaPulled: null,
|
|
70329
|
-
box: null,
|
|
70330
|
-
basePlace: null,
|
|
70331
|
-
baseOffset: null,
|
|
70332
|
-
pull: {
|
|
70333
|
-
x: 0,
|
|
70334
|
-
y: 0
|
|
70335
|
-
},
|
|
70336
|
-
progress: 0,
|
|
70337
|
-
offset: null,
|
|
70338
|
-
gesture: null
|
|
70339
|
-
};
|
|
70340
|
-
return {
|
|
70341
|
-
drag,
|
|
70342
|
-
onStart: ({
|
|
70343
|
-
axis,
|
|
70344
|
-
sign,
|
|
70345
|
-
target,
|
|
70346
|
-
caught
|
|
70347
|
-
}) => {
|
|
70348
|
-
// A travel left playing under the press, which the hand has now taken
|
|
70349
|
-
// hold of: stopped here, where it stands at this moment.
|
|
70350
|
-
if (caught) {
|
|
70351
|
-
caughtTravel = catchTravelInFlight();
|
|
70352
|
-
}
|
|
70353
|
-
// Everything positional is read HERE rather than when the pointer
|
|
70354
|
-
// landed: the travel that was playing then may have arrived since, and
|
|
70355
|
-
// it is what the slides are doing at the moment the gesture takes them
|
|
70356
|
-
// over that the gesture must carry on from.
|
|
70357
|
-
const track = trackRef.current;
|
|
70358
|
-
const {
|
|
70359
|
-
slideElements,
|
|
70360
|
-
placeOf
|
|
70361
|
-
} = readMap();
|
|
70362
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
70363
|
-
const box = track.getBoundingClientRect();
|
|
70364
|
-
if (!currentElement || !box.width || !box.height) {
|
|
70365
|
-
return false;
|
|
70366
|
-
}
|
|
70367
|
-
const currentArea = readArea(currentElement);
|
|
70368
|
-
const stage = stageRef.current;
|
|
70369
|
-
// Where a slide stands: where the stage put it while a travel is
|
|
70370
|
-
// playing, its place on the map otherwise.
|
|
70371
|
-
const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
|
|
70372
|
-
x: 0,
|
|
70373
|
-
y: 0
|
|
70374
|
-
};
|
|
70375
|
-
// A travel caught in flight is measured from the slide it SET OFF from,
|
|
70376
|
-
// with the slide it was going to as the one thing the hand can bring
|
|
70377
|
-
// in — the way drag_to_travel reads a `slack`: how far the box already
|
|
70378
|
-
// sits from its resting place, towards what is being pulled in. That is
|
|
70379
|
-
// what makes its answer "carry on" mean arriving. Based on the slide
|
|
70380
|
-
// arriving instead, the slack would point back at the origin, and a
|
|
70381
|
-
// hand that merely touched the box would send it all the way back.
|
|
70382
|
-
// A movement with no origin of its own (the track re-placed without a
|
|
70383
|
-
// travel) is dragged from the slide shown, like any slide at rest.
|
|
70384
|
-
const origin = caughtTravel && caughtTravel.from !== caughtTravel.to ? caughtTravel.from : null;
|
|
70385
|
-
let area;
|
|
70386
|
-
let areaBack;
|
|
70387
|
-
let areaOn;
|
|
70388
|
-
if (origin) {
|
|
70389
|
-
area = origin;
|
|
70390
|
-
const towardsEnd = axis === "x" ? placeOfArea(currentArea).x > placeOfArea(origin).x : placeOfArea(currentArea).y > placeOfArea(origin).y;
|
|
70391
|
-
areaBack = towardsEnd ? undefined : currentArea;
|
|
70392
|
-
areaOn = towardsEnd ? currentArea : undefined;
|
|
70393
|
-
} else {
|
|
70394
|
-
area = currentArea;
|
|
70395
|
-
areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
|
|
70396
|
-
areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
|
|
70397
|
-
// The hold goToArea reads at the release, read again HERE, off the
|
|
70398
|
-
// same slide and the same attribute: a slide that will refuse the
|
|
70399
|
-
// arrival must not offer the journey. A locked direction simply has
|
|
70400
|
-
// nowhere to go for the length of this gesture — the one case the
|
|
70401
|
-
// gesture already knows, being the last slide of a walk. The hand
|
|
70402
|
-
// then gets the wall it can lean on and never walk through (see
|
|
70403
|
-
// drag_to_travel), the slide behind it stays offstage instead of
|
|
70404
|
-
// being read on the way, and the release has nothing left to refuse.
|
|
70405
|
-
// Nothing here about `released` (--navi-done): that is one particular
|
|
70406
|
-
// departure letting go, decided as it happens, and a gesture armed
|
|
70407
|
-
// before it has no such thing to read — the attribute as rendered is
|
|
70408
|
-
// what the hand is answered from.
|
|
70409
|
-
if (currentElement.hasAttribute("data-prevent-nav-previous")) {
|
|
70410
|
-
areaBack = undefined;
|
|
70411
|
-
}
|
|
70412
|
-
if (currentElement.hasAttribute("data-prevent-nav-next")) {
|
|
70413
|
-
areaOn = undefined;
|
|
70414
|
-
}
|
|
70415
|
-
if (!areaBack && !areaOn ||
|
|
70416
|
-
// Something else with a better claim on the gesture: a scroller
|
|
70417
|
-
// between the finger and the slide, with room left that way.
|
|
70418
|
-
scrollRoomTowards(target, currentElement, axis, sign)) {
|
|
70419
|
-
return false;
|
|
70420
|
-
}
|
|
70421
|
-
}
|
|
70422
|
-
const basePlace = placeOfArea(area);
|
|
70423
|
-
const baseOffset = {
|
|
70424
|
-
x: -basePlace.x * box.width,
|
|
70425
|
-
y: -basePlace.y * box.height
|
|
70426
|
-
};
|
|
70427
|
-
// Where the track IS: a travel grabbed mid-flight was already stopped
|
|
70428
|
-
// where the eye saw it, at the press — this only reads that place, so
|
|
70429
|
-
// the gesture starts from it rather than from where the map rests.
|
|
70430
|
-
const onScreen = caughtTravel ? caughtTravel.onScreenPx : baseOffset;
|
|
70431
|
-
caughtTravel = null;
|
|
70432
|
-
trackAnimationRef.current?.cancel();
|
|
70433
|
-
trackAnimationRef.current = null;
|
|
70434
|
-
cancelTravelProgressAnimation();
|
|
70435
|
-
followerElementsRef.current = readFollowerElements(containerRef.current);
|
|
70436
|
-
drag.axis = axis;
|
|
70437
|
-
drag.areaBack = areaBack;
|
|
70438
|
-
drag.areaOn = areaOn;
|
|
70439
|
-
drag.area = area;
|
|
70440
|
-
drag.box = box;
|
|
70441
|
-
drag.basePlace = basePlace;
|
|
70442
|
-
drag.baseOffset = baseOffset;
|
|
70443
|
-
const size = axis === "x" ? box.width : box.height;
|
|
70444
|
-
// Where the track was when the gesture took it over — nowhere, unless
|
|
70445
|
-
// it was travelling. Every pull is measured from it.
|
|
70446
|
-
const slack = axis === "x" ? onScreen.x - baseOffset.x : onScreen.y - baseOffset.y;
|
|
70447
|
-
drag.pull = {
|
|
70448
|
-
x: 0,
|
|
70449
|
-
y: 0,
|
|
70450
|
-
[axis]: slack
|
|
70451
|
-
};
|
|
70452
|
-
drag.progress = slack / size;
|
|
70453
|
-
drag.areaPulled = slack > 0 ? areaBack : slack < 0 ? areaOn : null;
|
|
70454
|
-
stageDrag(drag, currentArea);
|
|
70455
|
-
// From here the box is busy, whichever input asked: a wheel gesture and
|
|
70456
|
-
// a press must not both be moving the same track.
|
|
70457
|
-
dragRef.current = drag;
|
|
70458
|
-
return {
|
|
70459
|
-
size,
|
|
70460
|
-
slack,
|
|
70461
|
-
travelBack: Boolean(areaBack),
|
|
70462
|
-
travelOn: Boolean(areaOn)
|
|
70463
|
-
};
|
|
70464
|
-
},
|
|
70465
|
-
onPull: ({
|
|
70466
|
-
axis,
|
|
70467
|
-
pulled,
|
|
70468
|
-
progress
|
|
70469
|
-
}) => {
|
|
70470
|
-
drag.pull = {
|
|
70471
|
-
...drag.pull,
|
|
70472
|
-
[axis]: pulled
|
|
70473
|
-
};
|
|
70474
|
-
drag.progress = progress;
|
|
70475
|
-
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
70476
|
-
paintDrag();
|
|
70477
|
-
},
|
|
70478
|
-
// The slide being brought in walked whole and the hand still going, or a
|
|
70479
|
-
// travel caught on its way thrown on: either way the hand asks for the
|
|
70480
|
-
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
70481
|
-
// for that — the slide reached becomes the one dragged, the one the hand
|
|
70482
|
-
// came from stays next to it for a hand that turns around, and the slide
|
|
70483
|
-
// past it is put on the other side.
|
|
70484
|
-
onEdge: ({
|
|
70485
|
-
axis,
|
|
70486
|
-
sign
|
|
70487
|
-
}) => {
|
|
70488
|
-
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
70489
|
-
const {
|
|
70490
|
-
slideElements,
|
|
70491
|
-
placeOf
|
|
70492
|
-
} = readMap();
|
|
70493
|
-
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
70494
|
-
if (!reachedElement) {
|
|
70495
|
-
return false;
|
|
70496
|
-
}
|
|
70497
|
-
// The gate goToArea reads at the release, asked of the slide the hand
|
|
70498
|
-
// is now in: a slide holding on to the user is not walked through.
|
|
70499
|
-
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
70500
|
-
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
70501
|
-
if (!past) {
|
|
70502
|
-
return false;
|
|
70503
|
-
}
|
|
70504
|
-
// Two slides from the one at rest, so possibly never rendered: asked
|
|
70505
|
-
// for now, and built before the frame that brings it in is painted.
|
|
70506
|
-
renderNear(past, {
|
|
70507
|
-
alongside: false
|
|
70508
|
-
});
|
|
70509
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
70510
|
-
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
70511
|
-
x: 0,
|
|
70512
|
-
y: 0
|
|
70513
|
-
};
|
|
70514
|
-
drag.areaBack = sign > 0 ? past : drag.area;
|
|
70515
|
-
drag.areaOn = sign > 0 ? drag.area : past;
|
|
70516
|
-
drag.area = reached;
|
|
70517
|
-
drag.basePlace = basePlace;
|
|
70518
|
-
drag.baseOffset = {
|
|
70519
|
-
x: -basePlace.x * drag.box.width,
|
|
70520
|
-
y: -basePlace.y * drag.box.height
|
|
70521
|
-
};
|
|
70522
|
-
stageDrag(drag, readArea(currentElement));
|
|
70523
|
-
return {
|
|
70524
|
-
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
70525
|
-
travelBack: Boolean(drag.areaBack),
|
|
70526
|
-
travelOn: Boolean(drag.areaOn)
|
|
70527
|
-
};
|
|
70528
|
-
},
|
|
70529
|
-
onEnd: ({
|
|
70530
|
-
axis,
|
|
70531
|
-
sign,
|
|
70532
|
-
travels,
|
|
70533
|
-
event
|
|
70534
|
-
}) => {
|
|
70535
|
-
dragRef.current = null;
|
|
70536
|
-
// Where the gesture leaves the slides: on the one it was pulling in
|
|
70537
|
-
// when it says so, back on the one it was dragging otherwise.
|
|
70538
|
-
const destination = travels ? sign > 0 ? drag.areaBack : drag.areaOn : drag.area;
|
|
70539
|
-
const {
|
|
70540
|
-
slideElements,
|
|
70541
|
-
placeOf
|
|
70542
|
-
} = readMap();
|
|
70543
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
70544
|
-
const currentArea = currentElement && readArea(currentElement);
|
|
70545
|
-
if (!destination || destination === currentArea) {
|
|
70546
|
-
// Nothing to ask of the state: the slide it names is already the one
|
|
70547
|
-
// shown — a drag let go of short of a travel, a travel caught in
|
|
70548
|
-
// flight and let carry on. The track goes there on its own.
|
|
70549
|
-
settleTowards(drag, destination || drag.area);
|
|
70550
|
-
return;
|
|
70551
|
-
}
|
|
70552
|
-
// Where the slide is being left, for the travel to depart from instead
|
|
70553
|
-
// of from the map.
|
|
70554
|
-
travelFromRef.current = drag.offset;
|
|
70555
|
-
// …and where the indicator is being left, said about the slide that is
|
|
70556
|
-
// ARRIVING: a box short of the slide being pulled in, right where the
|
|
70557
|
-
// finger left it when going back to the slide being dragged.
|
|
70558
|
-
travelProgressFromRef.current = travels ? drag.progress - sign : drag.progress;
|
|
70559
|
-
// Which way the travel goes, read off where the two slides stand
|
|
70560
|
-
// rather than off the pull: a travel caught in flight and put back
|
|
70561
|
-
// goes the way it came, whatever the finger did meanwhile.
|
|
70562
|
-
const stage = stageRef.current;
|
|
70563
|
-
const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
|
|
70564
|
-
x: 0,
|
|
70565
|
-
y: 0
|
|
70566
|
-
};
|
|
70567
|
-
const towards = Math.sign(placeOfArea(destination)[axis] - placeOfArea(currentArea)[axis]);
|
|
70568
|
-
const moved = goToArea(destination, {
|
|
70569
|
-
forward: towards > 0,
|
|
70570
|
-
event,
|
|
70571
|
-
dx: axis === "x" ? towards : 0,
|
|
70572
|
-
dy: axis === "y" ? towards : 0
|
|
70573
|
-
});
|
|
70574
|
-
if (!moved) {
|
|
70575
|
-
// Nowhere to go after all — a slide holding on to the user
|
|
70576
|
-
// (preventNav), or a caller that refused the change.
|
|
70577
|
-
travelFromRef.current = null;
|
|
70578
|
-
travelProgressFromRef.current = null;
|
|
70579
|
-
settleTowards(drag, currentArea);
|
|
70580
|
-
return;
|
|
70581
|
-
}
|
|
70582
|
-
// A container whose `current` is held outside and was not moved: no
|
|
70583
|
-
// render moved the track, so the hand-off is still waiting and the
|
|
70584
|
-
// track still under where the finger left it. One frame is all it
|
|
70585
|
-
// takes to know.
|
|
70586
|
-
requestAnimationFrame(() => {
|
|
70587
|
-
if (travelFromRef.current) {
|
|
70588
|
-
travelFromRef.current = null;
|
|
70589
|
-
travelProgressFromRef.current = null;
|
|
70590
|
-
settleTowards(drag, currentArea);
|
|
70591
|
-
}
|
|
70592
|
-
});
|
|
70593
|
-
},
|
|
70594
|
-
onGiveUp: () => {
|
|
70595
|
-
dragRef.current = null;
|
|
70596
|
-
// A press that never became a gesture: what it stopped goes on its way,
|
|
70597
|
-
// from where the finger caught it and over what is left of the travel.
|
|
70598
|
-
if (!caughtTravel) {
|
|
70599
|
-
return;
|
|
70600
|
-
}
|
|
70601
|
-
const {
|
|
70602
|
-
trackElement: trackCaught,
|
|
70603
|
-
offsetOnScreen,
|
|
70604
|
-
offsetTarget,
|
|
70605
|
-
onScreenPx: caughtOnScreenPx,
|
|
70606
|
-
from,
|
|
70607
|
-
to
|
|
70608
|
-
} = caughtTravel;
|
|
70609
|
-
caughtTravel = null;
|
|
70610
|
-
movingRef.current = {
|
|
70611
|
-
from,
|
|
70612
|
-
to
|
|
70613
|
-
};
|
|
70614
|
-
if (offsetOnScreen === offsetTarget) {
|
|
70615
|
-
settleTravel();
|
|
70616
|
-
return;
|
|
70617
|
-
}
|
|
70618
|
-
const durationMs = durationToMs(duration);
|
|
70619
|
-
// What is LEFT of it, at the pace it had: a travel caught nine tenths
|
|
70620
|
-
// of the way there and let go of does not start its duration again.
|
|
70621
|
-
const boxRect = trackCaught.getBoundingClientRect();
|
|
70622
|
-
const targetPx = offsetToPx(offsetTarget, boxRect);
|
|
70623
|
-
const leftToCover = Math.abs(drag.axis === "y" ? targetPx.y - caughtOnScreenPx.y : targetPx.x - caughtOnScreenPx.x);
|
|
70624
|
-
const size = drag.axis === "y" ? boxRect.height : boxRect.width;
|
|
70625
|
-
const travelRatio = size ? leftToCover / size : 1;
|
|
70626
|
-
trackCaught.style.setProperty("--slide-container-offset", offsetTarget);
|
|
70627
|
-
trackAnimationRef.current = trackCaught.animate([{
|
|
70628
|
-
translate: offsetOnScreen
|
|
70629
|
-
}, {
|
|
70630
|
-
translate: offsetTarget
|
|
70631
|
-
}], {
|
|
70632
|
-
duration: durationMs * travelRatio,
|
|
70633
|
-
easing: "ease-out"
|
|
70634
|
-
});
|
|
70635
|
-
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
70636
|
-
// taken over by a travel asked for since
|
|
70637
|
-
});
|
|
70638
|
-
}
|
|
70639
|
-
};
|
|
70640
|
-
};
|
|
70641
|
-
|
|
70642
|
-
// Whether a gesture may begin at all: one travel at a time, and a window
|
|
70643
|
-
// mid-roll has nothing to drag yet — it is on its way somewhere and the
|
|
70644
|
-
// content that goes with it has not moved (see goToArea).
|
|
70645
|
-
const canStartTravel = () => dragAxes && !dragRef.current && !rollingRef.current;
|
|
70646
|
-
const startDrag = pointerDownEvent => {
|
|
70647
|
-
if (!canStartTravel()) {
|
|
70648
|
-
return;
|
|
70649
|
-
}
|
|
70650
|
-
const travelInFlight = readTravelInFlight();
|
|
70651
|
-
// Left playing when the hand can take it: the press is then far more
|
|
70652
|
-
// likely the next swipe than a catch, and stopping the travel under it is
|
|
70653
|
-
// what that swipe feels as a stall — the gesture listens to the hand first
|
|
70654
|
-
// and says which it was (see inFlight in drag_to_travel.js). On an axis the
|
|
70655
|
-
// hand cannot drag, it is caught at the press, as a travel under a finger
|
|
70656
|
-
// that starts something else.
|
|
70657
|
-
const leftPlaying = Boolean(travelInFlight && dragAxes.includes(travelInFlight.axis));
|
|
70658
|
-
const caughtTravel = travelInFlight && !leftPlaying ? catchTravelInFlight() : null;
|
|
70659
|
-
const handlers = createTravelHandlers(caughtTravel);
|
|
70660
|
-
const gesture = startDragToTravel(pointerDownEvent, {
|
|
70661
|
-
element: containerRef.current,
|
|
70662
|
-
axes: dragAxes,
|
|
70663
|
-
inFlight: leftPlaying ? travelInFlight : false,
|
|
70664
|
-
// The next swipe over a travel still playing: one slide further, and the
|
|
70665
|
-
// travel goes on there from where it is — the way a second arrow press
|
|
70666
|
-
// sends it — without ever having stopped under the hand.
|
|
70667
|
-
onPushOn: ({
|
|
70668
|
-
axis,
|
|
70669
|
-
sign,
|
|
70670
|
-
event
|
|
70671
|
-
}) => {
|
|
70672
|
-
if (trackAnimationRef.current?.playState !== "running") {
|
|
70673
|
-
return false;
|
|
70674
|
-
}
|
|
70675
|
-
// Nothing is held: the box is free for the travel that follows.
|
|
70676
|
-
dragRef.current = null;
|
|
70677
|
-
if (axis === "x") {
|
|
70678
|
-
move(-sign, 0, event);
|
|
70679
|
-
} else {
|
|
70680
|
-
move(0, -sign, event);
|
|
70681
|
-
}
|
|
70682
|
-
return true;
|
|
70683
|
-
},
|
|
70684
|
-
...handlers
|
|
70685
|
-
});
|
|
70686
|
-
if (!gesture) {
|
|
70687
|
-
// Not a press this box can be about — something that reads the pointer
|
|
70688
|
-
// itself, a box below it that travels the same way. Whatever the press
|
|
70689
|
-
// stopped on its way in goes back on its way.
|
|
70690
|
-
handlers.onGiveUp();
|
|
70691
|
-
return;
|
|
70692
|
-
}
|
|
70693
|
-
handlers.drag.gesture = gesture;
|
|
70694
|
-
dragRef.current = handlers.drag;
|
|
70695
|
-
};
|
|
70696
|
-
|
|
70697
|
-
// A wheel pushing the box sideways asks for a SLIDE, not for a place between
|
|
70698
|
-
// two: one push, one slide — the same thing an arrow key asks for, and it
|
|
70699
|
-
// plays at its own pace rather than under a hand (see watchWheelTravel).
|
|
70700
|
-
// Watched for the whole life of the box because such a gesture has no press
|
|
70701
|
-
// to start it: it begins with its first event.
|
|
70702
|
-
//
|
|
70703
|
-
// Reached through a ref and never rebuilt for it: a travel CHANGES which
|
|
70704
|
-
// slide is current, so a watcher listening on that would be torn down halfway
|
|
70705
|
-
// through the very gesture moving it.
|
|
70706
|
-
const travelOneStepRef = useRef(null);
|
|
70707
|
-
travelOneStepRef.current = ({
|
|
70708
|
-
axis,
|
|
70709
|
-
sign,
|
|
70710
|
-
event
|
|
70711
|
-
}) => {
|
|
70712
|
-
if (dragRef.current) {
|
|
70713
|
-
// A hand is holding the slides. They are its until it lets go.
|
|
70714
|
-
return;
|
|
70715
|
-
}
|
|
70716
|
-
if (axis === "x") {
|
|
70717
|
-
move(-sign, 0, event);
|
|
70718
|
-
} else {
|
|
70719
|
-
move(0, -sign, event);
|
|
70720
|
-
}
|
|
70721
|
-
};
|
|
70722
|
-
useLayoutEffect(() => {
|
|
70723
|
-
if (!scrollAxes) {
|
|
70724
|
-
return undefined;
|
|
70725
|
-
}
|
|
70726
|
-
return watchWheelTravel(containerRef.current, {
|
|
70727
|
-
axes: scrollAxes,
|
|
70728
|
-
onStep: detail => travelOneStepRef.current(detail)
|
|
70729
|
-
});
|
|
70730
|
-
}, [scrollAxes]);
|
|
70731
|
-
|
|
70732
|
-
// A gesture is listening on things that outlive this component.
|
|
70733
69654
|
useLayoutEffect(() => {
|
|
70734
69655
|
return () => {
|
|
70735
|
-
dragRef.current?.gesture?.stop();
|
|
70736
|
-
dragRef.current = null;
|
|
70737
69656
|
cancelTravelProgressAnimation();
|
|
70738
69657
|
};
|
|
70739
69658
|
}, []);
|
|
@@ -70862,31 +69781,22 @@ const SlideContainer = ({
|
|
|
70862
69781
|
// names, and to write the place this box stands on (see `signal`).
|
|
70863
69782
|
,
|
|
70864
69783
|
"data-slide-shares-state": sharesState ? "" : undefined
|
|
70865
|
-
// Which axes
|
|
70866
|
-
//
|
|
70867
|
-
// has seen the gesture — and it is also what a box HOLDING this one reads
|
|
70868
|
-
// to know the gesture is not its own (see drag_to_travel.js).
|
|
70869
|
-
,
|
|
70870
|
-
"data-travel-by-drag": dragAxes ?? undefined
|
|
70871
|
-
// …and said as a listener too, from the same render: a touch this box may
|
|
70872
|
-
// take has to be refusable before the finger lands, and being registered
|
|
70873
|
-
// is the whole of it — see keepTouchRefusable for why a JSX prop is
|
|
70874
|
-
// enough here (an element-level touchmove listener is non-passive).
|
|
70875
|
-
// Without it, a swipe the box has taken can be cancelled mid-gesture by
|
|
70876
|
-
// the browser scrolling the page along the axis touch-action leaves it.
|
|
69784
|
+
// Which axes the row scrolls on under a hand, and which of them a finger may
|
|
69785
|
+
// not take (see the css above).
|
|
70877
69786
|
,
|
|
70878
|
-
|
|
70879
|
-
|
|
70880
|
-
//
|
|
69787
|
+
"data-slide-hand": travelAxes ?? undefined,
|
|
69788
|
+
"data-slide-no-touch": Array.from(scrollAxes ?? "").filter(axis => !dragAxes?.includes(axis)).join("") || undefined
|
|
69789
|
+
// Read by what stands around the box: a press landing here may become a
|
|
69790
|
+
// scroll, so a control acting on the press waits for its click, and a box
|
|
69791
|
+
// around that a finger pushes leaves this axis alone (see drag_to_travel.js).
|
|
70881
69792
|
,
|
|
70882
|
-
"data-travel-by-
|
|
70883
|
-
// The same fact, read by the shared gesture stylesheet: what scrolls
|
|
70884
|
-
// inside a box that travels must not spill onto the page behind it (see
|
|
70885
|
-
// drag_to_travel.js).
|
|
70886
|
-
,
|
|
70887
|
-
"data-drag-travel": travelAxes ?? undefined,
|
|
69793
|
+
"data-travel-by-drag": dragAxes ?? undefined,
|
|
70888
69794
|
onPointerDown: e => {
|
|
70889
|
-
|
|
69795
|
+
// A finger on a travel still playing: it is the row's from here, and the
|
|
69796
|
+
// row is where the travel is going.
|
|
69797
|
+
if (e.pointerType !== "mouse") {
|
|
69798
|
+
trackAnimationRef.current?.finish();
|
|
69799
|
+
}
|
|
70890
69800
|
rest.onPointerDown?.(e);
|
|
70891
69801
|
}
|
|
70892
69802
|
// The focusable one, and a Tab stop: a slide is not (see Slide), so the
|
|
@@ -72503,7 +71413,7 @@ const css$H = /* css */`@property --x-popup-group-top {
|
|
|
72503
71413
|
transition: none;
|
|
72504
71414
|
}
|
|
72505
71415
|
|
|
72506
|
-
:
|
|
71416
|
+
:root[data-navi-route-transition] .navi_popup_group:modal {
|
|
72507
71417
|
view-transition-class: navi_popup;
|
|
72508
71418
|
}
|
|
72509
71419
|
|
|
@@ -72511,10 +71421,6 @@ const css$H = /* css */`@property --x-popup-group-top {
|
|
|
72511
71421
|
:root[data-navi-route-transition]:not([data-navi-route-transition-target="area"]) .navi_popup_group:modal {
|
|
72512
71422
|
view-transition-name: match-element;
|
|
72513
71423
|
}
|
|
72514
|
-
|
|
72515
|
-
:root[data-navi-route-travel] .navi_popup_group:modal {
|
|
72516
|
-
view-transition-name: match-element;
|
|
72517
|
-
}
|
|
72518
71424
|
}
|
|
72519
71425
|
`;
|
|
72520
71426
|
|
|
@@ -74472,8 +73378,7 @@ const popupCss = /* css */ `
|
|
|
74472
73378
|
}
|
|
74473
73379
|
}
|
|
74474
73380
|
|
|
74475
|
-
/* While the PAGES are the ones moving
|
|
74476
|
-
an open popup takes a picture of its own. The pictures of such a movement
|
|
73381
|
+
/* While the PAGES are the ones moving, in a route transition, an open popup takes a picture of its own. The pictures of such a movement
|
|
74477
73382
|
are drawn in the top layer, above everything the document paints, and a
|
|
74478
73383
|
popup lives in the top layer too: uncaptured, it is simply covered for the
|
|
74479
73384
|
length of the movement, so it disappears the instant its page starts to
|
|
@@ -74483,9 +73388,7 @@ const popupCss = /* css */ `
|
|
|
74483
73388
|
The class is how navi reaches that picture afterwards: its clock below,
|
|
74484
73389
|
and its place above the pages (the z-order in nav/route_transition.jsx). */
|
|
74485
73390
|
:root[data-navi-route-transition] .navi_popover[aria-expanded="true"],
|
|
74486
|
-
:root[data-navi-route-transition] .navi_dialog[aria-expanded="true"]
|
|
74487
|
-
:root[data-navi-route-travel] .navi_popover[aria-expanded="true"],
|
|
74488
|
-
:root[data-navi-route-travel] .navi_dialog[aria-expanded="true"] {
|
|
73391
|
+
:root[data-navi-route-transition] .navi_dialog[aria-expanded="true"] {
|
|
74489
73392
|
view-transition-class: navi_popup;
|
|
74490
73393
|
}
|
|
74491
73394
|
|
|
@@ -74509,9 +73412,7 @@ const popupCss = /* css */ `
|
|
|
74509
73412
|
:root[data-navi-route-transition]:not(
|
|
74510
73413
|
[data-navi-route-transition-target="area"]
|
|
74511
73414
|
)
|
|
74512
|
-
.navi_dialog[aria-expanded="true"]
|
|
74513
|
-
:root[data-navi-route-travel] .navi_popover[aria-expanded="true"],
|
|
74514
|
-
:root[data-navi-route-travel] .navi_dialog[aria-expanded="true"] {
|
|
73415
|
+
.navi_dialog[aria-expanded="true"] {
|
|
74515
73416
|
view-transition-name: match-element;
|
|
74516
73417
|
}
|
|
74517
73418
|
}
|
|
@@ -74523,13 +73424,192 @@ const popupCss = /* css */ `
|
|
|
74523
73424
|
:root[data-navi-route-transition]::view-transition-new(.navi_popup) {
|
|
74524
73425
|
animation-duration: var(--navi-route-transition-duration, 300ms);
|
|
74525
73426
|
}
|
|
74526
|
-
:root[data-navi-route-travel]::view-transition-group(.navi_popup),
|
|
74527
|
-
:root[data-navi-route-travel]::view-transition-old(.navi_popup),
|
|
74528
|
-
:root[data-navi-route-travel]::view-transition-new(.navi_popup) {
|
|
74529
|
-
animation-duration: var(--navi-route-travel-duration, 300ms);
|
|
74530
|
-
}
|
|
74531
73427
|
`;
|
|
74532
73428
|
|
|
73429
|
+
/**
|
|
73430
|
+
* A modal dialog covered by another modal dialog is not in use, so it does not
|
|
73431
|
+
* answer the keyboard: a sheet raising one leaves what is behind it where it
|
|
73432
|
+
* is, under the keyboard like anything else behind that sheet.
|
|
73433
|
+
*
|
|
73434
|
+
* Two things make an open dialog follow the keyboard, and both are held here
|
|
73435
|
+
* for as long as it is covered:
|
|
73436
|
+
* - its ceiling (--dialog-maxmax-height), which CSS keeps tracking
|
|
73437
|
+
* --navi-app-height. Written back resolved, inline. `freezeSize` cannot do it:
|
|
73438
|
+
* a max-* wins over the width/height it writes;
|
|
73439
|
+
* - its placement, which reads the live viewport. Deferred, see
|
|
73440
|
+
* deferPlacementWhileCovered.
|
|
73441
|
+
*
|
|
73442
|
+
* A dialog back on top answers the screen as it is, at once, with one exception:
|
|
73443
|
+
* a keyboard on its way out. It is still counted for a while after the sheet
|
|
73444
|
+
* closes (the browser takes its time, then --navi-vvh waits for the resize to
|
|
73445
|
+
* settle, see responsive.js), and answering that makes the dialog jump up and
|
|
73446
|
+
* back down. A keyboard belongs to a focused field, so the focus says which case
|
|
73447
|
+
* this is: on a field the keyboard stays, anywhere else it is leaving and the
|
|
73448
|
+
* dialog waits for the viewport to change. No timer: how long a keyboard takes
|
|
73449
|
+
* to leave is unknown, and a duration would also delay a keyboard that stays.
|
|
73450
|
+
*
|
|
73451
|
+
* Only modal dialogs stack here. What sits under a non-modal one is still
|
|
73452
|
+
* usable, and keeps following the keyboard.
|
|
73453
|
+
*/
|
|
73454
|
+
|
|
73455
|
+
|
|
73456
|
+
const CEILING = "--dialog-maxmax-height";
|
|
73457
|
+
|
|
73458
|
+
const modalStack = [];
|
|
73459
|
+
|
|
73460
|
+
/**
|
|
73461
|
+
* @param {HTMLDialogElement} dialogEl - The modal dialog being shown.
|
|
73462
|
+
* @param {object} params
|
|
73463
|
+
* @param {() => void} params.place - Places the dialog against the viewport as
|
|
73464
|
+
* it is. Called on a dialog that was held once it answers the viewport again.
|
|
73465
|
+
* @returns {() => void} To call when the dialog closes.
|
|
73466
|
+
*/
|
|
73467
|
+
const enterModalStack = (dialogEl, { place }) => {
|
|
73468
|
+
const entry = { dialogEl, place, hold: null };
|
|
73469
|
+
const entryBelow = modalStack.at(-1);
|
|
73470
|
+
if (entryBelow) {
|
|
73471
|
+
holdEntry(entryBelow);
|
|
73472
|
+
}
|
|
73473
|
+
modalStack.push(entry);
|
|
73474
|
+
return () => {
|
|
73475
|
+
const index = modalStack.indexOf(entry);
|
|
73476
|
+
const wasOnTop = index === modalStack.length - 1;
|
|
73477
|
+
modalStack.splice(index, 1);
|
|
73478
|
+
dropHold(entry);
|
|
73479
|
+
if (wasOnTop && modalStack.length > 0) {
|
|
73480
|
+
releaseBackOnTop(modalStack.at(-1), { closedDialogEl: dialogEl });
|
|
73481
|
+
}
|
|
73482
|
+
};
|
|
73483
|
+
};
|
|
73484
|
+
|
|
73485
|
+
/**
|
|
73486
|
+
* Answers true when the dialog must not be placed right now, and remembers to
|
|
73487
|
+
* place it on release.
|
|
73488
|
+
*
|
|
73489
|
+
* Only while the room differs from the one it was covered with: the placement
|
|
73490
|
+
* reads the live viewport whatever triggered it, so with the same room it lands
|
|
73491
|
+
* where the dialog already is, and content growing under the sheet is answered
|
|
73492
|
+
* as usual.
|
|
73493
|
+
*/
|
|
73494
|
+
const deferPlacementWhileCovered = (dialogEl) => {
|
|
73495
|
+
const entry = modalStack.find((candidate) => candidate.dialogEl === dialogEl);
|
|
73496
|
+
if (!entry || !entry.hold) {
|
|
73497
|
+
return false;
|
|
73498
|
+
}
|
|
73499
|
+
if (readRoom() === entry.hold.room) {
|
|
73500
|
+
return false;
|
|
73501
|
+
}
|
|
73502
|
+
entry.hold.placementOwed = true;
|
|
73503
|
+
return true;
|
|
73504
|
+
};
|
|
73505
|
+
|
|
73506
|
+
const holdEntry = (entry) => {
|
|
73507
|
+
if (entry.hold) {
|
|
73508
|
+
// Covered again before the keyboard had left: the ceiling it holds is still
|
|
73509
|
+
// the one it had before all of this.
|
|
73510
|
+
entry.hold.stopWaiting?.();
|
|
73511
|
+
entry.hold.stopWaiting = null;
|
|
73512
|
+
return;
|
|
73513
|
+
}
|
|
73514
|
+
const { dialogEl, place } = entry;
|
|
73515
|
+
holdCeiling(dialogEl);
|
|
73516
|
+
entry.hold = {
|
|
73517
|
+
room: readRoom(),
|
|
73518
|
+
placementOwed: false,
|
|
73519
|
+
stopWaiting: null,
|
|
73520
|
+
// The keyboard does not fire this one (see safe_area.md): this is the
|
|
73521
|
+
// window itself changing, a rotation, which a covered dialog has to fit.
|
|
73522
|
+
stopWatchingWindow: subscribeWindowResizeSettled(() => {
|
|
73523
|
+
holdCeiling(dialogEl);
|
|
73524
|
+
entry.hold.room = readRoom();
|
|
73525
|
+
entry.hold.placementOwed = false;
|
|
73526
|
+
place();
|
|
73527
|
+
}),
|
|
73528
|
+
};
|
|
73529
|
+
};
|
|
73530
|
+
|
|
73531
|
+
const releaseBackOnTop = (entry, { closedDialogEl }) => {
|
|
73532
|
+
const { hold } = entry;
|
|
73533
|
+
if (!hold) {
|
|
73534
|
+
return;
|
|
73535
|
+
}
|
|
73536
|
+
// Nothing taken away, or a keyboard that stays: the room read here is the
|
|
73537
|
+
// screen as it is. A field in the dialog that closed is one still leaving: a
|
|
73538
|
+
// dialog leaving the tree only gives the focus back after the commit.
|
|
73539
|
+
const { activeElement } = document;
|
|
73540
|
+
if (
|
|
73541
|
+
readRoom() >= hold.room ||
|
|
73542
|
+
(isEditableTarget(activeElement) && !closedDialogEl.contains(activeElement))
|
|
73543
|
+
) {
|
|
73544
|
+
release(entry);
|
|
73545
|
+
return;
|
|
73546
|
+
}
|
|
73547
|
+
// Any change, not the room it was covered with: the bars of a mobile browser
|
|
73548
|
+
// may come back different, and the wait would never end.
|
|
73549
|
+
const roomWithKeyboardLeaving = readRoom();
|
|
73550
|
+
const releaseIfViewportChanged = () => {
|
|
73551
|
+
if (readRoom() !== roomWithKeyboardLeaving) {
|
|
73552
|
+
release(entry);
|
|
73553
|
+
}
|
|
73554
|
+
};
|
|
73555
|
+
// The focus can come back after the close (a popup leaving the tree gives it
|
|
73556
|
+
// back once the commit is over), and the keyboard then stays after all.
|
|
73557
|
+
const releaseIfFieldFocused = () => {
|
|
73558
|
+
if (isEditableTarget(document.activeElement)) {
|
|
73559
|
+
release(entry);
|
|
73560
|
+
}
|
|
73561
|
+
};
|
|
73562
|
+
document.addEventListener("focusin", releaseIfFieldFocused, {
|
|
73563
|
+
capture: true,
|
|
73564
|
+
});
|
|
73565
|
+
const unsubscribes = [
|
|
73566
|
+
visualViewportHeightSignal.subscribe(releaseIfViewportChanged),
|
|
73567
|
+
subscribeVirtualKeyboardGeometryChange(releaseIfViewportChanged),
|
|
73568
|
+
];
|
|
73569
|
+
hold.stopWaiting = () => {
|
|
73570
|
+
document.removeEventListener("focusin", releaseIfFieldFocused, {
|
|
73571
|
+
capture: true,
|
|
73572
|
+
});
|
|
73573
|
+
for (const unsubscribe of unsubscribes) {
|
|
73574
|
+
unsubscribe();
|
|
73575
|
+
}
|
|
73576
|
+
};
|
|
73577
|
+
};
|
|
73578
|
+
|
|
73579
|
+
const release = (entry) => {
|
|
73580
|
+
const { placementOwed } = entry.hold;
|
|
73581
|
+
dropHold(entry);
|
|
73582
|
+
if (placementOwed) {
|
|
73583
|
+
entry.place();
|
|
73584
|
+
}
|
|
73585
|
+
};
|
|
73586
|
+
|
|
73587
|
+
const dropHold = (entry) => {
|
|
73588
|
+
const { hold, dialogEl } = entry;
|
|
73589
|
+
if (!hold) {
|
|
73590
|
+
return;
|
|
73591
|
+
}
|
|
73592
|
+
hold.stopWaiting?.();
|
|
73593
|
+
hold.stopWatchingWindow();
|
|
73594
|
+
dialogEl.style.removeProperty(CEILING);
|
|
73595
|
+
entry.hold = null;
|
|
73596
|
+
};
|
|
73597
|
+
|
|
73598
|
+
// The declared ceiling rather than the used max-height: it is the one cap that
|
|
73599
|
+
// follows the keyboard, and the other caps (maxHeight) keep their say. Its
|
|
73600
|
+
// computed value has every var() resolved, so it stops following once written
|
|
73601
|
+
// back. Registered with inherits: false, it does not reach a dialog nested
|
|
73602
|
+
// inside this one.
|
|
73603
|
+
const holdCeiling = (dialogEl) => {
|
|
73604
|
+
dialogEl.style.removeProperty(CEILING);
|
|
73605
|
+
const ceiling = getComputedStyle(dialogEl).getPropertyValue(CEILING);
|
|
73606
|
+
dialogEl.style.setProperty(CEILING, ceiling);
|
|
73607
|
+
};
|
|
73608
|
+
|
|
73609
|
+
// An overlaying keyboard leaves the viewport alone and arrives on its own.
|
|
73610
|
+
const readRoom = () =>
|
|
73611
|
+
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
73612
|
+
|
|
74533
73613
|
/**
|
|
74534
73614
|
* Pushing a popup docked to an edge (a side panel, a bottom sheet) back the way
|
|
74535
73615
|
* it came in closes it: it follows the finger while it travels, and letting go
|
|
@@ -76871,6 +75951,9 @@ const useDialogProps = props => {
|
|
|
76871
75951
|
const rectEffect = visibleRectEffect(positionedAncestor, (visibleRect, {
|
|
76872
75952
|
event
|
|
76873
75953
|
}) => {
|
|
75954
|
+
if (deferPlacementWhileCovered(dialogEl)) {
|
|
75955
|
+
return;
|
|
75956
|
+
}
|
|
76874
75957
|
positionDialog(event);
|
|
76875
75958
|
// Only for what can have taken height away from the dialog — a
|
|
76876
75959
|
// scroll never does, and re-scrolling on one would fight the finger
|
|
@@ -76890,6 +75973,17 @@ const useDialogProps = props => {
|
|
|
76890
75973
|
freezeSize(dialogEl);
|
|
76891
75974
|
}
|
|
76892
75975
|
rectEffect.observeSize(dialogEl);
|
|
75976
|
+
if (isModal) {
|
|
75977
|
+
addCleanup(enterModalStack(dialogEl, {
|
|
75978
|
+
// "resize": what it was held from is a viewport change, and it answers
|
|
75979
|
+
// the way it answers one, animated. Called directly because the
|
|
75980
|
+
// effect's dedup already saw that viewport go by.
|
|
75981
|
+
place: () => {
|
|
75982
|
+
positionDialog(new CustomEvent("resize"));
|
|
75983
|
+
keepFocusedElementVisible(dialogEl);
|
|
75984
|
+
}
|
|
75985
|
+
}));
|
|
75986
|
+
}
|
|
76893
75987
|
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
76894
75988
|
// says so, after the placement read it: placed again on the spot, where
|
|
76895
75989
|
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
@@ -80627,6 +79721,13 @@ const PickerCustom = props => {
|
|
|
80627
79721
|
};
|
|
80628
79722
|
},
|
|
80629
79723
|
keyDown: e => {
|
|
79724
|
+
// Pressed in a popup this picker holds: the key is that popup's, and
|
|
79725
|
+
// `false` keeps it from the default reaction too, which would refuse
|
|
79726
|
+
// it on a read-only picker and take a Space from the button it was
|
|
79727
|
+
// pressed on.
|
|
79728
|
+
if (isPressOnLayerOver(e.target, e.currentTarget)) {
|
|
79729
|
+
return false;
|
|
79730
|
+
}
|
|
80630
79731
|
// A shortcut that applies has been through the gate already, under
|
|
80631
79732
|
// its own name and intent: the key is answered. Handed back as the
|
|
80632
79733
|
// reaction, its boolean would be asked again, unnamed — as a write,
|
|
@@ -84133,7 +83234,7 @@ const css$w = /* css */`@property --list-group-label-height {
|
|
|
84133
83234
|
}
|
|
84134
83235
|
|
|
84135
83236
|
@supports (view-transition-group: contain) {
|
|
84136
|
-
:root:not([data-navi-route-transition]
|
|
83237
|
+
:root:not([data-navi-route-transition]) .navi_list_container[data-item-transition] {
|
|
84137
83238
|
view-transition-name: match-element;
|
|
84138
83239
|
view-transition-class: navi_list_transition;
|
|
84139
83240
|
view-transition-group: contain;
|
|
@@ -85655,8 +84756,8 @@ const useListScrollSync = ({
|
|
|
85655
84756
|
return undefined;
|
|
85656
84757
|
}
|
|
85657
84758
|
return routePage.leavingSignal.subscribe(leaving => {
|
|
84759
|
+
pageLeftRef.current = leaving;
|
|
85658
84760
|
if (leaving) {
|
|
85659
|
-
pageLeftRef.current = true;
|
|
85660
84761
|
letGo();
|
|
85661
84762
|
}
|
|
85662
84763
|
});
|
|
@@ -106751,7 +105852,7 @@ const css = /* css */`.navi_side_panel {
|
|
|
106751
105852
|
--dialog-maxmax-width: var(--navi-app-width);
|
|
106752
105853
|
|
|
106753
105854
|
&[navi-side="left"], &[navi-side="right"] {
|
|
106754
|
-
height: var(--navi-side-panel-height, var(--
|
|
105855
|
+
height: var(--navi-side-panel-height, var(--dialog-maxmax-height));
|
|
106755
105856
|
}
|
|
106756
105857
|
|
|
106757
105858
|
&[navi-side="top"], &[navi-side="bottom"] {
|