@jsenv/navi 0.29.522 → 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 +1490 -2393
- package/dist/dev/jsenv_navi.js.map +40 -37
- package/dist/jsenv_navi.js +1490 -2379
- package/dist/jsenv_navi.js.map +40 -37
- 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/dev/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, createEventGroupLogger, 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, createEventGroupLogger, 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, useEffect, useReducer, 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, prefixFirstAndIndentRemainingLines, 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";
|
|
@@ -444,8 +444,7 @@ const flushSyncRendering = (fn) => {
|
|
|
444
444
|
* it was left, and the bottom arrives under a page that has already jumped. So
|
|
445
445
|
* whoever writes an arrival's offset builds first the boxes that offset shows
|
|
446
446
|
* (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
|
|
447
|
-
* right after it (the offset
|
|
448
|
-
* layout.
|
|
447
|
+
* right after it (the offset) reads the layout.
|
|
449
448
|
*/
|
|
450
449
|
|
|
451
450
|
const deferredBoxes = new Set();
|
|
@@ -546,9 +545,8 @@ const readTopInWindow = element => {
|
|
|
546
545
|
* around Preact's `options.debounceRendering`, and two of them installed
|
|
547
546
|
* independently restore each other in the wrong order when they let go — every
|
|
548
547
|
* render queued in between is then handed to a wrapper nobody will ever
|
|
549
|
-
* release. Everything that photographs a navigation
|
|
550
|
-
*
|
|
551
|
-
* wrapper of its own.
|
|
548
|
+
* release. Everything that photographs a navigation must therefore hold through
|
|
549
|
+
* this module, never through a wrapper of its own.
|
|
552
550
|
*/
|
|
553
551
|
|
|
554
552
|
|
|
@@ -10234,8 +10232,8 @@ const haveSameMembers = (setA, setB) => {
|
|
|
10234
10232
|
* A route matching is a signal changing, and the page it selects reaches the
|
|
10235
10233
|
* DOM only once Preact has rendered — an unknown number of passes later, in an
|
|
10236
10234
|
* unknown number of microtasks. Anyone who needs the page as it IS rather than
|
|
10237
|
-
* as it has been decided (a
|
|
10238
|
-
* browser
|
|
10235
|
+
* as it has been decided (a route transition about to have its picture taken by
|
|
10236
|
+
* the browser) waits for this instead of counting.
|
|
10239
10237
|
*
|
|
10240
10238
|
* A page waiting on data is announced too, by the boundary showing its loading
|
|
10241
10239
|
* state (see Loading in use_async_data.jsx): what the container could put on
|
|
@@ -24514,10 +24512,10 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
|
|
|
24514
24512
|
*
|
|
24515
24513
|
* A popup is painted OVER the page and stays a DOM descendant of whatever it
|
|
24516
24514
|
* was written in — a dialog inside the card that opens it, a callout beside the
|
|
24517
|
-
* button it belongs to. Its pointer events therefore bubble through
|
|
24518
|
-
* covers, and a box reading a gesture off one of them answers for
|
|
24519
|
-
* nobody aimed at it: a finger held in a sheet opening the card
|
|
24520
|
-
* sheet,
|
|
24515
|
+
* button it belongs to. Its pointer and key events therefore bubble through
|
|
24516
|
+
* boxes it covers, and a box reading a gesture off one of them answers for
|
|
24517
|
+
* something nobody aimed at it: a finger held in a sheet opening the card
|
|
24518
|
+
* underneath the sheet, an arrow pressed in a sheet walking the slides behind it.
|
|
24521
24519
|
*
|
|
24522
24520
|
* The same list, read for the same reason, as the drag sources in @jsenv/dom
|
|
24523
24521
|
* (see DRAG_IGNORED_SELECTOR in drag_to.js) and as the selection rule in
|
|
@@ -25274,26 +25272,7 @@ const ensureDocumentStartViewTransition = () => {
|
|
|
25274
25272
|
startViewTransitionPolyfill.isPolyfill = true;
|
|
25275
25273
|
document.startViewTransition = startViewTransitionPolyfill;
|
|
25276
25274
|
}
|
|
25277
|
-
return startViewTransition$
|
|
25278
|
-
};
|
|
25279
|
-
|
|
25280
|
-
// A transition a finger is holding still (see route_travel.jsx): it has to be
|
|
25281
|
-
// let go of before any other one starts, and this is the only place that knows
|
|
25282
|
-
// a new one is about to. There is a single transition per document — starting
|
|
25283
|
-
// one SKIPS the one in flight — and the hold that keeps pictures under a finger
|
|
25284
|
-
// is written against whatever transition is running, because everything the
|
|
25285
|
-
// gesture must carry along (a trait under a tab row) belongs to that same
|
|
25286
|
-
// transition and has no name of its own here. Left on, it takes hold of the
|
|
25287
|
-
// transition that has just replaced ours: born paused, held by nobody, it never
|
|
25288
|
-
// finishes, and its pictures stand over a page that cannot be touched anymore.
|
|
25289
|
-
let releaseHeldViewTransition = null;
|
|
25290
|
-
const holdViewTransition = (release) => {
|
|
25291
|
-
releaseHeldViewTransition = release;
|
|
25292
|
-
return () => {
|
|
25293
|
-
if (releaseHeldViewTransition === release) {
|
|
25294
|
-
releaseHeldViewTransition = null;
|
|
25295
|
-
}
|
|
25296
|
-
};
|
|
25275
|
+
return startViewTransition$1;
|
|
25297
25276
|
};
|
|
25298
25277
|
|
|
25299
25278
|
// The last transition started here, for code that runs a callback of the
|
|
@@ -25303,12 +25282,7 @@ const holdViewTransition = (release) => {
|
|
|
25303
25282
|
let lastViewTransition = null;
|
|
25304
25283
|
const peekLastViewTransition = () => lastViewTransition;
|
|
25305
25284
|
|
|
25306
|
-
const startViewTransition$
|
|
25307
|
-
if (releaseHeldViewTransition) {
|
|
25308
|
-
const release = releaseHeldViewTransition;
|
|
25309
|
-
releaseHeldViewTransition = null;
|
|
25310
|
-
release();
|
|
25311
|
-
}
|
|
25285
|
+
const startViewTransition$1 = (updateCallback) => {
|
|
25312
25286
|
const viewTransition = document.startViewTransition(updateCallback);
|
|
25313
25287
|
viewTransition.updateCallbackDone.catch(ignoreSkip);
|
|
25314
25288
|
viewTransition.ready.catch(ignoreSkip);
|
|
@@ -27301,6 +27275,9 @@ const createOnKeyDownForShortcutArray = (
|
|
|
27301
27275
|
};
|
|
27302
27276
|
};
|
|
27303
27277
|
/**
|
|
27278
|
+
* A key pressed in a popup nested in the listening element is not offered to
|
|
27279
|
+
* these shortcuts: see applyKeyboardShortcuts.
|
|
27280
|
+
*
|
|
27304
27281
|
* @param {object} shortcuts
|
|
27305
27282
|
* @param {object} [options]
|
|
27306
27283
|
* @param {(target: Element) => boolean} [options.ownsTarget] Whether a
|
|
@@ -27326,6 +27303,13 @@ const createOnKeyDownForShortcuts = (shortcuts, { ownsTarget } = {}) => {
|
|
|
27326
27303
|
const applyKeyboardShortcuts = (shortcuts, keyboardEvent, { ownsTarget }) => {
|
|
27327
27304
|
const currentTarget = keyboardEvent.currentTarget;
|
|
27328
27305
|
const target = keyboardEvent.target;
|
|
27306
|
+
// A popup stays a DOM descendant of what it was written in, so a key pressed
|
|
27307
|
+
// in it bubbles through boxes the reader cannot reach while it is open. Those
|
|
27308
|
+
// boxes have no shortcut to answer it with: an arrow in a sheet does not walk
|
|
27309
|
+
// the slides behind it, and Escape closes the popup it was pressed in.
|
|
27310
|
+
if (isPressOnLayerOver(target, currentTarget)) {
|
|
27311
|
+
return null;
|
|
27312
|
+
}
|
|
27329
27313
|
let canIntercept;
|
|
27330
27314
|
if (target === currentTarget) {
|
|
27331
27315
|
// the event occurs on the element itself so we are sure we won't interfere with native behaviors of child elements
|
|
@@ -27561,6 +27545,9 @@ const isSameKey = (browserEventKey, key) => {
|
|
|
27561
27545
|
* well, whether the gate lets the interaction through or refuses it and says
|
|
27562
27546
|
* why — the same as a number field's arrows.
|
|
27563
27547
|
*
|
|
27548
|
+
* A key pressed in a popup nested in the element is not this element's, like a
|
|
27549
|
+
* press there (see isPressOnLayerOver).
|
|
27550
|
+
*
|
|
27564
27551
|
* The interaction is dispatched under its full name, colon included, so
|
|
27565
27552
|
* `findEvent(event, "keyboard:ctrl+backspace")` gets back to it — the shortcut
|
|
27566
27553
|
* that asked is what the action wants to know, not that a key went down.
|
|
@@ -33139,8 +33126,7 @@ const LoadingFallback = ({
|
|
|
33139
33126
|
// A page that suspends never gets to say it is on screen — its own effects
|
|
33140
33127
|
// are held with it — so this says it for it: what the document shows of the
|
|
33141
33128
|
// page arriving is this. Anyone waiting for the page to be there before
|
|
33142
|
-
// moving (a
|
|
33143
|
-
// would otherwise wait for a render that cannot happen until the data does.
|
|
33129
|
+
// moving (a route transition about to have its picture taken) would otherwise wait for a render that cannot happen until the data does.
|
|
33144
33130
|
useLayoutEffect(() => {
|
|
33145
33131
|
publishRouteRender();
|
|
33146
33132
|
});
|
|
@@ -37628,29 +37614,6 @@ const samePages = (pages, otherPages) => {
|
|
|
37628
37614
|
|
|
37629
37615
|
installImportMetaCssBuild(import.meta);
|
|
37630
37616
|
|
|
37631
|
-
/**
|
|
37632
|
-
* Keep every container showing the page it is showing, whatever the routes say.
|
|
37633
|
-
*
|
|
37634
|
-
* For a caller whose picture of a page is LIVE and must not follow the router:
|
|
37635
|
-
* a travel being undone shows the page it is going back to on both sides at
|
|
37636
|
-
* once if the router is allowed to swap under it (see route_travel.jsx). Only
|
|
37637
|
-
* the pages are held still — the rest of the document goes on rendering, which
|
|
37638
|
-
* is the whole reason this lives here rather than in Preact's own scheduler.
|
|
37639
|
-
*/
|
|
37640
|
-
let routeRenderFrozen = false;
|
|
37641
|
-
const routeRenderFrozenSignal = signal(0);
|
|
37642
|
-
const freezeRouteRender = () => {
|
|
37643
|
-
routeRenderFrozen = true;
|
|
37644
|
-
return () => {
|
|
37645
|
-
if (!routeRenderFrozen) {
|
|
37646
|
-
return;
|
|
37647
|
-
}
|
|
37648
|
-
routeRenderFrozen = false;
|
|
37649
|
-
// Read during every container's render, so letting go brings them all back.
|
|
37650
|
-
routeRenderFrozenSignal.value++;
|
|
37651
|
-
};
|
|
37652
|
-
};
|
|
37653
|
-
|
|
37654
37617
|
/**
|
|
37655
37618
|
* Keep the page a container leaves, hidden, until its route transition is over.
|
|
37656
37619
|
*
|
|
@@ -37721,8 +37684,11 @@ const debug$1 = (...args) => {
|
|
|
37721
37684
|
* collectBranches).
|
|
37722
37685
|
*/
|
|
37723
37686
|
const Route = props => {
|
|
37687
|
+
const inRow = useContext(RouteRowContext);
|
|
37724
37688
|
if (props.children) {
|
|
37725
|
-
return jsx(
|
|
37689
|
+
return inRow ? jsx(RouteRowContainer, {
|
|
37690
|
+
...props
|
|
37691
|
+
}) : jsx(RouteContainer, {
|
|
37726
37692
|
...props
|
|
37727
37693
|
});
|
|
37728
37694
|
}
|
|
@@ -37786,6 +37752,105 @@ const collectRoutePages = children => {
|
|
|
37786
37752
|
return pages;
|
|
37787
37753
|
};
|
|
37788
37754
|
|
|
37755
|
+
/**
|
|
37756
|
+
* Whether the next container down is a row a hand scrolls: every page has a
|
|
37757
|
+
* place on it from the start, and the browser moves between them (see
|
|
37758
|
+
* RouteTravel's `nativeScroll`). Told to one container, not to the pages it holds.
|
|
37759
|
+
*/
|
|
37760
|
+
const RouteRowContext = createContext(false);
|
|
37761
|
+
|
|
37762
|
+
// A container as a row: one pane per leaf, in the order they are written. A page
|
|
37763
|
+
// still builds when its route first matches, since a route's actions only ask
|
|
37764
|
+
// while it does: before that its pane is empty. Once built it stays, with what
|
|
37765
|
+
// it holds and where it was scrolled to, so a hand brings back the page itself
|
|
37766
|
+
// and the address coming back to it finds it there.
|
|
37767
|
+
//
|
|
37768
|
+
// A page not on screen is treated as one kept while leaving, except that this
|
|
37769
|
+
// can be undone: it does not re-render for what it reads (isShown), and it steps
|
|
37770
|
+
// out of whatever it fills outside its pane (leavingSignal).
|
|
37771
|
+
const RouteRowContainer = ({
|
|
37772
|
+
children
|
|
37773
|
+
}) => {
|
|
37774
|
+
const {
|
|
37775
|
+
activeBranch,
|
|
37776
|
+
leaves
|
|
37777
|
+
} = collectBranches(children);
|
|
37778
|
+
const childrenRef = useRef(null);
|
|
37779
|
+
childrenRef.current = children;
|
|
37780
|
+
const readShownBranchRef = useRef(null);
|
|
37781
|
+
if (!readShownBranchRef.current) {
|
|
37782
|
+
readShownBranchRef.current = () => collectBranches(childrenRef.current).activeBranch;
|
|
37783
|
+
}
|
|
37784
|
+
const parentPage = useContext(RoutePageContext);
|
|
37785
|
+
const pagesRef = useRef(new Map());
|
|
37786
|
+
const pageOf = branch => {
|
|
37787
|
+
let page = pagesRef.current.get(branch.id);
|
|
37788
|
+
if (!page) {
|
|
37789
|
+
page = createRoutePage(readShownBranchRef.current, parentPage);
|
|
37790
|
+
pagesRef.current.set(branch.id, page);
|
|
37791
|
+
}
|
|
37792
|
+
page.branchId = branch.id;
|
|
37793
|
+
// Never answering for another pane: two leaves rendering the same element
|
|
37794
|
+
// are two pages here, each in its pane, where a container carries one over.
|
|
37795
|
+
page.element = null;
|
|
37796
|
+
return page;
|
|
37797
|
+
};
|
|
37798
|
+
const shownPage = activeBranch ? pageOf(activeBranch) : null;
|
|
37799
|
+
const containerKey = useRef({}).current;
|
|
37800
|
+
useLayoutEffect(() => {
|
|
37801
|
+
publishRouteRender();
|
|
37802
|
+
reportContainerShows(containerKey, {
|
|
37803
|
+
parentPage,
|
|
37804
|
+
page: shownPage,
|
|
37805
|
+
branchType: activeBranch ? activeBranch.type : null,
|
|
37806
|
+
routePage: branchRoutePage(activeBranch)
|
|
37807
|
+
});
|
|
37808
|
+
});
|
|
37809
|
+
useLayoutEffect(() => {
|
|
37810
|
+
return () => {
|
|
37811
|
+
forgetContainer(containerKey);
|
|
37812
|
+
};
|
|
37813
|
+
}, []);
|
|
37814
|
+
if (activeBranch && activeBranch.type === "fallback") {
|
|
37815
|
+
// An address the row has no place for: nothing to scroll between.
|
|
37816
|
+
return jsx(RouteRowPane, {
|
|
37817
|
+
branch: activeBranch,
|
|
37818
|
+
page: shownPage,
|
|
37819
|
+
shown: true
|
|
37820
|
+
}, "fallback");
|
|
37821
|
+
}
|
|
37822
|
+
return leaves.map(leaf => jsx(RouteRowPane, {
|
|
37823
|
+
branch: leaf,
|
|
37824
|
+
page: pageOf(leaf),
|
|
37825
|
+
shown: activeBranch !== null && leaf.id === activeBranch.id
|
|
37826
|
+
}, leaf.id));
|
|
37827
|
+
};
|
|
37828
|
+
const RouteRowPane = ({
|
|
37829
|
+
branch,
|
|
37830
|
+
page,
|
|
37831
|
+
shown
|
|
37832
|
+
}) => {
|
|
37833
|
+
// The very vnode rendered last, while not on screen: Preact does not look into
|
|
37834
|
+
// it, and a page nobody can see costs nothing when the address moves.
|
|
37835
|
+
const contentRef = useRef(null);
|
|
37836
|
+
if (shown) {
|
|
37837
|
+
contentRef.current = h(RouteRowContext.Provider, {
|
|
37838
|
+
value: false
|
|
37839
|
+
}, h(RoutePageContext.Provider, {
|
|
37840
|
+
value: page
|
|
37841
|
+
}, branch.node));
|
|
37842
|
+
}
|
|
37843
|
+
useLayoutEffect(() => {
|
|
37844
|
+
page.ownLeavingSignal.value = !shown;
|
|
37845
|
+
}, [shown]);
|
|
37846
|
+
return jsx("div", {
|
|
37847
|
+
"data-route-row-pane": "",
|
|
37848
|
+
"data-built": contentRef.current ? "" : undefined,
|
|
37849
|
+
inert: !shown,
|
|
37850
|
+
children: contentRef.current
|
|
37851
|
+
});
|
|
37852
|
+
};
|
|
37853
|
+
|
|
37789
37854
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
37790
37855
|
// and renders only that branch — or the fallback if nothing matches.
|
|
37791
37856
|
// No contexts, no state of its own: it reads the route signals directly, and
|
|
@@ -37802,16 +37867,7 @@ const RouteContainer = ({
|
|
|
37802
37867
|
pages
|
|
37803
37868
|
} = collectBranches(children);
|
|
37804
37869
|
const fallbackPage = fallbackBranch ? fallbackBranch.page : null;
|
|
37805
|
-
|
|
37806
|
-
// Told to hold still: what is on screen stays on screen. Kept as the very
|
|
37807
|
-
// vnode that was rendered last time, which is how Preact is told there is
|
|
37808
|
-
// nothing to look at in that subtree.
|
|
37809
|
-
const frozen = routeRenderFrozenSignal.value >= 0 && routeRenderFrozen;
|
|
37810
|
-
const shownBranchRef = useRef(null);
|
|
37811
|
-
if (!frozen) {
|
|
37812
|
-
shownBranchRef.current = activeBranch;
|
|
37813
|
-
}
|
|
37814
|
-
const branch = shownBranchRef.current || activeBranch;
|
|
37870
|
+
const branch = activeBranch;
|
|
37815
37871
|
// What this container would show if it rendered now, by the same rule as
|
|
37816
37872
|
// the render above — asked from outside any render (see isShown below).
|
|
37817
37873
|
const childrenRef = useRef(null);
|
|
@@ -37819,9 +37875,6 @@ const RouteContainer = ({
|
|
|
37819
37875
|
const readShownBranchRef = useRef(null);
|
|
37820
37876
|
if (!readShownBranchRef.current) {
|
|
37821
37877
|
readShownBranchRef.current = () => {
|
|
37822
|
-
if (routeRenderFrozen) {
|
|
37823
|
-
return shownBranchRef.current;
|
|
37824
|
-
}
|
|
37825
37878
|
return collectBranches(childrenRef.current).activeBranch;
|
|
37826
37879
|
};
|
|
37827
37880
|
}
|
|
@@ -38116,7 +38169,7 @@ const branchRouteNode = branch => {
|
|
|
38116
38169
|
// Walk JSX children vnodes (without rendering) to build a branch list and
|
|
38117
38170
|
// find the active one in the same pass. Anything that is not a <Route> is read
|
|
38118
38171
|
// through and kept around the branch it holds (see below).
|
|
38119
|
-
// Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
|
|
38172
|
+
// Returns { matchingBranch, fallbackBranch, activeBranch, pages, leaves }.
|
|
38120
38173
|
//
|
|
38121
38174
|
// Each branch is given an id from where it is written among the children,
|
|
38122
38175
|
// which is what tells a container it changed page: the vnodes themselves are
|
|
@@ -38131,6 +38184,8 @@ const collectBranches = (children, path = "") => {
|
|
|
38131
38184
|
// matching or a leaf matching under it is enough to select it, and the
|
|
38132
38185
|
// fallback then stands down.
|
|
38133
38186
|
const pages = [];
|
|
38187
|
+
// Every leaf, wrappers kept, whichever matches: what a row has a pane for.
|
|
38188
|
+
const leaves = [];
|
|
38134
38189
|
const visit = child => {
|
|
38135
38190
|
if (!child || child === true || child === false) {
|
|
38136
38191
|
return;
|
|
@@ -38160,11 +38215,15 @@ const collectBranches = (children, path = "") => {
|
|
|
38160
38215
|
const {
|
|
38161
38216
|
matchingBranch: matchingInside,
|
|
38162
38217
|
fallbackBranch: fallbackInside,
|
|
38163
|
-
pages: pagesInside
|
|
38218
|
+
pages: pagesInside,
|
|
38219
|
+
leaves: leavesInside
|
|
38164
38220
|
} = collectBranches(wrapperChildren, `${id}.`);
|
|
38165
38221
|
for (const pageInside of pagesInside) {
|
|
38166
38222
|
pages.push(pageInside);
|
|
38167
38223
|
}
|
|
38224
|
+
for (const leafInside of leavesInside) {
|
|
38225
|
+
leaves.push(wrapBranch(leafInside, child));
|
|
38226
|
+
}
|
|
38168
38227
|
if (matchingInside && !matchingBranch) {
|
|
38169
38228
|
matchingBranch = wrapBranch(matchingInside, child);
|
|
38170
38229
|
}
|
|
@@ -38237,6 +38296,7 @@ const collectBranches = (children, path = "") => {
|
|
|
38237
38296
|
id,
|
|
38238
38297
|
node: child
|
|
38239
38298
|
};
|
|
38299
|
+
leaves.push(branch);
|
|
38240
38300
|
pages.push({
|
|
38241
38301
|
route,
|
|
38242
38302
|
params: routeParams
|
|
@@ -38265,7 +38325,8 @@ const collectBranches = (children, path = "") => {
|
|
|
38265
38325
|
matchingBranch,
|
|
38266
38326
|
fallbackBranch,
|
|
38267
38327
|
activeBranch,
|
|
38268
|
-
pages
|
|
38328
|
+
pages,
|
|
38329
|
+
leaves
|
|
38269
38330
|
};
|
|
38270
38331
|
};
|
|
38271
38332
|
const describeContainer = ({
|
|
@@ -38435,14 +38496,7 @@ const releaseTransitionDestination = (owner) => {
|
|
|
38435
38496
|
*
|
|
38436
38497
|
* A control therefore may ask to keep answering, and while a route transition
|
|
38437
38498
|
* plays the press is caught at the document and handed to it when it fell
|
|
38438
|
-
* inside its rectangle — which is where the hand thinks it pressed.
|
|
38439
|
-
* move route_travel.jsx makes for the box a finger reaches for mid-flight.
|
|
38440
|
-
*
|
|
38441
|
-
* A `RouteTravel` holds this too, for the bars it leaves live. A control there
|
|
38442
|
-
* gets its click on its own except in the first frames of the travel, when a
|
|
38443
|
-
* press falls to the document root like any other; the part of a bar photographed
|
|
38444
|
-
* for the travel (`data-navi-fixed-bar-overhang`, fixed_bar.jsx) is deaf
|
|
38445
|
-
* throughout.
|
|
38499
|
+
* inside its rectangle — which is where the hand thinks it pressed.
|
|
38446
38500
|
*
|
|
38447
38501
|
* ASKED FOR, never derived, and that is the whole safety of it: only the
|
|
38448
38502
|
* application knows the control does not move. One that travels with the pages
|
|
@@ -39369,7 +39423,7 @@ const holdTransitionWindow = (
|
|
|
39369
39423
|
) => {
|
|
39370
39424
|
const bandBefore = stateBefore.band;
|
|
39371
39425
|
const rectAfter = element.getBoundingClientRect();
|
|
39372
|
-
|
|
39426
|
+
readBand();
|
|
39373
39427
|
const outerScroll = readOuterScroll(element);
|
|
39374
39428
|
// The state being left, at where its box stands under the offset the box
|
|
39375
39429
|
// is measured under NOW: what scrolled outside the box between the two
|
|
@@ -39403,9 +39457,7 @@ const holdTransitionWindow = (
|
|
|
39403
39457
|
setTransitionValue(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
|
|
39404
39458
|
setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
|
|
39405
39459
|
setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
|
|
39406
|
-
const oldBand =
|
|
39407
|
-
? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
|
|
39408
|
-
: bandBefore;
|
|
39460
|
+
const oldBand = bandBefore;
|
|
39409
39461
|
setTransitionValue(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
|
|
39410
39462
|
setTransitionValue(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
|
|
39411
39463
|
setTransitionValue(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
|
|
@@ -39446,21 +39498,6 @@ const placeTransitionWindow = (owner, groupName) => {
|
|
|
39446
39498
|
document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
|
|
39447
39499
|
};
|
|
39448
39500
|
|
|
39449
|
-
/**
|
|
39450
|
-
* Whether a point of the window lies in a band the furniture keeps: the pictures
|
|
39451
|
-
* are cut at those bands, so what is seen there is never a page, however far the
|
|
39452
|
-
* box runs under it.
|
|
39453
|
-
*/
|
|
39454
|
-
const isPointInTransitionBand = (clientX, clientY) => {
|
|
39455
|
-
const band = readBand();
|
|
39456
|
-
return (
|
|
39457
|
-
clientX < band.left ||
|
|
39458
|
-
clientX > window.innerWidth - band.right ||
|
|
39459
|
-
clientY < band.top ||
|
|
39460
|
-
clientY > window.innerHeight - band.bottom
|
|
39461
|
-
);
|
|
39462
|
-
};
|
|
39463
|
-
|
|
39464
39501
|
// The live layout takes the box back. A discontinuity by construction — the
|
|
39465
39502
|
// window stands at the held height, the box is at its own — and an invisible
|
|
39466
39503
|
// one: the page arriving is fully in place, and the strip below it that the
|
|
@@ -39543,8 +39580,7 @@ const shiftRect = (rect, byTop, byLeft) => {
|
|
|
39543
39580
|
// What has scrolled OUTSIDE the box: the offsets of every scroller above the
|
|
39544
39581
|
// one the pages scroll in, the viewport's included. That nearest scroller is
|
|
39545
39582
|
// the pages' own — an offset there is a page scrolled, and it is not counted;
|
|
39546
|
-
// a hidden overflow counts as one
|
|
39547
|
-
// travel asks about (see pagesScrollTheDocument in route_travel.jsx). What is
|
|
39583
|
+
// a hidden overflow counts as one. What is
|
|
39548
39584
|
// pinned to the viewport (fixed, in the top layer) is not carried by the
|
|
39549
39585
|
// document's offset, and the walk stops under it.
|
|
39550
39586
|
const readOuterScroll = (element) => {
|
|
@@ -39612,40 +39648,6 @@ const readBand = () => {
|
|
|
39612
39648
|
};
|
|
39613
39649
|
};
|
|
39614
39650
|
|
|
39615
|
-
// How far the cut may be opened for the picture being left when the furniture
|
|
39616
|
-
// is LIVE under the pictures — the band its own state kept free, which is the
|
|
39617
|
-
// whole point of photographing it: furniture present in only one of the two
|
|
39618
|
-
// states is not the frame, it is part of what changes, and cutting the page
|
|
39619
|
-
// being left at a bar it never had shows its header being sliced instead of
|
|
39620
|
-
// leaving.
|
|
39621
|
-
//
|
|
39622
|
-
// Never past what the picture ARRIVING needs, though: the cut is one line for
|
|
39623
|
-
// both pictures, so opening it for one opens it for the other. A box that runs
|
|
39624
|
-
// under the live furniture it arrives beside — a page scrolled below a top bar
|
|
39625
|
-
// — would then be watched painting over that bar for the length of the
|
|
39626
|
-
// movement, which is what the cut exists to prevent. On such an edge the
|
|
39627
|
-
// arriving band stands, and the page being left is cut as it always was.
|
|
39628
|
-
//
|
|
39629
|
-
// With the furniture photographed (a route transition) none of this holds: a
|
|
39630
|
-
// bar only the arriving state has is a picture ordered over the page it
|
|
39631
|
-
// belongs to, and a page arriving under it paints nothing over it. The band
|
|
39632
|
-
// of the state being left is then published as it was, so the page being
|
|
39633
|
-
// left is never cut at a bar it never had.
|
|
39634
|
-
const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
|
|
39635
|
-
return {
|
|
39636
|
-
top: rectAfter.top < bandAfter.top ? bandAfter.top : bandBefore.top,
|
|
39637
|
-
right:
|
|
39638
|
-
rectAfter.right > window.innerWidth - bandAfter.right
|
|
39639
|
-
? bandAfter.right
|
|
39640
|
-
: bandBefore.right,
|
|
39641
|
-
bottom:
|
|
39642
|
-
rectAfter.bottom > window.innerHeight - bandAfter.bottom
|
|
39643
|
-
? bandAfter.bottom
|
|
39644
|
-
: bandBefore.bottom,
|
|
39645
|
-
left: rectAfter.left < bandAfter.left ? bandAfter.left : bandBefore.left,
|
|
39646
|
-
};
|
|
39647
|
-
};
|
|
39648
|
-
|
|
39649
39651
|
/**
|
|
39650
39652
|
* A view transition already playing, walked back to where it started.
|
|
39651
39653
|
*
|
|
@@ -39821,7 +39823,7 @@ const easedProgress = (x, [x1, y1, x2, y2]) => {
|
|
|
39821
39823
|
};
|
|
39822
39824
|
|
|
39823
39825
|
installImportMetaCssBuild(import.meta);
|
|
39824
|
-
const startViewTransition
|
|
39826
|
+
const startViewTransition = ensureDocumentStartViewTransition();
|
|
39825
39827
|
const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
|
|
39826
39828
|
const TRANSITION_TYPE_ATTRIBUTE = "data-navi-route-transition-type";
|
|
39827
39829
|
const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
|
|
@@ -39853,12 +39855,6 @@ const AREA_NAME = "navi-route-transition";
|
|
|
39853
39855
|
// the pages', or the document's own when the pages ARE the document (see
|
|
39854
39856
|
// findLeadAnimation).
|
|
39855
39857
|
const LEAD_NAMES = [AREA_NAME, "root"];
|
|
39856
|
-
// route_travel.jsx wears this on the root for the length of one of its
|
|
39857
|
-
// travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
|
|
39858
|
-
// name rather than imported: importing route_travel.jsx would pull the whole
|
|
39859
|
-
// travel machinery into an application that only defines transitions.
|
|
39860
|
-
const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
39861
|
-
|
|
39862
39858
|
// The same movements, written once and played on either target: the document,
|
|
39863
39859
|
// or the marked area. The guard keeps the two exclusive — with an area marked,
|
|
39864
39860
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
@@ -40670,10 +40666,10 @@ const rebuildWatcher = () => {
|
|
|
40670
40666
|
from,
|
|
40671
40667
|
to
|
|
40672
40668
|
} of relations) {
|
|
40673
|
-
if (from && pageIndexOf
|
|
40669
|
+
if (from && pageIndexOf(pages, from) === -1) {
|
|
40674
40670
|
pages.push(from);
|
|
40675
40671
|
}
|
|
40676
|
-
if (pageIndexOf
|
|
40672
|
+
if (pageIndexOf(pages, to) === -1) {
|
|
40677
40673
|
pages.push(to);
|
|
40678
40674
|
}
|
|
40679
40675
|
}
|
|
@@ -40761,7 +40757,7 @@ const rebuildWatcher = () => {
|
|
|
40761
40757
|
// What plays when no relation matched (see defineRouteDefaultTransition), what
|
|
40762
40758
|
// the navigation now landing asked for on its own (see readNavigationRequest),
|
|
40763
40759
|
// where it goes, and whether it found an answer already — a relation's
|
|
40764
|
-
// transition, a "none"
|
|
40760
|
+
// transition, a "none". The last three are read at the
|
|
40765
40761
|
// start of every navigation, so they are always about the latest one.
|
|
40766
40762
|
let defaultTransition = null;
|
|
40767
40763
|
let navigationRequest = null;
|
|
@@ -41012,14 +41008,6 @@ const beginTransition = ({
|
|
|
41012
41008
|
}) => {
|
|
41013
41009
|
navigationAnimated = true;
|
|
41014
41010
|
const documentElement = document.documentElement;
|
|
41015
|
-
// One navigation, one animator. A RouteTravel box already travelling this
|
|
41016
|
-
// change owns the document's transition — and possibly a finger; starting
|
|
41017
|
-
// one here on top would skip its pictures mid-slide. A pair of routes must
|
|
41018
|
-
// be animated by RouteTravel or by a route transition, never both.
|
|
41019
|
-
if (documentElement.hasAttribute(ROUTE_TRAVEL_ATTRIBUTE)) {
|
|
41020
|
-
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.");
|
|
41021
|
-
return;
|
|
41022
|
-
}
|
|
41023
41011
|
navigationDecision = {
|
|
41024
41012
|
type,
|
|
41025
41013
|
direction,
|
|
@@ -41104,7 +41092,7 @@ const beginTransition = ({
|
|
|
41104
41092
|
// calls that callback a frame later, and a navigation that has already
|
|
41105
41093
|
// been decided renders its page in between — a wait armed then waits for
|
|
41106
41094
|
// something that has already happened.
|
|
41107
|
-
const renderWait = armRouteRenderWait
|
|
41095
|
+
const renderWait = armRouteRenderWait();
|
|
41108
41096
|
// What the browser ACTUALLY captured, read once the pictures exist: it is
|
|
41109
41097
|
// the only place the silent misconfigurations show. They are all about the
|
|
41110
41098
|
// same thing — a movement playing on pictures that are not the pages.
|
|
@@ -41133,7 +41121,7 @@ const beginTransition = ({
|
|
|
41133
41121
|
break;
|
|
41134
41122
|
}
|
|
41135
41123
|
};
|
|
41136
|
-
const viewTransition = startViewTransition
|
|
41124
|
+
const viewTransition = startViewTransition(async () => {
|
|
41137
41125
|
// The picture the browser is about to take must be of the page that was
|
|
41138
41126
|
// asked for, and a route matching is not yet a page rendered. Whatever
|
|
41139
41127
|
// is awaited here must be able to resolve without a frame: the document
|
|
@@ -41213,7 +41201,7 @@ const beginTransition = ({
|
|
|
41213
41201
|
}
|
|
41214
41202
|
};
|
|
41215
41203
|
transition.viewTransition = viewTransition;
|
|
41216
|
-
viewTransition.ready.then(viewTransitionReady, ignoreSkipped
|
|
41204
|
+
viewTransition.ready.then(viewTransitionReady, ignoreSkipped);
|
|
41217
41205
|
viewTransition.finished.then(end, end);
|
|
41218
41206
|
};
|
|
41219
41207
|
|
|
@@ -41321,7 +41309,7 @@ const turnRunningTransitionRound = (fromUrl, url) => {
|
|
|
41321
41309
|
const absoluteUrl = url => url ? new URL(url, window.location.href).href : null;
|
|
41322
41310
|
|
|
41323
41311
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
41324
|
-
const ignoreSkipped
|
|
41312
|
+
const ignoreSkipped = () => {};
|
|
41325
41313
|
|
|
41326
41314
|
// The names the browser captured, read off the pictures themselves: what was
|
|
41327
41315
|
// asked for in CSS and what was taken are not the same question (see
|
|
@@ -41390,7 +41378,7 @@ const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
|
|
|
41390
41378
|
continue;
|
|
41391
41379
|
}
|
|
41392
41380
|
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.";
|
|
41393
|
-
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
|
|
41381
|
+
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).`);
|
|
41394
41382
|
return;
|
|
41395
41383
|
}
|
|
41396
41384
|
};
|
|
@@ -41411,7 +41399,7 @@ const warnOnce = (id, message) => {
|
|
|
41411
41399
|
// answer it is its own business, and the render is the moment the picture can
|
|
41412
41400
|
// be taken. Listening starts before the change, or a render landing while the
|
|
41413
41401
|
// change settles is a render nobody heard.
|
|
41414
|
-
const armRouteRenderWait
|
|
41402
|
+
const armRouteRenderWait = () => {
|
|
41415
41403
|
let stopListening;
|
|
41416
41404
|
const rendered = new Promise(resolve => {
|
|
41417
41405
|
stopListening = observeRouteRender(resolve);
|
|
@@ -41451,7 +41439,7 @@ const samePage$1 = (a, b) => {
|
|
|
41451
41439
|
}
|
|
41452
41440
|
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
41453
41441
|
};
|
|
41454
|
-
const pageIndexOf
|
|
41442
|
+
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage$1(candidate, page));
|
|
41455
41443
|
|
|
41456
41444
|
// The FIRST page that answers, and every page read all the same: a page that
|
|
41457
41445
|
// is not the current one today is the one that must wake the reader tomorrow.
|
|
@@ -41534,1454 +41522,720 @@ const installRouteTransitionCss = () => {
|
|
|
41534
41522
|
installTransitionFurnitureCss();
|
|
41535
41523
|
};
|
|
41536
41524
|
|
|
41537
|
-
|
|
41538
|
-
|
|
41539
|
-
|
|
41540
|
-
|
|
41541
|
-
|
|
41542
|
-
|
|
41543
|
-
|
|
41544
|
-
|
|
41545
|
-
|
|
41546
|
-
const
|
|
41547
|
-
const startViewTransition = ensureDocumentStartViewTransition();
|
|
41548
|
-
|
|
41549
|
-
// route_transition.js reads this attribute by name (not imported, see its
|
|
41550
|
-
// ROUTE_TRAVEL_ATTRIBUTE) to refuse animating a navigation a travel already
|
|
41551
|
-
// animates — keep the two in sync.
|
|
41552
|
-
const TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
41553
|
-
// Which way the pages move, said on the document: the pictures of a transition
|
|
41554
|
-
// hang off the root, not off the box that travels, so the box's own `axis` has
|
|
41555
|
-
// to be lent to the document for the length of the travel.
|
|
41556
|
-
const TRAVEL_AXIS_ATTRIBUTE = "data-navi-route-travel-axis";
|
|
41557
|
-
// While a finger holds the travel: the pictures stand still and go exactly
|
|
41558
|
-
// where it says (see the CSS, and scrubTravel).
|
|
41559
|
-
const HOLD_ATTRIBUTE = "data-navi-route-travel-held";
|
|
41560
|
-
// A travel a finger set off, for its whole life — including what plays out
|
|
41561
|
-
// after the finger is gone. It moves by another law than one asked for by a
|
|
41562
|
-
// press (see the CSS).
|
|
41563
|
-
const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
41564
|
-
// A travel that changed its mind about where it was going (see
|
|
41565
|
-
// redirectTravel). Only the pages can be aimed somewhere else: everything the
|
|
41566
|
-
// transition carries was measured once, at the start, against a destination
|
|
41567
|
-
// this travel is no longer going to.
|
|
41568
|
-
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
41569
|
-
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
41570
|
-
const TRAVEL_NAME = "navi-route-travel";
|
|
41571
|
-
const css$16 = /* css */`.navi_route_travel {
|
|
41572
|
-
position: relative;
|
|
41525
|
+
/**
|
|
41526
|
+
* Where a row of routes scrolled by the browser stands, for what is drawn about
|
|
41527
|
+
* it elsewhere: the bar under the tabs, in a <Nav> that sits outside the box and
|
|
41528
|
+
* knows nothing of it. A tab says which page it is through its address, so a
|
|
41529
|
+
* nav follows any row it holds tabs for, with nothing to wire.
|
|
41530
|
+
*
|
|
41531
|
+
* Said per scroll and not through a signal: a number written on one element is
|
|
41532
|
+
* all it takes, and a signal would make that a render.
|
|
41533
|
+
*/
|
|
41534
|
+
const followers = new Set();
|
|
41573
41535
|
|
|
41574
|
-
|
|
41575
|
-
|
|
41576
|
-
|
|
41536
|
+
const followRouteRows = (onMove) => {
|
|
41537
|
+
followers.add(onMove);
|
|
41538
|
+
return () => {
|
|
41539
|
+
followers.delete(onMove);
|
|
41540
|
+
};
|
|
41541
|
+
};
|
|
41577
41542
|
|
|
41578
|
-
|
|
41579
|
-
|
|
41543
|
+
/**
|
|
41544
|
+
* @param {{basePath: string, towardPath: string|null, progress: number}|null} move
|
|
41545
|
+
* - the two pages the row stands between, by the pathname of their address,
|
|
41546
|
+
* and how far past the first in the terms of --slide-travel-progress. `null`
|
|
41547
|
+
* once it has stopped.
|
|
41548
|
+
*/
|
|
41549
|
+
const publishRouteRowMove = (move) => {
|
|
41550
|
+
for (const follower of followers) {
|
|
41551
|
+
follower(move);
|
|
41580
41552
|
}
|
|
41581
|
-
}
|
|
41582
|
-
|
|
41583
|
-
:root[data-navi-route-travel] {
|
|
41584
|
-
view-transition-name: none;
|
|
41553
|
+
};
|
|
41585
41554
|
|
|
41586
|
-
|
|
41587
|
-
|
|
41588
|
-
|
|
41555
|
+
/**
|
|
41556
|
+
* A row of pages the browser scrolls itself, and what is left to read from it.
|
|
41557
|
+
*
|
|
41558
|
+
* The hand is not interpreted here: the browser follows it, on its compositor,
|
|
41559
|
+
* and it alone knows what the page cannot be told (a notch from a trackpad, the
|
|
41560
|
+
* end of a stroke from its momentum, which scroller a stream is latched on). What
|
|
41561
|
+
* remains is reading where the row stands and when it has come to rest.
|
|
41562
|
+
*
|
|
41563
|
+
* Rows are measured off their pages rather than by rank and width: a row that
|
|
41564
|
+
* ends early because a page is held leaves what lies beyond out (`data-off-row`),
|
|
41565
|
+
* and ranks would then lie about where the others are. An `offsetLeft` is rounded,
|
|
41566
|
+
* but by half a pixel at most whatever the rank, which a rank multiplied by a
|
|
41567
|
+
* rounded width is not. It also ignores a transform: a row inside a popup still
|
|
41568
|
+
* growing into place measures as it will stand.
|
|
41569
|
+
*
|
|
41570
|
+
* Each scroller must be the offsetParent of its pages.
|
|
41571
|
+
*/
|
|
41589
41572
|
|
|
41590
|
-
|
|
41591
|
-
|
|
41592
|
-
|
|
41593
|
-
|
|
41594
|
-
|
|
41595
|
-
|
|
41573
|
+
// What the rest of a scroll is worth while a page sits a rounding away from it.
|
|
41574
|
+
const AT_REST_DISTANCE = 1;
|
|
41575
|
+
// How long without a scroll event means the row stopped. The browser's word is
|
|
41576
|
+
// not enough: Firefox says `scrollend` once, when the wheel goes quiet, some 200ms
|
|
41577
|
+
// before its snap has finished and never again, and Safari before 26.2 does not
|
|
41578
|
+
// say it at all.
|
|
41579
|
+
const STOPPED_SILENCE = 150;
|
|
41596
41580
|
|
|
41597
|
-
|
|
41598
|
-
|
|
41599
|
-
left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
|
|
41600
|
-
width: auto;
|
|
41601
|
-
}
|
|
41581
|
+
const isOnRow = (page) =>
|
|
41582
|
+
page.offsetWidth > 0 && !page.hasAttribute("data-off-row");
|
|
41602
41583
|
|
|
41603
|
-
|
|
41604
|
-
|
|
41605
|
-
|
|
41606
|
-
width: auto;
|
|
41607
|
-
}
|
|
41584
|
+
const restsOn = (scroller, page) =>
|
|
41585
|
+
Math.abs(page.offsetLeft - scroller.scrollLeft) <= AT_REST_DISTANCE &&
|
|
41586
|
+
Math.abs(page.offsetTop - scroller.scrollTop) <= AT_REST_DISTANCE;
|
|
41608
41587
|
|
|
41609
|
-
|
|
41610
|
-
|
|
41611
|
-
|
|
41588
|
+
/**
|
|
41589
|
+
* @returns {Element|undefined} the page the row rests on, nothing while it stands
|
|
41590
|
+
* between two: a hand can pause midway, and an engine may call that the end of
|
|
41591
|
+
* a scroll.
|
|
41592
|
+
*/
|
|
41593
|
+
const pageAtRest = (scroller, pages) =>
|
|
41594
|
+
pages.find((page) => isOnRow(page) && restsOn(scroller, page));
|
|
41612
41595
|
|
|
41613
|
-
|
|
41614
|
-
|
|
41615
|
-
|
|
41616
|
-
|
|
41617
|
-
|
|
41618
|
-
|
|
41619
|
-
|
|
41620
|
-
|
|
41621
|
-
|
|
41622
|
-
|
|
41623
|
-
|
|
41624
|
-
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41637
|
-
|
|
41638
|
-
|
|
41639
|
-
|
|
41640
|
-
|
|
41641
|
-
|
|
41642
|
-
|
|
41596
|
+
/**
|
|
41597
|
+
* Where the row stands between two neighbouring pages, on the side it has moved
|
|
41598
|
+
* to from `page`: a hand is not limited to the next one, so what it has passed
|
|
41599
|
+
* since is walked over. A map moves along one axis at a time, the one it has
|
|
41600
|
+
* moved furthest on.
|
|
41601
|
+
*
|
|
41602
|
+
* @returns {{base: Element, toward: Element|undefined, fraction: number, forward: boolean}}
|
|
41603
|
+
* the last page the row has passed (`page` itself until then), the one after,
|
|
41604
|
+
* and how far between the two from 0 to 1. Nothing `toward` past the last.
|
|
41605
|
+
*/
|
|
41606
|
+
const readRowStretch = (scroller, pages, page) => {
|
|
41607
|
+
const alongX =
|
|
41608
|
+
Math.abs(scroller.scrollLeft - page.offsetLeft) >=
|
|
41609
|
+
Math.abs(scroller.scrollTop - page.offsetTop);
|
|
41610
|
+
const position = alongX ? "offsetLeft" : "offsetTop";
|
|
41611
|
+
const cross = alongX ? "offsetTop" : "offsetLeft";
|
|
41612
|
+
const scrolled = alongX ? scroller.scrollLeft : scroller.scrollTop;
|
|
41613
|
+
const forward = scrolled > page[position];
|
|
41614
|
+
const step = forward ? 1 : -1;
|
|
41615
|
+
const line = pages
|
|
41616
|
+
.filter(
|
|
41617
|
+
(candidate) => isOnRow(candidate) && candidate[cross] === page[cross],
|
|
41618
|
+
)
|
|
41619
|
+
.sort((pageA, pageB) => pageA[position] - pageB[position]);
|
|
41620
|
+
let index = line.indexOf(page);
|
|
41621
|
+
let base = page;
|
|
41622
|
+
let toward = line[index + step];
|
|
41623
|
+
while (
|
|
41624
|
+
toward &&
|
|
41625
|
+
(forward ? toward[position] <= scrolled : toward[position] >= scrolled)
|
|
41626
|
+
) {
|
|
41627
|
+
index += step;
|
|
41628
|
+
base = toward;
|
|
41629
|
+
toward = line[index + step];
|
|
41643
41630
|
}
|
|
41631
|
+
const span = toward ? Math.abs(toward[position] - base[position]) : 0;
|
|
41632
|
+
const fraction = span ? Math.abs(scrolled - base[position]) / span : 0;
|
|
41633
|
+
return { base, toward, fraction, forward };
|
|
41634
|
+
};
|
|
41644
41635
|
|
|
41645
|
-
|
|
41646
|
-
|
|
41647
|
-
|
|
41648
|
-
|
|
41649
|
-
|
|
41650
|
-
|
|
41651
|
-
|
|
41636
|
+
/**
|
|
41637
|
+
* @param {Element} scroller
|
|
41638
|
+
* @param {object} options
|
|
41639
|
+
* @param {() => void} options.onScroll - the row moved.
|
|
41640
|
+
* @param {(event: Event|null) => void} options.onLand - the row may have stopped.
|
|
41641
|
+
* Read `pageAtRest` to know whether it stopped on a page. Heard more than once
|
|
41642
|
+
* for one landing, so what it does must find nothing to do the second time.
|
|
41643
|
+
* @returns {() => void} stop listening.
|
|
41644
|
+
*/
|
|
41645
|
+
const watchRowScroll = (scroller, { onScroll, onLand }) => {
|
|
41646
|
+
let stoppedTimeout = null;
|
|
41647
|
+
let touching = false;
|
|
41648
|
+
const landOnSilence = () => {
|
|
41649
|
+
// A finger holding the row still: nothing has stopped.
|
|
41650
|
+
if (!touching) {
|
|
41651
|
+
onLand(null);
|
|
41652
|
+
}
|
|
41653
|
+
};
|
|
41654
|
+
const armSilence = () => {
|
|
41655
|
+
clearTimeout(stoppedTimeout);
|
|
41656
|
+
stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
|
|
41657
|
+
};
|
|
41658
|
+
const onScrollEvent = () => {
|
|
41659
|
+
onScroll();
|
|
41660
|
+
armSilence();
|
|
41661
|
+
};
|
|
41662
|
+
const onTouchStart = () => {
|
|
41663
|
+
touching = true;
|
|
41664
|
+
clearTimeout(stoppedTimeout);
|
|
41665
|
+
};
|
|
41666
|
+
const onTouchEnd = (touchEvent) => {
|
|
41667
|
+
touching = touchEvent.touches.length > 0;
|
|
41668
|
+
if (!touching) {
|
|
41669
|
+
armSilence();
|
|
41670
|
+
}
|
|
41671
|
+
};
|
|
41672
|
+
scroller.addEventListener("scroll", onScrollEvent, { passive: true });
|
|
41673
|
+
scroller.addEventListener("scrollend", onLand);
|
|
41674
|
+
scroller.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
41675
|
+
scroller.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
41676
|
+
scroller.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
41677
|
+
return () => {
|
|
41678
|
+
clearTimeout(stoppedTimeout);
|
|
41679
|
+
scroller.removeEventListener("scroll", onScrollEvent);
|
|
41680
|
+
scroller.removeEventListener("scrollend", onLand);
|
|
41681
|
+
scroller.removeEventListener("touchstart", onTouchStart);
|
|
41682
|
+
scroller.removeEventListener("touchend", onTouchEnd);
|
|
41683
|
+
scroller.removeEventListener("touchcancel", onTouchEnd);
|
|
41684
|
+
};
|
|
41685
|
+
};
|
|
41652
41686
|
|
|
41653
|
-
|
|
41654
|
-
|
|
41655
|
-
|
|
41656
|
-
}
|
|
41687
|
+
installImportMetaCssBuild(import.meta);
|
|
41688
|
+
const css$16 = /* css */`.navi_route_travel {
|
|
41689
|
+
position: relative;
|
|
41657
41690
|
}
|
|
41658
41691
|
|
|
41659
|
-
|
|
41660
|
-
|
|
41661
|
-
|
|
41662
|
-
|
|
41663
|
-
|
|
41664
|
-
|
|
41665
|
-
|
|
41692
|
+
[data-route-travel-row] {
|
|
41693
|
+
overflow-anchor: none;
|
|
41694
|
+
overscroll-behavior-x: contain;
|
|
41695
|
+
scroll-snap-type: x mandatory;
|
|
41696
|
+
scrollbar-width: none;
|
|
41697
|
+
flex: auto;
|
|
41698
|
+
grid-template-rows: minmax(0, 1fr);
|
|
41699
|
+
grid-auto-columns: 100%;
|
|
41700
|
+
grid-auto-flow: column;
|
|
41701
|
+
min-width: 0;
|
|
41702
|
+
min-height: 0;
|
|
41703
|
+
display: grid;
|
|
41704
|
+
position: relative;
|
|
41705
|
+
overflow: auto hidden;
|
|
41666
41706
|
|
|
41667
|
-
|
|
41668
|
-
&::view-transition-group(*) {
|
|
41707
|
+
&::-webkit-scrollbar {
|
|
41669
41708
|
display: none;
|
|
41670
41709
|
}
|
|
41671
41710
|
|
|
41672
|
-
|
|
41673
|
-
|
|
41674
|
-
}
|
|
41675
|
-
}
|
|
41676
|
-
|
|
41677
|
-
:root[data-navi-route-travel-dragged] {
|
|
41678
|
-
&::view-transition-group(navi-route-travel), &::view-transition-old(navi-route-travel), &::view-transition-new(navi-route-travel) {
|
|
41679
|
-
animation-timing-function: linear;
|
|
41680
|
-
}
|
|
41681
|
-
}
|
|
41682
|
-
|
|
41683
|
-
:root[data-navi-route-travel="forward"] {
|
|
41684
|
-
&::view-transition-old(navi-route-travel) {
|
|
41685
|
-
animation-name: navi-route-travel-leave-towards-start;
|
|
41686
|
-
}
|
|
41687
|
-
|
|
41688
|
-
&::view-transition-new(navi-route-travel) {
|
|
41689
|
-
animation-name: navi-route-travel-enter-from-end;
|
|
41690
|
-
}
|
|
41691
|
-
}
|
|
41692
|
-
|
|
41693
|
-
:root[data-navi-route-travel="back"] {
|
|
41694
|
-
&::view-transition-old(navi-route-travel) {
|
|
41695
|
-
animation-name: navi-route-travel-leave-towards-end;
|
|
41696
|
-
}
|
|
41697
|
-
|
|
41698
|
-
&::view-transition-new(navi-route-travel) {
|
|
41699
|
-
animation-name: navi-route-travel-enter-from-start;
|
|
41700
|
-
}
|
|
41701
|
-
}
|
|
41702
|
-
|
|
41703
|
-
:root[data-navi-route-travel-axis="y"] {
|
|
41704
|
-
&[data-navi-route-travel="forward"] {
|
|
41705
|
-
&::view-transition-old(navi-route-travel) {
|
|
41706
|
-
animation-name: navi-route-travel-leave-towards-top;
|
|
41707
|
-
}
|
|
41708
|
-
|
|
41709
|
-
&::view-transition-new(navi-route-travel) {
|
|
41710
|
-
animation-name: navi-route-travel-enter-from-bottom;
|
|
41711
|
-
}
|
|
41712
|
-
}
|
|
41713
|
-
|
|
41714
|
-
&[data-navi-route-travel="back"] {
|
|
41715
|
-
&::view-transition-old(navi-route-travel) {
|
|
41716
|
-
animation-name: navi-route-travel-leave-towards-bottom;
|
|
41717
|
-
}
|
|
41718
|
-
|
|
41719
|
-
&::view-transition-new(navi-route-travel) {
|
|
41720
|
-
animation-name: navi-route-travel-enter-from-top;
|
|
41721
|
-
}
|
|
41722
|
-
}
|
|
41723
|
-
}
|
|
41724
|
-
|
|
41725
|
-
@keyframes navi-route-travel-leave-towards-start {
|
|
41726
|
-
from {
|
|
41727
|
-
translate: 0;
|
|
41728
|
-
}
|
|
41729
|
-
|
|
41730
|
-
to {
|
|
41731
|
-
translate: -100%;
|
|
41732
|
-
}
|
|
41733
|
-
}
|
|
41734
|
-
|
|
41735
|
-
@keyframes navi-route-travel-enter-from-end {
|
|
41736
|
-
from {
|
|
41737
|
-
translate: 100%;
|
|
41738
|
-
}
|
|
41739
|
-
|
|
41740
|
-
to {
|
|
41741
|
-
translate: 0;
|
|
41742
|
-
}
|
|
41743
|
-
}
|
|
41744
|
-
|
|
41745
|
-
@keyframes navi-route-travel-leave-towards-end {
|
|
41746
|
-
from {
|
|
41747
|
-
translate: 0;
|
|
41748
|
-
}
|
|
41749
|
-
|
|
41750
|
-
to {
|
|
41751
|
-
translate: 100%;
|
|
41752
|
-
}
|
|
41753
|
-
}
|
|
41754
|
-
|
|
41755
|
-
@keyframes navi-route-travel-enter-from-start {
|
|
41756
|
-
from {
|
|
41757
|
-
translate: -100%;
|
|
41758
|
-
}
|
|
41759
|
-
|
|
41760
|
-
to {
|
|
41761
|
-
translate: 0;
|
|
41711
|
+
&[data-route-travel-row="code"] {
|
|
41712
|
+
overflow-x: hidden;
|
|
41762
41713
|
}
|
|
41763
|
-
}
|
|
41764
41714
|
|
|
41765
|
-
|
|
41766
|
-
|
|
41767
|
-
translate: 0;
|
|
41768
|
-
}
|
|
41769
|
-
|
|
41770
|
-
to {
|
|
41771
|
-
translate: 0 -100%;
|
|
41715
|
+
&[data-route-travel-row="still"] {
|
|
41716
|
+
overflow: visible;
|
|
41772
41717
|
}
|
|
41773
|
-
}
|
|
41774
41718
|
|
|
41775
|
-
|
|
41776
|
-
|
|
41777
|
-
|
|
41719
|
+
& > [data-route-row-pane] {
|
|
41720
|
+
scroll-snap-align: start;
|
|
41721
|
+
scroll-snap-stop: always;
|
|
41722
|
+
flex-direction: column;
|
|
41723
|
+
grid-row: 1;
|
|
41724
|
+
min-width: 0;
|
|
41725
|
+
min-height: 0;
|
|
41726
|
+
display: flex;
|
|
41778
41727
|
}
|
|
41779
41728
|
|
|
41780
|
-
|
|
41781
|
-
|
|
41729
|
+
&:not([data-moving]) > [data-route-row-pane][inert] {
|
|
41730
|
+
visibility: hidden;
|
|
41782
41731
|
}
|
|
41783
|
-
}
|
|
41784
41732
|
|
|
41785
|
-
|
|
41786
|
-
|
|
41787
|
-
|
|
41733
|
+
& > [data-route-row-pane][inert] {
|
|
41734
|
+
top: var(--navi-route-row-pin, 0);
|
|
41735
|
+
max-height: 100dvh;
|
|
41736
|
+
position: relative;
|
|
41737
|
+
overflow: clip;
|
|
41788
41738
|
}
|
|
41789
41739
|
|
|
41790
|
-
|
|
41791
|
-
|
|
41740
|
+
& > [data-route-row-live] {
|
|
41741
|
+
flex-direction: column;
|
|
41742
|
+
grid-row: 1;
|
|
41743
|
+
min-width: 0;
|
|
41744
|
+
min-height: 0;
|
|
41745
|
+
display: flex;
|
|
41746
|
+
position: relative;
|
|
41792
41747
|
}
|
|
41793
|
-
}
|
|
41794
41748
|
|
|
41795
|
-
|
|
41796
|
-
|
|
41797
|
-
translate: 0 -100%;
|
|
41749
|
+
& > [data-route-row-pictures] {
|
|
41750
|
+
display: contents;
|
|
41798
41751
|
}
|
|
41799
41752
|
|
|
41800
|
-
|
|
41801
|
-
|
|
41753
|
+
& > [data-route-row-pictures] > * {
|
|
41754
|
+
contain: layout paint;
|
|
41755
|
+
pointer-events: none;
|
|
41756
|
+
grid-row: 1;
|
|
41757
|
+
min-width: 0;
|
|
41802
41758
|
}
|
|
41803
41759
|
}
|
|
41804
41760
|
`;
|
|
41805
41761
|
|
|
41762
|
+
// How far a hand must have pulled before the address moves for a page that is not
|
|
41763
|
+
// there yet: past what a hand that only shook covers, so that a shake costs
|
|
41764
|
+
// neither a navigation nor the requests of a page.
|
|
41765
|
+
const SET_OFF_PULL = 16;
|
|
41766
|
+
|
|
41806
41767
|
/**
|
|
41807
|
-
*
|
|
41808
|
-
*
|
|
41809
|
-
*
|
|
41810
|
-
*
|
|
41811
|
-
* the
|
|
41812
|
-
*
|
|
41813
|
-
*
|
|
41814
|
-
* `null` when they stop, which is when `scrollsnapchange` fires and a pager's
|
|
41815
|
-
* `settledPage` changes. A page walked whole under the hand has arrived too. A
|
|
41816
|
-
* travel no hand set off (a tab pressed, a wheel) holds nothing: it goes where
|
|
41817
|
-
* it goes.
|
|
41768
|
+
* What the URL says while a hand holds the row is where it is heading, for a page
|
|
41769
|
+
* not built yet: it builds by the address moving. A counter, a title or a
|
|
41770
|
+
* breadcrumb around the box must not move for a pull that is let go of: it reads
|
|
41771
|
+
* this first. The page the reader is still on while that lasts, and `null` when
|
|
41772
|
+
* the row has come to rest, which is when `scrollsnapchange` fires and a pager's
|
|
41773
|
+
* `settledPage` changes. A travel no hand set off holds nothing: it goes where it
|
|
41774
|
+
* goes.
|
|
41818
41775
|
*
|
|
41819
|
-
* One for the document
|
|
41820
|
-
* which row, for a page holding several boxes.
|
|
41776
|
+
* One for the document. `route` says which row, for a page holding several boxes.
|
|
41821
41777
|
*/
|
|
41822
41778
|
const routeTravelSettledPageSignal = signal(null);
|
|
41823
|
-
// Which travel wrote it, the only one that may clear it: a travel ending after
|
|
41824
|
-
// another has taken over leaves that one's answer alone.
|
|
41825
|
-
let travelHoldingSettledPage = null;
|
|
41826
41779
|
|
|
41827
41780
|
/**
|
|
41828
41781
|
* @type {import("preact").FunctionComponent<{
|
|
41829
41782
|
* routes?: Array<object|{route: object, params?: object}>,
|
|
41830
|
-
* axis?: "x"|"y",
|
|
41831
41783
|
* travelByDrag?: boolean,
|
|
41832
41784
|
* onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
|
|
41833
41785
|
* }>}
|
|
41834
|
-
* @param {Array<object|{route: object, params?: object}>} [props.routes] -
|
|
41835
|
-
*
|
|
41836
|
-
*
|
|
41837
|
-
*
|
|
41838
|
-
*
|
|
41839
|
-
*
|
|
41840
|
-
*
|
|
41841
|
-
*
|
|
41842
|
-
*
|
|
41843
|
-
*
|
|
41844
|
-
*
|
|
41845
|
-
*
|
|
41846
|
-
*
|
|
41847
|
-
*
|
|
41848
|
-
*
|
|
41849
|
-
*
|
|
41850
|
-
*
|
|
41851
|
-
*
|
|
41852
|
-
*
|
|
41853
|
-
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
41854
|
-
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
41855
|
-
* page under six entries. A tab pressed goes the way its <Link> says —
|
|
41856
|
-
* `<Link replace>` for a row of tabs, so a press and a swipe towards the
|
|
41857
|
-
* same neighbour write the same history.
|
|
41858
|
-
*
|
|
41859
|
-
* Every other prop lands on the box itself (`id`, `data-testid`, `className`,
|
|
41860
|
-
* …), which is how one of these is named: a page can hold several — a row of
|
|
41861
|
-
* sections inside the box the whole application travels in — and the class they
|
|
41862
|
-
* share plus their axis are not enough to tell them apart from the outside.
|
|
41863
|
-
*
|
|
41864
|
-
* The pages are cut at the edge of this box while they travel, and at the app's
|
|
41865
|
-
* safe area the box runs under, which is written on the transition's own
|
|
41866
|
-
* pseudo-elements — no overflow of the document reaches pictures drawn in the
|
|
41867
|
-
* top layer. It needs nothing of the browser beyond view transitions
|
|
41868
|
-
* themselves: a browser without them navigates without the movement, and the
|
|
41869
|
-
* gesture applies its change on release instead of dragging a picture that does
|
|
41870
|
-
* not exist.
|
|
41871
|
-
*
|
|
41872
|
-
* While a travel plays, the rest of the page is NOT taken as a picture: the
|
|
41873
|
-
* root's name is dropped for that time (`view-transition-name: none`), so what
|
|
41874
|
-
* stands around the box — a tab row beside it — stays live and answers a press
|
|
41875
|
-
* mid-travel. The application's own rule for the root applies again the moment
|
|
41876
|
-
* the travel is over. What stands out of a `FixedBar` is the exception, when it
|
|
41877
|
-
* asks to stay over the pages (see `data-navi-fixed-bar-overhang` there).
|
|
41786
|
+
* @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
|
|
41787
|
+
* over something longer, in order. Read from the <Route> children by default,
|
|
41788
|
+
* in the order they are written. Given here, the children are one page rendered
|
|
41789
|
+
* once, whatever the address, and may be anything: use it when the pages are one
|
|
41790
|
+
* route with other params, or not children of this box. An entry is a route, or
|
|
41791
|
+
* `{ route, params }`.
|
|
41792
|
+
* @param {boolean} [props.travelByDrag=true] - whether a hand travels. Off where the
|
|
41793
|
+
* gesture belongs to the content; a tab pressed still does.
|
|
41794
|
+
* @param {(detail: {route: object, params: object|undefined, cause: "drag"}) => void|Promise<void>} [props.onTravel]
|
|
41795
|
+
* - how to go to a page a hand has brought in. The default REPLACES the current
|
|
41796
|
+
* history entry rather than pushing one: a swipe is how one browses a page, not a
|
|
41797
|
+
* place one aimed at, and three swipes back and forth must not bury the way out
|
|
41798
|
+
* of the page under six entries. A tab pressed goes the way its <Link> says —
|
|
41799
|
+
* `<Link replace>` for a row of tabs, so a press and a swipe towards the same
|
|
41800
|
+
* neighbour write the same history.
|
|
41801
|
+
*
|
|
41802
|
+
* Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
|
|
41803
|
+
* The box has to outlive every page of its row: around those <Route>s, or around
|
|
41804
|
+
* one <Route> whose params are the tabs, never inside one of the pages.
|
|
41878
41805
|
*/
|
|
41879
41806
|
const RouteTravel = ({
|
|
41880
41807
|
routes: routesProp,
|
|
41881
|
-
axis = "x",
|
|
41882
41808
|
travelByDrag = true,
|
|
41883
41809
|
onTravel = ({
|
|
41884
41810
|
route,
|
|
41885
41811
|
params
|
|
41886
|
-
}) => route.
|
|
41812
|
+
}) => route.navTo(params, {
|
|
41813
|
+
replace: true,
|
|
41814
|
+
routeTransition: "none"
|
|
41815
|
+
}),
|
|
41887
41816
|
className,
|
|
41888
41817
|
children,
|
|
41889
41818
|
...rest
|
|
41890
41819
|
}) => {
|
|
41891
41820
|
import.meta.css = [css$16, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
41892
|
-
|
|
41893
|
-
const
|
|
41894
|
-
const
|
|
41895
|
-
|
|
41896
|
-
|
|
41897
|
-
// browser has them ready. Null when no page is on its way anywhere.
|
|
41898
|
-
const travelRef = useRef(null);
|
|
41899
|
-
// The page this box has ASKED for and is still waiting to see arrive.
|
|
41900
|
-
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
41901
|
-
// travel decided anything about it — sometimes after the travel was undone —
|
|
41902
|
-
// and read back as "the page changed" it would start a second travel nobody
|
|
41903
|
-
// asked for, over pictures that are already showing something else.
|
|
41904
|
-
const pageAskedForRef = useRef(null);
|
|
41905
|
-
// The latest way to answer a gesture, for a watcher that outlives every
|
|
41906
|
-
// render (see the wheel effect below).
|
|
41907
|
-
const travelHandlersRef = useRef(null);
|
|
41908
|
-
const pointerDownRef = useRef(null);
|
|
41821
|
+
const rowRef = useRef();
|
|
41822
|
+
const liveRef = useRef();
|
|
41823
|
+
const picturesRef = useRef();
|
|
41824
|
+
const windowed = Boolean(routesProp);
|
|
41825
|
+
const [, render] = useReducer(count => count + 1, 0);
|
|
41909
41826
|
const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
|
|
41910
41827
|
const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
|
|
41911
|
-
|
|
41912
|
-
|
|
41913
|
-
|
|
41914
|
-
|
|
41915
|
-
|
|
41916
|
-
|
|
41828
|
+
// Held while the row moves: a window recentred under a hand would move the very
|
|
41829
|
+
// panes it is pushing. It is taken as soon as the row has stopped.
|
|
41830
|
+
const pagesAsked = pagesFromProp || pagesFromChildren;
|
|
41831
|
+
const movementRef = useRef(null);
|
|
41832
|
+
const pagesRef = useRef(pagesAsked);
|
|
41833
|
+
if (!movementRef.current) {
|
|
41834
|
+
pagesRef.current = pagesAsked;
|
|
41835
|
+
}
|
|
41836
|
+
const pages = pagesRef.current;
|
|
41917
41837
|
const currentIndex = currentPageIndex(pages);
|
|
41918
|
-
//
|
|
41919
|
-
//
|
|
41920
|
-
|
|
41921
|
-
|
|
41922
|
-
|
|
41923
|
-
|
|
41924
|
-
|
|
41925
|
-
const
|
|
41926
|
-
|
|
41927
|
-
|
|
41928
|
-
|
|
41929
|
-
|
|
41930
|
-
|
|
41931
|
-
|
|
41932
|
-
|
|
41933
|
-
|
|
41934
|
-
|
|
41935
|
-
|
|
41936
|
-
|
|
41937
|
-
|
|
41938
|
-
|
|
41939
|
-
|
|
41940
|
-
|
|
41941
|
-
|
|
41942
|
-
|
|
41943
|
-
|
|
41944
|
-
|
|
41945
|
-
|
|
41946
|
-
|
|
41947
|
-
|
|
41948
|
-
|
|
41949
|
-
|
|
41950
|
-
|
|
41951
|
-
|
|
41952
|
-
|
|
41953
|
-
|
|
41954
|
-
|
|
41955
|
-
|
|
41956
|
-
|
|
41957
|
-
|
|
41958
|
-
|
|
41959
|
-
|
|
41960
|
-
ended: false,
|
|
41961
|
-
// The box as it set off, kept rather than read from the ref: the
|
|
41962
|
-
// navigation a travel asks for can take the box down (see the update
|
|
41963
|
-
// callback), and the name it wears is still given back to it.
|
|
41964
|
-
element: elementRef.current
|
|
41838
|
+
// Where the page of a window stands: the address, or where it last was for an
|
|
41839
|
+
// address the row has no place for.
|
|
41840
|
+
const liveIndexRef = useRef(0);
|
|
41841
|
+
if (currentIndex !== -1) {
|
|
41842
|
+
liveIndexRef.current = currentIndex;
|
|
41843
|
+
}
|
|
41844
|
+
const liveIndex = liveIndexRef.current;
|
|
41845
|
+
const latestRef = useRef(null);
|
|
41846
|
+
latestRef.current = {
|
|
41847
|
+
pages,
|
|
41848
|
+
currentIndex,
|
|
41849
|
+
liveIndex,
|
|
41850
|
+
onTravel,
|
|
41851
|
+
travelByDrag
|
|
41852
|
+
};
|
|
41853
|
+
|
|
41854
|
+
// The page this box asked for: routing lands later, and read back as somebody
|
|
41855
|
+
// changing the address it would send the row where the hand already is.
|
|
41856
|
+
const askedPageRef = useRef(null);
|
|
41857
|
+
const panesOf = row => Array.from(row.children).filter(child => child.hasAttribute("data-route-row-pane"));
|
|
41858
|
+
const pathOf = ({
|
|
41859
|
+
route,
|
|
41860
|
+
params
|
|
41861
|
+
}) => new URL(route.buildUrl(params), window.location.href).pathname;
|
|
41862
|
+
|
|
41863
|
+
// One movement, whoever set it off, from the first thing that moves to the row
|
|
41864
|
+
// at rest. Null when nothing moves.
|
|
41865
|
+
const beginMovement = fields => {
|
|
41866
|
+
const row = rowRef.current;
|
|
41867
|
+
if (movementRef.current) {
|
|
41868
|
+
Object.assign(movementRef.current, fields);
|
|
41869
|
+
return movementRef.current;
|
|
41870
|
+
}
|
|
41871
|
+
row.setAttribute("data-moving", "");
|
|
41872
|
+
const movement = {
|
|
41873
|
+
travelling: false,
|
|
41874
|
+
pictures: new Map(),
|
|
41875
|
+
resumeScrollRecording: null,
|
|
41876
|
+
scrollsDocument: getScrollContainer(row.parentElement, {
|
|
41877
|
+
includeHidden: true
|
|
41878
|
+
}) === document.documentElement,
|
|
41879
|
+
...fields
|
|
41965
41880
|
};
|
|
41966
|
-
|
|
41967
|
-
|
|
41968
|
-
|
|
41969
|
-
|
|
41970
|
-
|
|
41971
|
-
|
|
41972
|
-
|
|
41973
|
-
|
|
41974
|
-
|
|
41975
|
-
|
|
41976
|
-
|
|
41977
|
-
|
|
41978
|
-
|
|
41881
|
+
movementRef.current = movement;
|
|
41882
|
+
if (movement.scrollsDocument) {
|
|
41883
|
+
// Whatever the browser does with the offset meanwhile (clamping it to a page
|
|
41884
|
+
// shorter than the trip) is not the reader scrolling, and the address it
|
|
41885
|
+
// would be written against may already be another page's.
|
|
41886
|
+
movement.resumeScrollRecording = suspendScrollRecording();
|
|
41887
|
+
row.style.setProperty("--navi-route-row-pin", `${pinOf(row)}px`);
|
|
41888
|
+
}
|
|
41889
|
+
return movement;
|
|
41890
|
+
};
|
|
41891
|
+
|
|
41892
|
+
// A clone of the page in the pane it stands in, before anything changes it. In
|
|
41893
|
+
// that pane and the same height: the row keeps its size, so the document its
|
|
41894
|
+
// offset, and the page one looked at is where it was.
|
|
41895
|
+
const leavePictureOfLive = movement => {
|
|
41896
|
+
const live = liveRef.current;
|
|
41897
|
+
const index = latestRef.current.liveIndex;
|
|
41898
|
+
const picture = live.cloneNode(true);
|
|
41899
|
+
picture.removeAttribute("data-route-row-live");
|
|
41900
|
+
picture.setAttribute("inert", "");
|
|
41901
|
+
picture.setAttribute("aria-hidden", "true");
|
|
41902
|
+
picture.style.gridColumn = String(index + 1);
|
|
41903
|
+
picture.style.top = "";
|
|
41904
|
+
movement.pictures.get(index)?.remove();
|
|
41905
|
+
movement.pictures.set(index, picture);
|
|
41906
|
+
// After the live page in the document: an id it holds still finds the page.
|
|
41907
|
+
picturesRef.current.append(picture);
|
|
41908
|
+
copyScrollPositions(live, picture);
|
|
41909
|
+
if (movement.scrollsDocument) {
|
|
41910
|
+
live.style.top = `${pinOf(rowRef.current)}px`;
|
|
41911
|
+
}
|
|
41912
|
+
};
|
|
41913
|
+
const askFor = page => {
|
|
41914
|
+
const {
|
|
41915
|
+
onTravel
|
|
41916
|
+
} = latestRef.current;
|
|
41917
|
+
askedPageRef.current = page;
|
|
41918
|
+
notePageChange();
|
|
41919
|
+
return onTravel({
|
|
41920
|
+
route: page.route,
|
|
41921
|
+
params: page.params,
|
|
41922
|
+
cause: "drag"
|
|
41979
41923
|
});
|
|
41980
|
-
|
|
41981
|
-
|
|
41982
|
-
|
|
41983
|
-
|
|
41984
|
-
|
|
41985
|
-
|
|
41986
|
-
|
|
41987
|
-
|
|
41988
|
-
|
|
41989
|
-
|
|
41990
|
-
|
|
41991
|
-
|
|
41992
|
-
|
|
41993
|
-
|
|
41994
|
-
|
|
41995
|
-
|
|
41996
|
-
|
|
41997
|
-
|
|
41998
|
-
|
|
41999
|
-
|
|
42000
|
-
|
|
42001
|
-
|
|
42002
|
-
|
|
42003
|
-
|
|
42004
|
-
|
|
42005
|
-
|
|
42006
|
-
|
|
42007
|
-
if (
|
|
42008
|
-
|
|
42009
|
-
|
|
42010
|
-
|
|
42011
|
-
|
|
42012
|
-
|
|
42013
|
-
|
|
42014
|
-
|
|
42015
|
-
|
|
42016
|
-
|
|
42017
|
-
|
|
42018
|
-
|
|
42019
|
-
|
|
42020
|
-
|
|
42021
|
-
|
|
42022
|
-
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
|
|
42026
|
-
|
|
42027
|
-
const renderWait = armRouteRenderWait();
|
|
42028
|
-
const viewTransition = startViewTransition(async () => {
|
|
42029
|
-
// Let go of too early before the browser got here, a frame or several on
|
|
42030
|
-
// a slow phone. Skipping a transition does not cancel this callback, and
|
|
42031
|
-
// the router asked from here would take the page anyway, with nobody left
|
|
42032
|
-
// to bring it back.
|
|
42033
|
-
if (travel.reverting) {
|
|
42034
|
-
renderWait.stop();
|
|
42035
|
-
releaseRendering();
|
|
42036
|
-
return;
|
|
42037
|
-
}
|
|
42038
|
-
travel.calledBack = true;
|
|
42039
|
-
// Whatever is awaited here must be able to resolve without the page being
|
|
42040
|
-
// rendered: the document is frozen for the whole of this callback, and a
|
|
42041
|
-
// frame never comes — waiting for one waits until the browser gives up on
|
|
42042
|
-
// the transition. And it stays frozen exactly this long, so this is also
|
|
42043
|
-
// the shortest thing there is to keep short.
|
|
42044
|
-
await whilePageRenders(page, async () => {
|
|
42045
|
-
releaseRendering();
|
|
42046
|
-
if (change) {
|
|
42047
|
-
await change();
|
|
42048
|
-
}
|
|
42049
|
-
}, renderWait);
|
|
42050
|
-
// Where the arriving tab was left, or its top when it has never been
|
|
42051
|
-
// read. A travel is an arrival — the tab pressed is another route, and
|
|
42052
|
-
// the replace it navigates by is the only thing about it that says
|
|
42053
|
-
// otherwise — and this is the one place that knows it. Read from the
|
|
42054
|
-
// travel rather than from the page it set off for: a travel aimed
|
|
42055
|
-
// somewhere else while it waited lands where it is aimed now.
|
|
42056
|
-
if (travel.scrollsDocument) {
|
|
41924
|
+
};
|
|
41925
|
+
const sayRowMoves = () => {
|
|
41926
|
+
const row = rowRef.current;
|
|
41927
|
+
const panes = panesOf(row);
|
|
41928
|
+
if (panes.length < 2) {
|
|
41929
|
+
return;
|
|
41930
|
+
}
|
|
41931
|
+
const {
|
|
41932
|
+
pages
|
|
41933
|
+
} = latestRef.current;
|
|
41934
|
+
const {
|
|
41935
|
+
base,
|
|
41936
|
+
toward,
|
|
41937
|
+
fraction
|
|
41938
|
+
} = readRowStretch(row, panes, panes[0]);
|
|
41939
|
+
publishRouteRowMove({
|
|
41940
|
+
basePath: pathOf(pages[panes.indexOf(base)]),
|
|
41941
|
+
towardPath: toward ? pathOf(pages[panes.indexOf(toward)]) : null,
|
|
41942
|
+
progress: -fraction
|
|
41943
|
+
});
|
|
41944
|
+
};
|
|
41945
|
+
|
|
41946
|
+
// The row has stopped, on the page of the address: everything held for the
|
|
41947
|
+
// movement is let go of in one go, before anything paints.
|
|
41948
|
+
const endMovement = () => {
|
|
41949
|
+
const row = rowRef.current;
|
|
41950
|
+
const movement = movementRef.current;
|
|
41951
|
+
if (!movement) {
|
|
41952
|
+
return;
|
|
41953
|
+
}
|
|
41954
|
+
movementRef.current = null;
|
|
41955
|
+
row.removeAttribute("data-moving");
|
|
41956
|
+
publishRouteRowMove(null);
|
|
41957
|
+
routeTravelSettledPageSignal.value = null;
|
|
41958
|
+
if (liveRef.current) {
|
|
41959
|
+
liveRef.current.style.top = "";
|
|
41960
|
+
}
|
|
41961
|
+
for (const picture of movement.pictures.values()) {
|
|
41962
|
+
picture.remove();
|
|
41963
|
+
}
|
|
41964
|
+
if (movement.scrollsDocument) {
|
|
41965
|
+
row.style.removeProperty("--navi-route-row-pin");
|
|
41966
|
+
const {
|
|
41967
|
+
pages,
|
|
41968
|
+
currentIndex
|
|
41969
|
+
} = latestRef.current;
|
|
41970
|
+
if (currentIndex !== -1) {
|
|
42057
41971
|
const {
|
|
42058
41972
|
route,
|
|
42059
41973
|
params
|
|
42060
|
-
} =
|
|
41974
|
+
} = pages[currentIndex];
|
|
42061
41975
|
arriveAtScrollPosition(route.buildUrl(params));
|
|
42062
41976
|
}
|
|
42063
|
-
|
|
42064
|
-
// row rather than around them, it went down with the page being left,
|
|
42065
|
-
// and there is no box left for the page arriving to be photographed in.
|
|
42066
|
-
// The navigation stands — it is what was asked for — and the movement
|
|
42067
|
-
// is dropped.
|
|
42068
|
-
if (!travel.element.isConnected) {
|
|
42069
|
-
{
|
|
42070
|
-
warnBoxTakenDown(travel.page);
|
|
42071
|
-
}
|
|
42072
|
-
travel.viewTransition.skipTransition();
|
|
42073
|
-
endTravel(travel);
|
|
42074
|
-
return;
|
|
42075
|
-
}
|
|
42076
|
-
// The page arriving is in the DOM and the transition has not started
|
|
42077
|
-
// playing: the one moment both boxes can be known. After the scroll,
|
|
42078
|
-
// which is what the box is measured through: the two states are at the
|
|
42079
|
-
// same place in the layout without being at the same place in the window
|
|
42080
|
-
// (see transition_window.js).
|
|
42081
|
-
// The bars stay live under the pictures for a travel: only the box is
|
|
42082
|
-
// photographed (see the TRAVEL_ATTRIBUTE css).
|
|
42083
|
-
holdTransitionWindow(travel, travel.element, stateBefore, {
|
|
42084
|
-
furnitureLive: true
|
|
42085
|
-
});
|
|
42086
|
-
});
|
|
42087
|
-
travel.viewTransition = viewTransition;
|
|
42088
|
-
// Subscribed first, so it reads before the scrub below writes anything.
|
|
42089
|
-
viewTransition.ready.then(() => {
|
|
42090
|
-
placeTransitionWindow(travel, TRAVEL_NAME);
|
|
42091
|
-
}, ignoreSkipped);
|
|
42092
|
-
if (scrub) {
|
|
42093
|
-
// Said only now: the release has to have something to let go of, and the
|
|
42094
|
-
// transition did not exist a line above.
|
|
42095
|
-
travel.dropHold = holdViewTransition(() => {
|
|
42096
|
-
viewTransition.skipTransition();
|
|
42097
|
-
endTravel(travel);
|
|
42098
|
-
});
|
|
41977
|
+
movement.resumeScrollRecording();
|
|
42099
41978
|
}
|
|
42100
|
-
//
|
|
42101
|
-
|
|
42102
|
-
// hold: the hold is written in CSS against whatever transition is running
|
|
42103
|
-
// (it has to be, so that everything named — a trait under a tab row —
|
|
42104
|
-
// follows the same finger), so left on it would take hold of the transition
|
|
42105
|
-
// that has just replaced ours. Born paused, with nobody holding it, that
|
|
42106
|
-
// one never finishes: its pictures stand over a page that cannot be
|
|
42107
|
-
// touched anymore.
|
|
42108
|
-
viewTransition.finished.catch(() => {
|
|
42109
|
-
// A transition that fails before it ever calls back leaves the page held:
|
|
42110
|
-
// whoever asked for the hold gives it back, here as everywhere else.
|
|
42111
|
-
renderWait.stop();
|
|
42112
|
-
releaseRendering();
|
|
42113
|
-
endTravel(travel);
|
|
42114
|
-
});
|
|
42115
|
-
if (!scrub) {
|
|
42116
|
-
// Nobody is holding it: it plays as any transition does, and is over when
|
|
42117
|
-
// the browser says so.
|
|
42118
|
-
viewTransition.finished.then(() => {
|
|
42119
|
-
endTravel(travel);
|
|
42120
|
-
}, ignoreSkipped);
|
|
42121
|
-
return travel;
|
|
42122
|
-
}
|
|
42123
|
-
// The pictures exist from `ready` on, and the finger may have moved a long
|
|
42124
|
-
// way by then — held at their start meanwhile, so this is where they catch
|
|
42125
|
-
// up with it rather than where they set off.
|
|
42126
|
-
viewTransition.ready.then(() => {
|
|
42127
|
-
if (travel === travelRef.current) {
|
|
42128
|
-
scrubTravel(travel, travel.ratio);
|
|
42129
|
-
}
|
|
42130
|
-
}, ignoreSkipped);
|
|
42131
|
-
return travel;
|
|
41979
|
+
// A window recentred meanwhile is taken now.
|
|
41980
|
+
render();
|
|
42132
41981
|
};
|
|
42133
41982
|
|
|
42134
|
-
//
|
|
42135
|
-
//
|
|
42136
|
-
//
|
|
42137
|
-
|
|
42138
|
-
//
|
|
42139
|
-
// Watched as a position in the row rather than route by route. A route
|
|
42140
|
-
// announces its own status, and the row's tabs can all be one route: the
|
|
42141
|
-
// announcement then says a section changed without saying which is on screen,
|
|
42142
|
-
// and it says it about things this row does not move for (a route that has
|
|
42143
|
-
// now been visited, a param of its own that is not a tab of this row). Worse,
|
|
42144
|
-
// a status is published from inside the routing and the params it carries are
|
|
42145
|
-
// the ones known at that instant — a section that lands as a signal settles
|
|
42146
|
-
// is announced late, or not at all. The signals ARE the position, so the
|
|
42147
|
-
// position is read from them: one computed over the whole row, notified once
|
|
42148
|
-
// per move, whichever route moved and whether by matching or by params.
|
|
41983
|
+
// The address and the row, in step. The opening and a row given again around the
|
|
41984
|
+
// page on screen have nothing to show for it; somebody else moving the address
|
|
41985
|
+
// (a tab pressed, the back button) is a travel.
|
|
41986
|
+
const answeredPageRef = useRef(null);
|
|
42149
41987
|
useLayoutEffect(() => {
|
|
42150
|
-
const
|
|
42151
|
-
const
|
|
42152
|
-
|
|
42153
|
-
|
|
42154
|
-
|
|
42155
|
-
|
|
42156
|
-
|
|
42157
|
-
|
|
42158
|
-
|
|
42159
|
-
|
|
42160
|
-
// for is here nonetheless, so the wait is called off.
|
|
42161
|
-
if (samePage(pageAskedForRef.current, page)) {
|
|
42162
|
-
pageAskedForRef.current = null;
|
|
42163
|
-
}
|
|
42164
|
-
return;
|
|
42165
|
-
}
|
|
42166
|
-
// Whoever asked for it: a tab of this row can be a param, and a move
|
|
42167
|
-
// between two of them is a page change the path does not show (see
|
|
42168
|
-
// page_change.js).
|
|
42169
|
-
notePageChange();
|
|
42170
|
-
// A page this box asked for itself — a travel's own navigation, or one
|
|
42171
|
-
// it had given up waiting on: what arrives here is the answer to a
|
|
42172
|
-
// question already answered, not somebody going somewhere.
|
|
42173
|
-
if (samePage(pageAskedForRef.current, page)) {
|
|
42174
|
-
pageAskedForRef.current = null;
|
|
42175
|
-
currentPageRef.current = page;
|
|
42176
|
-
return;
|
|
42177
|
-
}
|
|
42178
|
-
// Somewhere else arrived first: whatever this box was still waiting for
|
|
42179
|
-
// is not coming, or no longer means anything. Forgotten here rather
|
|
42180
|
-
// than kept, or the next press on that very tab would be taken for the
|
|
42181
|
-
// late answer to a question nobody remembers asking.
|
|
42182
|
-
pageAskedForRef.current = null;
|
|
42183
|
-
// Asked for a page while one was already on its way: the travel in
|
|
42184
|
-
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
42185
|
-
// top would leave this one's pictures to be dropped mid-slide.
|
|
42186
|
-
if (travelRef.current) {
|
|
42187
|
-
currentPageRef.current = page;
|
|
42188
|
-
retargetTravel(travelRef.current, page);
|
|
42189
|
-
return;
|
|
42190
|
-
}
|
|
42191
|
-
const fromIndex = pageIndexOf(pages, currentPageRef.current);
|
|
42192
|
-
currentPageRef.current = page;
|
|
42193
|
-
if (fromIndex === -1) {
|
|
42194
|
-
// Arriving from outside this row: there is no pair of pages to show,
|
|
42195
|
-
// so there is nothing to travel between.
|
|
42196
|
-
return;
|
|
42197
|
-
}
|
|
42198
|
-
beginTravel({
|
|
42199
|
-
page,
|
|
42200
|
-
fromPage: pages[fromIndex],
|
|
42201
|
-
direction: index > fromIndex ? "forward" : "back",
|
|
42202
|
-
scrub: false
|
|
42203
|
-
});
|
|
42204
|
-
};
|
|
42205
|
-
// `subscribe` rather than `effect`: it hands the value to a callback that
|
|
42206
|
-
// is NOT being tracked, and what this one does — navigate, ask the router
|
|
42207
|
-
// for another page — reads and writes the very signals the row is watched
|
|
42208
|
-
// through.
|
|
42209
|
-
return currentIndexSignal.subscribe(onRowMove);
|
|
42210
|
-
}, [pages]);
|
|
42211
|
-
|
|
42212
|
-
// Rendering is held for the length of a navigation, so that whatever picture
|
|
42213
|
-
// this box is about to take is of the page being LEFT (see
|
|
42214
|
-
// rendering_hold.js).
|
|
42215
|
-
useLayoutEffect(() => {
|
|
42216
|
-
const stopWatchingStart = observeBeforeRouting(holdRenderingForRouting);
|
|
42217
|
-
// Nobody may have had a picture to take: this navigation is not always one
|
|
42218
|
-
// this box travels. Whoever wanted the hold took it over (beginTravel)
|
|
42219
|
-
// while the change was being applied, and left nothing here.
|
|
42220
|
-
const stopWatchingEnd = observeAfterRouting(releaseRoutingRenderingHold);
|
|
42221
|
-
return () => {
|
|
42222
|
-
stopWatchingStart();
|
|
42223
|
-
stopWatchingEnd();
|
|
42224
|
-
};
|
|
42225
|
-
}, []);
|
|
42226
|
-
|
|
42227
|
-
// What the next announcement will compare itself against: written after the
|
|
42228
|
-
// render that shows it, so it is always the page one is looking at.
|
|
42229
|
-
useLayoutEffect(() => {
|
|
42230
|
-
currentPageRef.current = currentPage;
|
|
42231
|
-
}, [currentPage]);
|
|
42232
|
-
|
|
42233
|
-
// Let go of far enough: the movement carries on from under the finger, at its
|
|
42234
|
-
// own pace, to the end.
|
|
42235
|
-
const finishTravel = travel => {
|
|
42236
|
-
// Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
|
|
42237
|
-
// set them off: left standing after the release, this travel would go on
|
|
42238
|
-
// claiming a hand that is no longer there — and everything that asks
|
|
42239
|
-
// "is somebody holding this?" before touching it (a press on the tab one
|
|
42240
|
-
// came from, a wheel push) would be answered yes and do nothing, while the
|
|
42241
|
-
// pages carry on to a page the router has already left.
|
|
42242
|
-
travel.scrub = false;
|
|
42243
|
-
releaseHold();
|
|
42244
|
-
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
42245
|
-
};
|
|
42246
|
-
|
|
42247
|
-
// Let go of too early: the pages go back the way they came, and the URL with
|
|
42248
|
-
// them. The way back is not a travel of its own — the same animations are run
|
|
42249
|
-
// backwards, and the page that was left is put back under the picture BEFORE
|
|
42250
|
-
// the picture is dropped, so the two are the same thing at the moment they
|
|
42251
|
-
// are swapped and nothing is seen changing.
|
|
42252
|
-
const revertTravel = travel => {
|
|
42253
|
-
travel.reverting = true;
|
|
42254
|
-
// A revert somebody ASKED for — a press on the tab this travel came from —
|
|
42255
|
-
// has a problem the others do not: that page is already back, and the
|
|
42256
|
-
// picture being brought in is LIVE, so it shows the page one is going back
|
|
42257
|
-
// to. Both sides then show the same thing and the way back is invisible:
|
|
42258
|
-
// one presses, and one is simply there.
|
|
42259
|
-
//
|
|
42260
|
-
// So the pages are held where they are until the pictures have finished
|
|
42261
|
-
// going back. Only the pages: unlike the hold a navigation takes to have
|
|
42262
|
-
// its picture taken, nothing is being photographed here — everything else
|
|
42263
|
-
// may go on rendering, and a tab row beside the box keeps answering.
|
|
42264
|
-
const releaseRendering = freezeRouteRender();
|
|
42265
|
-
const animations = travelAnimations(travel);
|
|
42266
|
-
walkPicturesHome(animations, TRAVEL_LEAD_NAMES);
|
|
42267
|
-
releaseHold(travel);
|
|
42268
|
-
whenPicturesArrived(animations).then(async () => {
|
|
42269
|
-
try {
|
|
42270
|
-
pageAskedForRef.current = travel.fromPage;
|
|
42271
|
-
// The page that was left is put back UNDER the picture before the
|
|
42272
|
-
// picture is dropped, so the two are the same thing at the moment they
|
|
42273
|
-
// are swapped: that only holds once the page is really back.
|
|
42274
|
-
if (pageIsCurrent(travel.fromPage)) {
|
|
42275
|
-
// It never left: the press that set this revert off put it back
|
|
42276
|
-
// there, and the pages have been held where they were until now.
|
|
42277
|
-
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
42278
|
-
// render that never comes and a page frozen under its own pictures.
|
|
42279
|
-
releaseRendering();
|
|
42280
|
-
} else {
|
|
42281
|
-
await whilePageRenders(travel.fromPage, () => onTravel({
|
|
42282
|
-
route: travel.fromPage.route,
|
|
42283
|
-
params: travel.fromPage.params,
|
|
42284
|
-
cause: "revert"
|
|
42285
|
-
}));
|
|
42286
|
-
}
|
|
42287
|
-
travel.viewTransition.skipTransition();
|
|
42288
|
-
} finally {
|
|
42289
|
-
// Refused before the browser called back, nothing was swapped: the
|
|
42290
|
-
// offset is still the page's own, and no render is on its way to say so.
|
|
42291
|
-
if (travel.scrollsDocument && travel.calledBack) {
|
|
42292
|
-
// The page coming back was read somewhere else, and the offset
|
|
42293
|
-
// currently on the document is the one the page that came in put
|
|
42294
|
-
// there. Given back once the page is really back — the render is
|
|
42295
|
-
// what makes the document tall enough to hold that offset again —
|
|
42296
|
-
// and the recording stays deaf until then, over the clamp the swap
|
|
42297
|
-
// back makes on the way. Its own deafness rather than the travel's:
|
|
42298
|
-
// the travel ends here, and this outlives it by a render. Written
|
|
42299
|
-
// once the commit that says so has ended, as arriveAtScrollPosition
|
|
42300
|
-
// asks.
|
|
42301
|
-
const resumeScrollRecording = suspendScrollRecording();
|
|
42302
|
-
const stopWatchingRender = observeRouteRender(() => {
|
|
42303
|
-
stopWatchingRender();
|
|
42304
|
-
queueMicrotask(() => {
|
|
42305
|
-
const {
|
|
42306
|
-
route,
|
|
42307
|
-
params
|
|
42308
|
-
} = travel.fromPage;
|
|
42309
|
-
arriveAtScrollPosition(route.buildUrl(params));
|
|
42310
|
-
resumeScrollRecording();
|
|
42311
|
-
});
|
|
42312
|
-
});
|
|
42313
|
-
}
|
|
42314
|
-
releaseRendering();
|
|
42315
|
-
// A travel ENDS, whatever happened on the way back: put the state back,
|
|
42316
|
-
// fail to drop the picture, be interrupted by something else — the one
|
|
42317
|
-
// thing that must not happen is a travel that stays "in flight"
|
|
42318
|
-
// forever. Nothing would lift the hold, the pictures would stand where
|
|
42319
|
-
// they are over a page that cannot be touched, and every gesture after
|
|
42320
|
-
// this one would find the box busy.
|
|
42321
|
-
endTravel(travel);
|
|
41988
|
+
const row = rowRef.current;
|
|
41989
|
+
const page = currentIndex === -1 ? null : pages[currentIndex];
|
|
41990
|
+
const pane = panesOf(row)[currentIndex];
|
|
41991
|
+
const answered = answeredPageRef.current;
|
|
41992
|
+
if (samePage(answered, page)) {
|
|
41993
|
+
if (pane && !movementRef.current && !restsOn(row, pane)) {
|
|
41994
|
+
row.scrollTo({
|
|
41995
|
+
left: pane.offsetLeft,
|
|
41996
|
+
behavior: "instant"
|
|
41997
|
+
});
|
|
42322
41998
|
}
|
|
42323
|
-
});
|
|
42324
|
-
};
|
|
42325
|
-
|
|
42326
|
-
// Taken back in hand: the pictures stop where they are and answer the finger
|
|
42327
|
-
// again (see the CSS hold).
|
|
42328
|
-
const holdTravel = travel => {
|
|
42329
|
-
if (travel.ended || travel.reverting) {
|
|
42330
41999
|
return;
|
|
42331
42000
|
}
|
|
42332
|
-
|
|
42333
|
-
|
|
42334
|
-
// Whatever asks for a transition next takes ours away (there is one per
|
|
42335
|
-
// document): it must find this one already let go of, or it inherits a hold
|
|
42336
|
-
// nobody is holding.
|
|
42337
|
-
travel.dropHold = holdViewTransition(() => {
|
|
42338
|
-
travel.viewTransition?.skipTransition();
|
|
42339
|
-
endTravel(travel);
|
|
42340
|
-
});
|
|
42341
|
-
};
|
|
42342
|
-
|
|
42343
|
-
// The same travel, aimed at another page. The still it starts from does not
|
|
42344
|
-
// change — only what is being brought in against it, and that one is LIVE:
|
|
42345
|
-
// pointing the router elsewhere is all it takes for the picture to show that
|
|
42346
|
-
// page instead.
|
|
42347
|
-
const redirectTravel = (travel, page, direction) => {
|
|
42348
|
-
travel.page = page;
|
|
42349
|
-
// Everything the transition carries that is NOT the pages was measured
|
|
42350
|
-
// against a destination this travel is no longer going to (see the CSS).
|
|
42351
|
-
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
42352
|
-
if (direction === travel.direction) {
|
|
42353
|
-
// Same way, another page: the pictures in hand are already the right
|
|
42354
|
-
// pair, and nothing has to move.
|
|
42001
|
+
answeredPageRef.current = page;
|
|
42002
|
+
if (!page || !pane) {
|
|
42355
42003
|
return;
|
|
42356
42004
|
}
|
|
42357
|
-
|
|
42358
|
-
|
|
42359
|
-
|
|
42360
|
-
|
|
42361
|
-
|
|
42362
|
-
|
|
42363
|
-
|
|
42364
|
-
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
42365
|
-
};
|
|
42366
|
-
|
|
42367
|
-
// Somebody asked for a page while one was on its way. Where they asked for
|
|
42368
|
-
// decides what that means.
|
|
42369
|
-
const retargetTravel = (travel, page) => {
|
|
42370
|
-
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
42371
|
-
// A hand is holding the pages, or they are already on their way back:
|
|
42372
|
-
// either way this travel's end is decided by somebody else.
|
|
42373
|
-
return;
|
|
42374
|
-
}
|
|
42375
|
-
if (samePage(page, travel.page)) {
|
|
42376
|
-
// Already on its way there.
|
|
42377
|
-
return;
|
|
42378
|
-
}
|
|
42379
|
-
if (samePage(page, travel.fromPage)) {
|
|
42380
|
-
// Back where it set off from: that is not another travel, it is this one
|
|
42381
|
-
// undone — the same pictures, run backwards.
|
|
42382
|
-
revertTravel(travel);
|
|
42005
|
+
const asked = askedPageRef.current;
|
|
42006
|
+
if (asked && samePage(asked, page)) {
|
|
42007
|
+
askedPageRef.current = null;
|
|
42008
|
+
// The hand landed before the address did.
|
|
42009
|
+
if (movementRef.current?.landed && restsOn(row, pane)) {
|
|
42010
|
+
Promise.resolve().then(endMovement);
|
|
42011
|
+
}
|
|
42383
42012
|
return;
|
|
42384
42013
|
}
|
|
42385
|
-
|
|
42386
|
-
|
|
42387
|
-
|
|
42014
|
+
if (!answered || restsOn(row, pane)) {
|
|
42015
|
+
if (!restsOn(row, pane)) {
|
|
42016
|
+
row.scrollTo({
|
|
42017
|
+
left: pane.offsetLeft,
|
|
42018
|
+
behavior: "instant"
|
|
42019
|
+
});
|
|
42020
|
+
}
|
|
42388
42021
|
return;
|
|
42389
42022
|
}
|
|
42390
|
-
|
|
42391
|
-
|
|
42023
|
+
beginMovement({
|
|
42024
|
+
travelling: true
|
|
42025
|
+
});
|
|
42026
|
+
row.scrollTo({
|
|
42027
|
+
left: pane.offsetLeft,
|
|
42028
|
+
behavior: "smooth"
|
|
42029
|
+
});
|
|
42030
|
+
// In this very commit: the tab pressed already wears its bar, and the first
|
|
42031
|
+
// scroll event is a frame away.
|
|
42032
|
+
sayRowMoves();
|
|
42033
|
+
});
|
|
42392
42034
|
|
|
42393
|
-
//
|
|
42394
|
-
//
|
|
42395
|
-
|
|
42396
|
-
|
|
42397
|
-
|
|
42398
|
-
// simply arrives where it was going.
|
|
42399
|
-
const aimOnePageFurther = (travel, sign) => {
|
|
42400
|
-
const fromIndex = pageIndexOf(pages, travel.page);
|
|
42401
|
-
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
42402
|
-
if (fromIndex === -1 || !page) {
|
|
42403
|
-
return;
|
|
42404
|
-
}
|
|
42405
|
-
redirectTravel(travel, page, travel.direction);
|
|
42406
|
-
pageAskedForRef.current = page;
|
|
42407
|
-
travelTo(page, "drag");
|
|
42408
|
-
};
|
|
42409
|
-
const endTravel = travel => {
|
|
42410
|
-
if (travel.ended) {
|
|
42411
|
-
return;
|
|
42035
|
+
// Somebody else is about to move the address to another page of a window: its
|
|
42036
|
+
// picture has to be taken now, while it still shows the page being left.
|
|
42037
|
+
useLayoutEffect(() => {
|
|
42038
|
+
if (!windowed) {
|
|
42039
|
+
return undefined;
|
|
42412
42040
|
}
|
|
42413
|
-
|
|
42414
|
-
|
|
42415
|
-
travel.dropHold = null;
|
|
42416
|
-
if (travelHoldingSettledPage === travel) {
|
|
42417
|
-
travelHoldingSettledPage = null;
|
|
42418
|
-
routeTravelSettledPageSignal.value = null;
|
|
42419
|
-
}
|
|
42420
|
-
if (travelRef.current === travel) {
|
|
42421
|
-
document.documentElement.removeEventListener("touchmove", keepTouchRefusable);
|
|
42422
|
-
}
|
|
42423
|
-
// The offset on the scrollport belongs to a page again — the one that
|
|
42424
|
-
// arrived, or the one put back. A travel that never got as far as saying
|
|
42425
|
-
// which still has to give the recording back; the gesture's own
|
|
42426
|
-
// pseudo-travel never took it (see noPicture).
|
|
42427
|
-
travel.resumeScrollRecording?.();
|
|
42428
|
-
// Its own hold, always — whether or not this travel is still the current
|
|
42429
|
-
// one. Nobody else will lift it.
|
|
42430
|
-
releaseHold(travel);
|
|
42431
|
-
if (travelRef.current === travel) {
|
|
42432
|
-
travelRef.current = null;
|
|
42433
|
-
// Given back, so another box may wear it: kept, two of them on a page
|
|
42434
|
-
// would both answer to it and the browser refuses the whole transition
|
|
42435
|
-
// rather than pick.
|
|
42436
|
-
unnameAfterTravel(travel.element);
|
|
42437
|
-
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
42438
|
-
document.documentElement.removeAttribute(TRAVEL_AXIS_ATTRIBUTE);
|
|
42439
|
-
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
42440
|
-
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
42441
|
-
releaseTransitionWindow(travel);
|
|
42442
|
-
releaseTransitionDestination(travel);
|
|
42443
|
-
releaseTransitionPress(travel);
|
|
42444
|
-
}
|
|
42445
|
-
};
|
|
42446
|
-
|
|
42447
|
-
// What a gesture is about, whichever hand made it: a thumb dragging the page
|
|
42448
|
-
// and two fingers pushing it sideways on a trackpad ask for the same travel,
|
|
42449
|
-
// so they are answered by the same three callbacks and only the reading of
|
|
42450
|
-
// the input differs (see drag_to_travel.js).
|
|
42451
|
-
const boxSizeOnAxis = () => {
|
|
42452
|
-
const box = elementRef.current.getBoundingClientRect();
|
|
42453
|
-
return axis === "x" ? box.width : box.height;
|
|
42454
|
-
};
|
|
42455
|
-
const travelHandlers = {
|
|
42456
|
-
onStart: ({
|
|
42457
|
-
sign,
|
|
42458
|
-
target
|
|
42459
|
-
}) => {
|
|
42460
|
-
const size = boxSizeOnAxis();
|
|
42461
|
-
const travelInFlight = travelRef.current;
|
|
42462
|
-
if (travelInFlight) {
|
|
42463
|
-
// A travel is already playing, and a second one cannot be started on
|
|
42464
|
-
// top of it: there is one picture of the page being left, and it is
|
|
42465
|
-
// taken. So the gesture takes over THIS travel instead of asking for
|
|
42466
|
-
// another — which is what a hand reaching for a page still sliding is
|
|
42467
|
-
// asking for anyway. It is refused only when there is nothing to take
|
|
42468
|
-
// over (no picture at all).
|
|
42469
|
-
//
|
|
42470
|
-
// Never given up, even then: a gesture handed back to the browser is a
|
|
42471
|
-
// page that rocks under a travel that is already moving.
|
|
42472
|
-
// A travel being undone is not up for grabs either: it is already on
|
|
42473
|
-
// its way back and its end is decided. Held again mid-revert, its
|
|
42474
|
-
// animations would never finish — and the wait for them never resolves,
|
|
42475
|
-
// so the pictures stay where they are, over a page that cannot be
|
|
42476
|
-
// touched anymore.
|
|
42477
|
-
if (travelInFlight.noPicture || travelInFlight.ended || travelInFlight.reverting) {
|
|
42478
|
-
return {
|
|
42479
|
-
size,
|
|
42480
|
-
travelBack: false,
|
|
42481
|
-
travelOn: false
|
|
42482
|
-
};
|
|
42483
|
-
}
|
|
42484
|
-
// Stopped here, where it stands now: left playing under the press
|
|
42485
|
-
// until the hand said it holds it (see inFlight in onPointerDown).
|
|
42486
|
-
holdTravel(travelInFlight);
|
|
42487
|
-
// Where the pictures stand right now, said as a pull: what the finger
|
|
42488
|
-
// continues from, so nothing jumps when it takes them over.
|
|
42489
|
-
const ratio = ratioOfTravel(travelInFlight);
|
|
42490
|
-
travelInFlight.ratio = ratio;
|
|
42491
|
-
const pulledSign = travelInFlight.direction === "back" ? 1 : -1;
|
|
42492
|
-
return {
|
|
42493
|
-
size,
|
|
42494
|
-
slack: ratio * size * pulledSign,
|
|
42495
|
-
// One box, the one being travelled. Either of its ends can be walked
|
|
42496
|
-
// out of and the hand carries on into the next page, but that is a
|
|
42497
|
-
// travel of its own and the gesture asks for it when it gets there
|
|
42498
|
-
// (see onEdge) — from here there is one page on its way.
|
|
42499
|
-
travelBack: travelInFlight.direction === "back",
|
|
42500
|
-
travelOn: travelInFlight.direction === "forward"
|
|
42501
|
-
};
|
|
42502
|
-
}
|
|
42503
|
-
// Dragging the page towards the end of the axis brings in what is
|
|
42504
|
-
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
42505
|
-
const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
42506
|
-
if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
42507
|
-
return false;
|
|
42508
|
-
}
|
|
42509
|
-
if (!CAN_KEEP_PICTURE) {
|
|
42510
|
-
travelRef.current = {
|
|
42511
|
-
page,
|
|
42512
|
-
noPicture: true,
|
|
42513
|
-
ended: false
|
|
42514
|
-
};
|
|
42515
|
-
return {
|
|
42516
|
-
size,
|
|
42517
|
-
travelBack: sign > 0,
|
|
42518
|
-
travelOn: sign < 0
|
|
42519
|
-
};
|
|
42520
|
-
}
|
|
42521
|
-
return {
|
|
42522
|
-
size,
|
|
42523
|
-
travelBack: sign > 0,
|
|
42524
|
-
travelOn: sign < 0,
|
|
42525
|
-
// A page change costs what a slide does not, whatever the hand does
|
|
42526
|
-
// next: a navigation, a render, the requests of the page arriving, and
|
|
42527
|
-
// those of the page coming back. So nothing is asked of the router
|
|
42528
|
-
// until the hand has really pulled, and the neighbour is read then:
|
|
42529
|
-
// turned around before that, there is nothing to undo.
|
|
42530
|
-
setOff: ({
|
|
42531
|
-
sign: signNow
|
|
42532
|
-
}) => {
|
|
42533
|
-
const pageThatWay = signNow > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
42534
|
-
if (!pageThatWay || travelRef.current) {
|
|
42535
|
-
return false;
|
|
42536
|
-
}
|
|
42537
|
-
beginTravel({
|
|
42538
|
-
page: pageThatWay,
|
|
42539
|
-
fromPage: pages[currentIndex],
|
|
42540
|
-
direction: signNow > 0 ? "back" : "forward",
|
|
42541
|
-
scrub: true,
|
|
42542
|
-
change: () => travelTo(pageThatWay, "drag")
|
|
42543
|
-
});
|
|
42544
|
-
return {
|
|
42545
|
-
travelBack: signNow > 0,
|
|
42546
|
-
travelOn: signNow < 0
|
|
42547
|
-
};
|
|
42548
|
-
}
|
|
42549
|
-
};
|
|
42550
|
-
},
|
|
42551
|
-
onPull: ({
|
|
42552
|
-
progress
|
|
42041
|
+
return observeBeforeRouting(({
|
|
42042
|
+
url
|
|
42553
42043
|
}) => {
|
|
42554
|
-
const
|
|
42555
|
-
|
|
42044
|
+
const {
|
|
42045
|
+
pages,
|
|
42046
|
+
currentIndex
|
|
42047
|
+
} = latestRef.current;
|
|
42048
|
+
const asked = askedPageRef.current;
|
|
42049
|
+
if (asked || currentIndex === -1) {
|
|
42556
42050
|
return;
|
|
42557
42051
|
}
|
|
42558
|
-
|
|
42559
|
-
|
|
42560
|
-
|
|
42561
|
-
const ratio = travel.direction === "back" ? progress : -progress;
|
|
42562
|
-
travel.ratio = ratio > 0 ? ratio : 0;
|
|
42563
|
-
scrubTravel(travel, travel.ratio);
|
|
42564
|
-
},
|
|
42565
|
-
// A page walked all the way to one of its ends and the finger still going:
|
|
42566
|
-
// what it asks for is the page past that end, and it has said so by not
|
|
42567
|
-
// stopping. Which end decides how it is answered, and the two are not the
|
|
42568
|
-
// same amount of work.
|
|
42569
|
-
onEdge: ({
|
|
42570
|
-
sign,
|
|
42571
|
-
thrown
|
|
42572
|
-
}) => {
|
|
42573
|
-
const travel = travelRef.current;
|
|
42574
|
-
if (thrown) {
|
|
42575
|
-
// Caught, then thrown on before its end: the page after is reached by
|
|
42576
|
-
// aiming the travel in hand further, and let go of it arrives there.
|
|
42577
|
-
// There is no box to hand over — a new one would mean jumping these
|
|
42578
|
-
// pictures the rest of the way first, a page seen leaping — so none
|
|
42579
|
-
// is answered, and the gesture lets the travel go as it is.
|
|
42580
|
-
if (travel && !travel.noPicture && !travel.ended && !travel.reverting) {
|
|
42581
|
-
aimOnePageFurther(travel, sign);
|
|
42582
|
-
}
|
|
42583
|
-
return false;
|
|
42052
|
+
const path = new URL(url, window.location.href).pathname;
|
|
42053
|
+
if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
|
|
42054
|
+
return;
|
|
42584
42055
|
}
|
|
42585
|
-
|
|
42586
|
-
|
|
42056
|
+
const movement = beginMovement({
|
|
42057
|
+
travelling: true
|
|
42058
|
+
});
|
|
42059
|
+
leavePictureOfLive(movement);
|
|
42060
|
+
});
|
|
42061
|
+
}, [windowed]);
|
|
42062
|
+
useLayoutEffect(() => {
|
|
42063
|
+
const row = rowRef.current;
|
|
42064
|
+
const onScroll = () => {
|
|
42065
|
+
const {
|
|
42066
|
+
pages,
|
|
42067
|
+
currentIndex,
|
|
42068
|
+
travelByDrag
|
|
42069
|
+
} = latestRef.current;
|
|
42070
|
+
const panes = panesOf(row);
|
|
42071
|
+
if (panes.length < 2) {
|
|
42072
|
+
return;
|
|
42587
42073
|
}
|
|
42588
|
-
|
|
42589
|
-
|
|
42590
|
-
|
|
42591
|
-
|
|
42592
|
-
|
|
42593
|
-
// Walked whole. What the pictures show is the page that has arrived,
|
|
42594
|
-
// and the one leaving is gone: there is no pair left to travel with, so
|
|
42595
|
-
// the next travel needs pictures of its own — a navigation, a render, a
|
|
42596
|
-
// snapshot, and for those few frames the page does not follow the
|
|
42597
|
-
// finger before catching up with it (see `ready` in beginTravel). At
|
|
42598
|
-
// the start of a gesture that gap is invisible, the hand has barely
|
|
42599
|
-
// moved; here the hand is at full speed, and this is what it costs.
|
|
42600
|
-
//
|
|
42601
|
-
// Where we are is what THIS travel was bringing in — the URL changed as
|
|
42602
|
-
// it set off, so `currentIndex` belongs to a render this gesture is
|
|
42603
|
-
// older than.
|
|
42604
|
-
const fromIndex = pageIndexOf(pages, travel.page);
|
|
42605
|
-
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
42606
|
-
if (fromIndex === -1 || !page) {
|
|
42607
|
-
return false;
|
|
42608
|
-
}
|
|
42609
|
-
// At their very end before they are let go of: what ends the travel in
|
|
42610
|
-
// hand is the next transition starting (there is one per document, and
|
|
42611
|
-
// the funnel skips ours), and a picture skipped short of its end is a
|
|
42612
|
-
// page seen jumping the last few pixels.
|
|
42613
|
-
scrubTravel(travel, 1);
|
|
42614
|
-
travel.ratio = 1;
|
|
42615
|
-
beginTravel({
|
|
42616
|
-
page,
|
|
42617
|
-
fromPage: travel.page,
|
|
42618
|
-
direction,
|
|
42619
|
-
scrub: true,
|
|
42620
|
-
change: () => travelTo(page, "drag")
|
|
42621
|
-
});
|
|
42622
|
-
return {
|
|
42623
|
-
size: boxSizeOnAxis(),
|
|
42624
|
-
travelBack: sign > 0,
|
|
42625
|
-
travelOn: sign < 0
|
|
42626
|
-
};
|
|
42074
|
+
const moving = movementRef.current;
|
|
42075
|
+
const pane = panes[currentIndex];
|
|
42076
|
+
if (!moving && pane && restsOn(row, pane)) {
|
|
42077
|
+
// Written by this box, at rest: nobody is moving anything.
|
|
42078
|
+
return;
|
|
42627
42079
|
}
|
|
42628
|
-
|
|
42629
|
-
|
|
42630
|
-
|
|
42631
|
-
|
|
42632
|
-
// other neighbour is enough for it to show that one instead. The travel
|
|
42633
|
-
// turns around where it stands, on the same transition and under the same
|
|
42634
|
-
// hand, and there is no gap at all.
|
|
42635
|
-
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
42636
|
-
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
42637
|
-
if (fromIndex === -1 || !page) {
|
|
42638
|
-
return false;
|
|
42080
|
+
const movement = moving || beginMovement({});
|
|
42081
|
+
sayRowMoves();
|
|
42082
|
+
if (movement.travelling || !travelByDrag || !pane) {
|
|
42083
|
+
return;
|
|
42639
42084
|
}
|
|
42640
|
-
|
|
42641
|
-
|
|
42642
|
-
|
|
42643
|
-
return {
|
|
42644
|
-
size: boxSizeOnAxis(),
|
|
42645
|
-
travelBack: sign > 0,
|
|
42646
|
-
travelOn: sign < 0
|
|
42647
|
-
};
|
|
42648
|
-
},
|
|
42649
|
-
// The next swipe over a page still arriving: the page after it, and the
|
|
42650
|
-
// travel in flight goes there — the same pictures, the router pointed one
|
|
42651
|
-
// page further, exactly what a tab pressed while it plays does (see
|
|
42652
|
-
// retargetTravel). Nothing is held and nothing restarts.
|
|
42653
|
-
onPushOn: ({
|
|
42654
|
-
sign
|
|
42655
|
-
}) => {
|
|
42656
|
-
const travel = travelRef.current;
|
|
42657
|
-
if (!travel || travel.noPicture || travel.ended || travel.reverting || travel.scrub) {
|
|
42658
|
-
return false;
|
|
42085
|
+
const pulled = row.scrollLeft - pane.offsetLeft;
|
|
42086
|
+
if (Math.abs(pulled) < SET_OFF_PULL) {
|
|
42087
|
+
return;
|
|
42659
42088
|
}
|
|
42660
|
-
|
|
42661
|
-
|
|
42662
|
-
|
|
42663
|
-
|
|
42664
|
-
travels
|
|
42665
|
-
}) => {
|
|
42666
|
-
gestureRef.current = null;
|
|
42667
|
-
const travel = travelRef.current;
|
|
42668
|
-
if (!travel) {
|
|
42669
|
-
// The travel this gesture was holding ended under it. Nothing left to
|
|
42670
|
-
// decide, but the hold is this gesture's own doing and nobody else will
|
|
42671
|
-
// take it off — a hold nobody lifts is a page nobody can touch.
|
|
42672
|
-
releaseHold();
|
|
42089
|
+
const index = pulled > 0 ? currentIndex + 1 : currentIndex - 1;
|
|
42090
|
+
const paneTowards = panes[index];
|
|
42091
|
+
const page = pages[index];
|
|
42092
|
+
if (!paneTowards || samePage(askedPageRef.current, page)) {
|
|
42673
42093
|
return;
|
|
42674
42094
|
}
|
|
42675
|
-
if (
|
|
42676
|
-
|
|
42677
|
-
|
|
42678
|
-
travelTo(travel.page, "drag");
|
|
42095
|
+
if (windowed) {
|
|
42096
|
+
if (movement.pictures.has(index)) {
|
|
42097
|
+
return;
|
|
42679
42098
|
}
|
|
42099
|
+
leavePictureOfLive(movement);
|
|
42100
|
+
} else if (paneTowards.hasAttribute("data-built")) {
|
|
42680
42101
|
return;
|
|
42681
42102
|
}
|
|
42682
|
-
if (
|
|
42683
|
-
|
|
42684
|
-
return;
|
|
42103
|
+
if (routeTravelSettledPageSignal.peek() === null) {
|
|
42104
|
+
routeTravelSettledPageSignal.value = pages[currentIndex];
|
|
42685
42105
|
}
|
|
42686
|
-
|
|
42687
|
-
}
|
|
42688
|
-
};
|
|
42689
|
-
const onPointerDown = pointerDownEvent => {
|
|
42690
|
-
if (!travelByDrag || gestureRef.current) {
|
|
42691
|
-
return;
|
|
42692
|
-
}
|
|
42693
|
-
// A travel playing under the press is left playing: a hand landing on a
|
|
42694
|
-
// page still sliding is far more often the next swipe than a catch, and
|
|
42695
|
-
// stopping the pages at the press is what that swipe feels as a stall. The
|
|
42696
|
-
// gesture listens to the hand first and says which it was — the next page
|
|
42697
|
-
// (onPushOn) or this one, held (onStart) — see inFlight in
|
|
42698
|
-
// drag_to_travel.js. Either way the press is not refused: a gesture given
|
|
42699
|
-
// back to the browser is a page that rocks under a travel already moving.
|
|
42700
|
-
const travelInFlight = travelRef.current;
|
|
42701
|
-
const leftPlaying = travelInFlight && !travelInFlight.noPicture && !travelInFlight.ended && !travelInFlight.reverting && !travelInFlight.scrub;
|
|
42702
|
-
if (currentIndex === -1) {
|
|
42703
|
-
return;
|
|
42704
|
-
}
|
|
42705
|
-
const giveUp = () => {
|
|
42706
|
-
gestureRef.current = null;
|
|
42106
|
+
askFor(page);
|
|
42707
42107
|
};
|
|
42708
|
-
const
|
|
42709
|
-
|
|
42710
|
-
|
|
42711
|
-
inFlight: leftPlaying ? {
|
|
42712
|
-
axis,
|
|
42713
|
-
// The way a pull bringing in the page on its way would go:
|
|
42714
|
-
// dragging towards the end of the axis brings in what is BEFORE.
|
|
42715
|
-
way: travelInFlight.direction === "back" ? 1 : -1
|
|
42716
|
-
} : false,
|
|
42717
|
-
...travelHandlers,
|
|
42718
|
-
onGiveUp: giveUp
|
|
42719
|
-
});
|
|
42720
|
-
if (!gesture) {
|
|
42721
|
-
// Not a press this box can be about — something that reads the pointer
|
|
42722
|
-
// itself, a box below it that travels the same way.
|
|
42723
|
-
giveUp();
|
|
42724
|
-
return;
|
|
42725
|
-
}
|
|
42726
|
-
gestureRef.current = gesture;
|
|
42727
|
-
};
|
|
42728
|
-
|
|
42729
|
-
// A press aimed at a page while a travel is playing does not always reach the
|
|
42730
|
-
// box: the browser's transition covers the page and the press can be delivered
|
|
42731
|
-
// to the document root instead, whatever the pictures are told about pointer
|
|
42732
|
-
// events. So while a travel plays, the press is caught at the document and
|
|
42733
|
-
// handed to the box when it fell where a page is SEEN, which is where the hand
|
|
42734
|
-
// thinks it pressed.
|
|
42735
|
-
//
|
|
42736
|
-
// Decided by the position and never by the target: in the first frames of a
|
|
42737
|
-
// travel a press on a live bar falls to the root like any other, and what lies
|
|
42738
|
-
// under a picture is not what the hand is aiming at.
|
|
42739
|
-
pointerDownRef.current = onPointerDown;
|
|
42740
|
-
useLayoutEffect(() => {
|
|
42741
|
-
const onDocumentPointerDown = pointerDownEvent => {
|
|
42742
|
-
if (!travelRef.current) {
|
|
42108
|
+
const onLand = () => {
|
|
42109
|
+
const movement = movementRef.current;
|
|
42110
|
+
if (!movement) {
|
|
42743
42111
|
return;
|
|
42744
42112
|
}
|
|
42745
|
-
const
|
|
42746
|
-
|
|
42747
|
-
|
|
42113
|
+
const panes = panesOf(row);
|
|
42114
|
+
const pane = pageAtRest(row, panes);
|
|
42115
|
+
if (!pane) {
|
|
42748
42116
|
return;
|
|
42749
42117
|
}
|
|
42118
|
+
movement.landed = true;
|
|
42750
42119
|
const {
|
|
42751
|
-
|
|
42752
|
-
|
|
42753
|
-
|
|
42754
|
-
|
|
42755
|
-
|
|
42756
|
-
|
|
42757
|
-
|
|
42758
|
-
clientY
|
|
42759
|
-
} = pointerDownEvent;
|
|
42760
|
-
if (clientX < left || clientX > right || clientY < top || clientY > bottom) {
|
|
42120
|
+
pages
|
|
42121
|
+
} = latestRef.current;
|
|
42122
|
+
const page = pages[panes.indexOf(pane)];
|
|
42123
|
+
if (!page || pageIsCurrent(page)) {
|
|
42124
|
+
if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
|
|
42125
|
+
endMovement();
|
|
42126
|
+
}
|
|
42761
42127
|
return;
|
|
42762
42128
|
}
|
|
42763
|
-
|
|
42764
|
-
// the pictures are cut at that bar: the press is aimed at the bar.
|
|
42765
|
-
if (isPointInTransitionBand(clientX, clientY)) {
|
|
42129
|
+
if (samePage(askedPageRef.current, page)) {
|
|
42766
42130
|
return;
|
|
42767
42131
|
}
|
|
42768
|
-
|
|
42132
|
+
if (windowed && !movement.pictures.has(panes.indexOf(pane))) {
|
|
42133
|
+
leavePictureOfLive(movement);
|
|
42134
|
+
}
|
|
42135
|
+
Promise.resolve(askFor(page)).then(() => {
|
|
42136
|
+
if (pageIsCurrent(page)) {
|
|
42137
|
+
return;
|
|
42138
|
+
}
|
|
42139
|
+
// Refused, or sent elsewhere: the row says where the address is.
|
|
42140
|
+
askedPageRef.current = null;
|
|
42141
|
+
const {
|
|
42142
|
+
currentIndex
|
|
42143
|
+
} = latestRef.current;
|
|
42144
|
+
const paneCurrent = panesOf(row)[currentIndex];
|
|
42145
|
+
if (paneCurrent) {
|
|
42146
|
+
movement.travelling = true;
|
|
42147
|
+
row.scrollTo({
|
|
42148
|
+
left: paneCurrent.offsetLeft,
|
|
42149
|
+
behavior: "smooth"
|
|
42150
|
+
});
|
|
42151
|
+
}
|
|
42152
|
+
});
|
|
42769
42153
|
};
|
|
42770
|
-
|
|
42154
|
+
const stopWatching = watchRowScroll(row, {
|
|
42155
|
+
onScroll,
|
|
42156
|
+
onLand
|
|
42157
|
+
});
|
|
42771
42158
|
return () => {
|
|
42772
|
-
|
|
42159
|
+
stopWatching();
|
|
42160
|
+
movementRef.current?.resumeScrollRecording?.();
|
|
42161
|
+
movementRef.current = null;
|
|
42162
|
+
publishRouteRowMove(null);
|
|
42773
42163
|
};
|
|
42774
|
-
}, []);
|
|
42164
|
+
}, [windowed]);
|
|
42775
42165
|
|
|
42776
|
-
// A
|
|
42777
|
-
//
|
|
42778
|
-
|
|
42779
|
-
|
|
42780
|
-
|
|
42781
|
-
if (travelInFlight?.scrub) {
|
|
42782
|
-
// A hand is holding the pages. They are its until it lets go.
|
|
42783
|
-
return;
|
|
42784
|
-
}
|
|
42785
|
-
// Where the box is going, which is not where it is: a step asked for while
|
|
42786
|
-
// a travel plays is the page after the one on its way.
|
|
42787
|
-
const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
|
|
42788
|
-
const fromIndex = pageIndexOf(pages, fromPage);
|
|
42789
|
-
if (fromIndex === -1) {
|
|
42790
|
-
return;
|
|
42791
|
-
}
|
|
42792
|
-
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
42793
|
-
if (!page) {
|
|
42166
|
+
// A window's page lands on a pane as soon as the address does. The row has come
|
|
42167
|
+
// to rest on it or will: when it already has, nothing else will say so.
|
|
42168
|
+
useLayoutEffect(() => {
|
|
42169
|
+
const movement = movementRef.current;
|
|
42170
|
+
if (!movement?.landed || currentIndex === -1) {
|
|
42794
42171
|
return;
|
|
42795
42172
|
}
|
|
42796
|
-
|
|
42797
|
-
|
|
42798
|
-
|
|
42799
|
-
// page seen jumping the last few pixels.
|
|
42800
|
-
scrubTravel(travelInFlight, 1);
|
|
42801
|
-
travelInFlight.ratio = 1;
|
|
42173
|
+
const pane = panesOf(rowRef.current)[currentIndex];
|
|
42174
|
+
if (pane && restsOn(rowRef.current, pane)) {
|
|
42175
|
+
Promise.resolve().then(endMovement);
|
|
42802
42176
|
}
|
|
42803
|
-
|
|
42804
|
-
|
|
42805
|
-
fromPage,
|
|
42806
|
-
direction: sign > 0 ? "back" : "forward",
|
|
42807
|
-
scrub: false,
|
|
42808
|
-
change: () => travelTo(page, "wheel")
|
|
42809
|
-
});
|
|
42810
|
-
};
|
|
42811
|
-
|
|
42812
|
-
// Reached through a ref, and the watcher is never rebuilt for it: a travel
|
|
42813
|
-
// CHANGES the current page, so anything listening on `currentIndex` would be
|
|
42814
|
-
// torn down halfway through the very gesture that is moving it.
|
|
42815
|
-
travelHandlersRef.current = travelHandlers;
|
|
42816
|
-
const travelOneStepRef = useRef(null);
|
|
42817
|
-
travelOneStepRef.current = travelOneStep;
|
|
42818
|
-
useLayoutEffect(() => {
|
|
42819
|
-
if (!travelByDrag) {
|
|
42820
|
-
return undefined;
|
|
42821
|
-
}
|
|
42822
|
-
return watchWheelTravel(elementRef.current, {
|
|
42823
|
-
axes: axis,
|
|
42824
|
-
onStep: ({
|
|
42825
|
-
sign
|
|
42826
|
-
}) => travelOneStepRef.current(sign)
|
|
42827
|
-
});
|
|
42828
|
-
}, [travelByDrag, axis]);
|
|
42177
|
+
}, [currentIndex]);
|
|
42178
|
+
const rowKind = pages.length < 2 && windowed ? "still" : travelByDrag ? "" : "code";
|
|
42829
42179
|
return jsx("div", {
|
|
42830
42180
|
...rest,
|
|
42831
|
-
ref: elementRef,
|
|
42832
42181
|
className: className ? `navi_route_travel ${className}` : "navi_route_travel",
|
|
42833
|
-
"
|
|
42834
|
-
|
|
42835
|
-
|
|
42836
|
-
|
|
42837
|
-
|
|
42838
|
-
|
|
42839
|
-
|
|
42840
|
-
|
|
42841
|
-
|
|
42842
|
-
|
|
42843
|
-
|
|
42844
|
-
|
|
42845
|
-
|
|
42846
|
-
|
|
42847
|
-
|
|
42848
|
-
|
|
42849
|
-
|
|
42850
|
-
|
|
42851
|
-
|
|
42852
|
-
|
|
42853
|
-
|
|
42182
|
+
children: jsx("div", {
|
|
42183
|
+
ref: rowRef,
|
|
42184
|
+
"data-route-travel-row": rowKind
|
|
42185
|
+
// What a box around this one and a control acting on the press read: this
|
|
42186
|
+
// axis is taken here (see drag_to_travel.js).
|
|
42187
|
+
,
|
|
42188
|
+
"data-travel-by-drag": travelByDrag && rowKind !== "still" ? "x" : undefined,
|
|
42189
|
+
children: windowed ? jsxs(Fragment, {
|
|
42190
|
+
children: [pages.map((page, index) => jsx("div", {
|
|
42191
|
+
"data-route-row-pane": "",
|
|
42192
|
+
style: {
|
|
42193
|
+
gridColumn: index + 1
|
|
42194
|
+
}
|
|
42195
|
+
}, index)), jsx("div", {
|
|
42196
|
+
ref: liveRef,
|
|
42197
|
+
"data-route-row-live": "",
|
|
42198
|
+
style: {
|
|
42199
|
+
gridColumn: liveIndex + 1
|
|
42200
|
+
},
|
|
42201
|
+
children: children
|
|
42202
|
+
}), jsx("div", {
|
|
42203
|
+
ref: picturesRef,
|
|
42204
|
+
"data-route-row-pictures": ""
|
|
42205
|
+
})]
|
|
42206
|
+
}) : jsx(RouteRowContext.Provider, {
|
|
42207
|
+
value: true,
|
|
42208
|
+
children: children
|
|
42209
|
+
})
|
|
42210
|
+
})
|
|
42854
42211
|
});
|
|
42855
42212
|
};
|
|
42856
42213
|
|
|
42857
|
-
//
|
|
42858
|
-
//
|
|
42859
|
-
|
|
42860
|
-
|
|
42861
|
-
|
|
42862
|
-
|
|
42863
|
-
|
|
42864
|
-
// pictures nobody is holding.
|
|
42865
|
-
let travelHoldingPictures = null;
|
|
42866
|
-
const holdPictures = travel => {
|
|
42867
|
-
travelHoldingPictures = travel;
|
|
42868
|
-
document.documentElement.setAttribute(HOLD_ATTRIBUTE, "");
|
|
42869
|
-
};
|
|
42870
|
-
const releaseHold = travel => {
|
|
42871
|
-
if (travel && travelHoldingPictures !== travel) {
|
|
42872
|
-
return;
|
|
42873
|
-
}
|
|
42874
|
-
travelHoldingPictures = null;
|
|
42875
|
-
document.documentElement.removeAttribute(HOLD_ATTRIBUTE);
|
|
42876
|
-
};
|
|
42877
|
-
|
|
42878
|
-
// The animations of the pictures, asked for again until there are some: they
|
|
42879
|
-
// come into existence with the transition, several frames after it was asked
|
|
42880
|
-
// for, and the gesture has already begun by then. Kept once found — the set
|
|
42881
|
-
// does not change for the length of one travel.
|
|
42882
|
-
const travelAnimations = travel => {
|
|
42883
|
-
if (travel.animations) {
|
|
42884
|
-
return travel.animations;
|
|
42885
|
-
}
|
|
42886
|
-
const animations = viewTransitionAnimations();
|
|
42887
|
-
if (animations.length) {
|
|
42888
|
-
travel.animations = animations;
|
|
42889
|
-
}
|
|
42890
|
-
return animations;
|
|
42891
|
-
};
|
|
42892
|
-
|
|
42893
|
-
// How far a travel has come, read off the pictures themselves rather than off
|
|
42894
|
-
// what the last gesture wrote: one let go of is still moving, and a hand
|
|
42895
|
-
// reaching for it must find it where it IS.
|
|
42896
|
-
const ratioOfTravel = travel => {
|
|
42897
|
-
const animations = travelAnimations(travel);
|
|
42898
|
-
const animation = findLeadAnimation(animations, TRAVEL_LEAD_NAMES) || animations[0];
|
|
42899
|
-
if (!animation) {
|
|
42900
|
-
return travel.ratio;
|
|
42901
|
-
}
|
|
42902
|
-
const timing = animation.effect.getComputedTiming();
|
|
42903
|
-
const duration = timing.delay + timing.activeDuration;
|
|
42904
|
-
if (!duration) {
|
|
42905
|
-
return travel.ratio;
|
|
42906
|
-
}
|
|
42907
|
-
return animation.currentTime / duration;
|
|
42908
|
-
};
|
|
42909
|
-
|
|
42910
|
-
// Where the two pictures stand, said as a moment in the movement they would
|
|
42911
|
-
// have played on their own: the browser knows how they move (it is written in
|
|
42912
|
-
// CSS), so the finger only has to say how far in. They are held still by CSS
|
|
42913
|
-
// while this lasts, so a time written here is a place they stay at.
|
|
42914
|
-
const scrubTravel = (travel, ratio) => {
|
|
42915
|
-
for (const animation of travelAnimations(travel)) {
|
|
42916
|
-
const timing = animation.effect.getComputedTiming();
|
|
42917
|
-
const duration = timing.delay + timing.activeDuration;
|
|
42918
|
-
animation.currentTime = ratio * duration;
|
|
42919
|
-
}
|
|
42214
|
+
// How far down the row the reader is looking, for a page brought in: a page on
|
|
42215
|
+
// screen scrolls with the document, and one arriving has to start from there.
|
|
42216
|
+
const pinOf = row => {
|
|
42217
|
+
const {
|
|
42218
|
+
top
|
|
42219
|
+
} = row.getBoundingClientRect();
|
|
42220
|
+
return top < 0 ? -top : 0;
|
|
42920
42221
|
};
|
|
42921
42222
|
|
|
42922
|
-
//
|
|
42923
|
-
|
|
42924
|
-
|
|
42925
|
-
|
|
42926
|
-
|
|
42927
|
-
|
|
42928
|
-
|
|
42929
|
-
|
|
42930
|
-
|
|
42931
|
-
|
|
42932
|
-
|
|
42933
|
-
|
|
42934
|
-
// because the two do not always happen at the same moment: a view transition
|
|
42935
|
-
// calls its update callback a frame after it is started, and the render can
|
|
42936
|
-
// land in that gap — see beginTravel, which arms this the moment the travel is
|
|
42937
|
-
// decided and hands it over.
|
|
42938
|
-
const armRouteRenderWait = () => {
|
|
42939
|
-
let stopListening;
|
|
42940
|
-
const rendered = new Promise(resolve => {
|
|
42941
|
-
stopListening = observeRouteRender(resolve);
|
|
42942
|
-
});
|
|
42943
|
-
return {
|
|
42944
|
-
rendered,
|
|
42945
|
-
stop: () => stopListening()
|
|
42946
|
-
};
|
|
42947
|
-
};
|
|
42948
|
-
const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
|
|
42949
|
-
try {
|
|
42950
|
-
await change();
|
|
42951
|
-
if (pageIsCurrent(page)) {
|
|
42952
|
-
await wait.rendered;
|
|
42223
|
+
// What a clone does not take: where what scrolls inside was left.
|
|
42224
|
+
const copyScrollPositions = (from, to) => {
|
|
42225
|
+
const fromElements = from.querySelectorAll("*");
|
|
42226
|
+
const toElements = to.querySelectorAll("*");
|
|
42227
|
+
for (let i = 0; i < fromElements.length; i++) {
|
|
42228
|
+
const {
|
|
42229
|
+
scrollTop,
|
|
42230
|
+
scrollLeft
|
|
42231
|
+
} = fromElements[i];
|
|
42232
|
+
if (scrollTop || scrollLeft) {
|
|
42233
|
+
toElements[i].scrollTop = scrollTop;
|
|
42234
|
+
toElements[i].scrollLeft = scrollLeft;
|
|
42953
42235
|
}
|
|
42954
|
-
} finally {
|
|
42955
|
-
wait.stop();
|
|
42956
42236
|
}
|
|
42957
42237
|
};
|
|
42958
42238
|
|
|
42959
|
-
// Whether the document's offset belongs to the tabs. The pages of a row scroll
|
|
42960
|
-
// the document when nothing between the box and the viewport is a scroll
|
|
42961
|
-
// container: the tab on screen is then what makes the document tall, and the
|
|
42962
|
-
// offset on it is that tab's — it has to be given back with the tab, and the
|
|
42963
|
-
// browser's clamping of it while pages are swapped has to be ignored.
|
|
42964
|
-
//
|
|
42965
|
-
// A box that lives inside a scroller of its own — a frame in an article, a
|
|
42966
|
-
// panel beside other content — shares nothing with the document: the offset
|
|
42967
|
-
// there is the surrounding page's, the reader never left it, and a travel has
|
|
42968
|
-
// no business moving it. Each of its pages brings its own scrollport, which
|
|
42969
|
-
// goes away with the page and has nothing to restore.
|
|
42970
|
-
//
|
|
42971
|
-
// includeHidden, because an `overflow: hidden` ancestor is one of those
|
|
42972
|
-
// scrollers: it shows no scrollbar and still holds an offset of its own, and
|
|
42973
|
-
// what is under it never reaches the document. A wrapper that merely clips
|
|
42974
|
-
// (`overflow: clip`) holds nothing and is walked straight through — see
|
|
42975
|
-
// is_scrollable.js.
|
|
42976
|
-
const pagesScrollTheDocument = element => {
|
|
42977
|
-
// What window.scrollTo moves. A body whose overflow is the viewport's is
|
|
42978
|
-
// walked through to it (see overflowGoesToViewport in @jsenv/dom); a body
|
|
42979
|
-
// the walk stops on scrolls itself, out of the viewport's reach.
|
|
42980
|
-
return getScrollContainer(element, {
|
|
42981
|
-
includeHidden: true
|
|
42982
|
-
}) === document.documentElement;
|
|
42983
|
-
};
|
|
42984
|
-
|
|
42985
42239
|
// A page of the row: a route, and the params that say which of its tabs when
|
|
42986
42240
|
// several of them share it. Written as a bare route by a caller whose tabs are
|
|
42987
42241
|
// routes of their own — which is the same page with nothing to tell apart.
|
|
@@ -43002,8 +42256,6 @@ const samePage = (a, b) => {
|
|
|
43002
42256
|
}
|
|
43003
42257
|
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
43004
42258
|
};
|
|
43005
|
-
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
|
|
43006
|
-
|
|
43007
42259
|
// The FIRST page that answers, as with the branches of a <Route>: several
|
|
43008
42260
|
// routes match at once — a literal one and the parameterized one it is a case of
|
|
43009
42261
|
// ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
|
|
@@ -43024,31 +42276,6 @@ const currentPageIndex = pages => {
|
|
|
43024
42276
|
return currentIndex;
|
|
43025
42277
|
};
|
|
43026
42278
|
|
|
43027
|
-
// A transition skipped by another one starting is an outcome, not a failure.
|
|
43028
|
-
const ignoreSkipped = () => {};
|
|
43029
|
-
|
|
43030
|
-
// Said once, whatever the number of swipes: a box in the wrong place is one
|
|
43031
|
-
// fact about the application, and repeating it every time would bury it.
|
|
43032
|
-
let boxTakenDownWarned = false;
|
|
43033
|
-
const warnBoxTakenDown = page => {
|
|
43034
|
-
if (boxTakenDownWarned) {
|
|
43035
|
-
return;
|
|
43036
|
-
}
|
|
43037
|
-
boxTakenDownWarned = true;
|
|
43038
|
-
console.warn(`[navi] RouteTravel: the page ${page.route.buildUrl(page.params)} is not rendered under this box — the navigation took the box down with the page it was in, so the travel is dropped. The box must outlive every page of its row: around those <Route>s (a layout route), or around one <Route> whose params are the tabs, never inside one of the pages.`);
|
|
43039
|
-
};
|
|
43040
|
-
|
|
43041
|
-
// The name is lent to the box that is travelling and taken back afterwards.
|
|
43042
|
-
// There is one transition in a document at a time, so one box wears it at a
|
|
43043
|
-
// time — and the others, unnamed, are simply not captured: they stay live
|
|
43044
|
-
// under the pictures rather than being frozen with the page.
|
|
43045
|
-
const nameForTravel = element => {
|
|
43046
|
-
element.style.viewTransitionName = TRAVEL_NAME;
|
|
43047
|
-
};
|
|
43048
|
-
const unnameAfterTravel = element => {
|
|
43049
|
-
element.style.viewTransitionName = "";
|
|
43050
|
-
};
|
|
43051
|
-
|
|
43052
42279
|
/**
|
|
43053
42280
|
* What covers the box a route movement plays in, from INSIDE the document: a
|
|
43054
42281
|
* sticky row of tabs above a `<RouteTravel>`, a header pinned to the top of a
|
|
@@ -58206,9 +57433,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
|
58206
57433
|
* press on it is aimed at what the reader can see. Say it there, and only
|
|
58207
57434
|
* there: a control that TRAVELS with the pages has a rectangle where it will
|
|
58208
57435
|
* be, not where it is seen, and a press handed to it fires something nobody
|
|
58209
|
-
* aimed at.
|
|
58210
|
-
* the part it photographs (`data-navi-fixed-bar-overhang`) is deaf for the
|
|
58211
|
-
* whole travel, and the rest for its first frames.
|
|
57436
|
+
* aimed at.
|
|
58212
57437
|
* @param {boolean} [props.replace] - Go to the destination by TAKING THE PLACE
|
|
58213
57438
|
* of the current history entry instead of stacking onto it: the link stays a
|
|
58214
57439
|
* link (an address, a middle click, the keyboard, `aria-current`), only the
|
|
@@ -58713,6 +57938,10 @@ const SlideContainerContext = createContext(null);
|
|
|
58713
57938
|
// ([data-slide-ways~="right"]).
|
|
58714
57939
|
const SLIDE_CURRENT_ATTRIBUTE = "data-slide-current";
|
|
58715
57940
|
const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
|
|
57941
|
+
// The slide --slide-travel-progress counts from, when that is not the current
|
|
57942
|
+
// one: a hand on a row the browser scrolls can carry it past several slides, and
|
|
57943
|
+
// what follows it goes on between the two it is standing between.
|
|
57944
|
+
const SLIDE_BASE_ATTRIBUTE = "data-slide-travel-base";
|
|
58716
57945
|
// Every way out that would DO something right now, in the words the commands
|
|
58717
57946
|
// use: a direction ("left", "right", "up", "down") when there is a slide that
|
|
58718
57947
|
// way, plus "first" and "last" when one is not already standing at that end of
|
|
@@ -58950,11 +58179,15 @@ const css$_ = /* css */`@layer navi {
|
|
|
58950
58179
|
}
|
|
58951
58180
|
}
|
|
58952
58181
|
|
|
58182
|
+
.navi_nav[data-nav-following-row] .navi_current_indicator {
|
|
58183
|
+
visibility: hidden;
|
|
58184
|
+
}
|
|
58185
|
+
|
|
58953
58186
|
.navi_nav .navi_link[data-href-current] .navi_current_indicator {
|
|
58954
58187
|
view-transition-name: var(--nav-indicator-name);
|
|
58955
58188
|
}
|
|
58956
58189
|
|
|
58957
|
-
:root
|
|
58190
|
+
:root[data-navi-route-transition] .navi_nav:not([data-nav-between-tabs]) .navi_current_indicator {
|
|
58958
58191
|
view-transition-name: none;
|
|
58959
58192
|
}
|
|
58960
58193
|
|
|
@@ -59085,6 +58318,64 @@ const NavStyleCSSVars = {
|
|
|
59085
58318
|
currentIndicatorColor: "--nav-current-indicator-color",
|
|
59086
58319
|
currentIndicatorSize: ["--nav-current-indicator-size", "height"]
|
|
59087
58320
|
};
|
|
58321
|
+
|
|
58322
|
+
// Where the bar is, where it is headed and in which colour at each end: the
|
|
58323
|
+
// numbers the CSS above interpolates between. Measured off the DOM (offsets force
|
|
58324
|
+
// a layout, the colours a style resolve), so asked only when the two tabs change.
|
|
58325
|
+
const writeIndicatorGeometry = (navElement, {
|
|
58326
|
+
baseTabElement,
|
|
58327
|
+
towardTabElement,
|
|
58328
|
+
towardIsAfter,
|
|
58329
|
+
vertical
|
|
58330
|
+
}) => {
|
|
58331
|
+
const measure = tabElement => vertical ? {
|
|
58332
|
+
position: tabElement.offsetTop,
|
|
58333
|
+
length: tabElement.offsetHeight
|
|
58334
|
+
} : {
|
|
58335
|
+
position: tabElement.offsetLeft,
|
|
58336
|
+
length: tabElement.offsetWidth
|
|
58337
|
+
};
|
|
58338
|
+
// The colour a tab asks the bar to be under it. Read from the tab and not
|
|
58339
|
+
// from the row, so a tab that says nothing gives the row's value back —
|
|
58340
|
+
// it inherits it (see the css above) — and one that says something gives
|
|
58341
|
+
// its own. Empty when neither resolves to anything: the css then falls
|
|
58342
|
+
// back on its own, rather than being handed a colour that is not one.
|
|
58343
|
+
const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
|
|
58344
|
+
const writeColor = (property, color) => {
|
|
58345
|
+
if (color) {
|
|
58346
|
+
navElement.style.setProperty(property, color);
|
|
58347
|
+
} else {
|
|
58348
|
+
navElement.style.removeProperty(property);
|
|
58349
|
+
}
|
|
58350
|
+
};
|
|
58351
|
+
const baseMeasure = measure(baseTabElement);
|
|
58352
|
+
let positionDelta = 0;
|
|
58353
|
+
let lengthDelta = 0;
|
|
58354
|
+
let towardSign = 0;
|
|
58355
|
+
let towardColor = "";
|
|
58356
|
+
if (towardTabElement) {
|
|
58357
|
+
const towardMeasure = measure(towardTabElement);
|
|
58358
|
+
// What one box of travel is worth in pixels of this row, signed so that
|
|
58359
|
+
// the trait is exactly on the other tab when the progress is at its own
|
|
58360
|
+
// end: the container counts +1 when the picture leans on a slide sitting
|
|
58361
|
+
// BEFORE the current one and -1 when it sits after.
|
|
58362
|
+
const sign = towardIsAfter ? -1 : 1;
|
|
58363
|
+
positionDelta = (towardMeasure.position - baseMeasure.position) * sign;
|
|
58364
|
+
lengthDelta = (towardMeasure.length - baseMeasure.length) * sign;
|
|
58365
|
+
towardSign = sign;
|
|
58366
|
+
towardColor = colorOf(towardTabElement);
|
|
58367
|
+
}
|
|
58368
|
+
const {
|
|
58369
|
+
style
|
|
58370
|
+
} = navElement;
|
|
58371
|
+
style.setProperty("--nav-indicator-position", baseMeasure.position);
|
|
58372
|
+
style.setProperty("--nav-indicator-length", baseMeasure.length);
|
|
58373
|
+
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
58374
|
+
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
58375
|
+
style.setProperty("--nav-indicator-toward-sign", towardSign);
|
|
58376
|
+
writeColor("--nav-indicator-color", colorOf(baseTabElement));
|
|
58377
|
+
writeColor("--nav-indicator-color-toward", towardColor);
|
|
58378
|
+
};
|
|
59088
58379
|
const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
59089
58380
|
if (currentIndicator === true) {
|
|
59090
58381
|
return vertical ? "left" : "bottom";
|
|
@@ -59157,6 +58448,8 @@ const Nav = ({
|
|
|
59157
58448
|
const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
|
|
59158
58449
|
const slideContainerElementRef = useRef(null);
|
|
59159
58450
|
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
58451
|
+
// A row of routes draws that same bar only while it follows a row scrolling.
|
|
58452
|
+
const rowIndicatorPosition = slideContainer ? null : positionOfCurrentIndicator(currentIndicator, vertical);
|
|
59160
58453
|
|
|
59161
58454
|
// Which tab of this row is the one being shown: the slide the container says
|
|
59162
58455
|
// it is on, or, for a row of routes, the link the browser is on.
|
|
@@ -59169,7 +58462,9 @@ const Nav = ({
|
|
|
59169
58462
|
if (!containerElement) {
|
|
59170
58463
|
return navElement.querySelector("[data-href-current]");
|
|
59171
58464
|
}
|
|
59172
|
-
|
|
58465
|
+
// The tab the trait counts from: the current one, unless a hand has carried
|
|
58466
|
+
// the row past it.
|
|
58467
|
+
const currentArea = containerElement.getAttribute(SLIDE_BASE_ATTRIBUTE) ?? containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
59173
58468
|
if (currentArea === null) {
|
|
59174
58469
|
return null;
|
|
59175
58470
|
}
|
|
@@ -59229,56 +58524,13 @@ const Nav = ({
|
|
|
59229
58524
|
tabCount: tabElements.length,
|
|
59230
58525
|
vertical
|
|
59231
58526
|
};
|
|
59232
|
-
const
|
|
59233
|
-
|
|
59234
|
-
|
|
59235
|
-
|
|
59236
|
-
|
|
59237
|
-
|
|
59238
|
-
};
|
|
59239
|
-
// The colour a tab asks the bar to be under it. Read from the tab and not
|
|
59240
|
-
// from the row, so a tab that says nothing gives the row's value back —
|
|
59241
|
-
// it inherits it (see the css above) — and one that says something gives
|
|
59242
|
-
// its own. Empty when neither resolves to anything: the css then falls
|
|
59243
|
-
// back on its own, rather than being handed a colour that is not one.
|
|
59244
|
-
const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
|
|
59245
|
-
const writeColor = (property, color) => {
|
|
59246
|
-
if (color) {
|
|
59247
|
-
navElement.style.setProperty(property, color);
|
|
59248
|
-
} else {
|
|
59249
|
-
navElement.style.removeProperty(property);
|
|
59250
|
-
}
|
|
59251
|
-
};
|
|
59252
|
-
const currentMeasure = measure(tabElements[currentIndex]);
|
|
59253
|
-
const towardArea = towardAreaNow;
|
|
59254
|
-
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
59255
|
-
let positionDelta = 0;
|
|
59256
|
-
let lengthDelta = 0;
|
|
59257
|
-
let towardSign = 0;
|
|
59258
|
-
let towardColor = "";
|
|
59259
|
-
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
59260
|
-
const towardTabElement = tabElements[towardIndex];
|
|
59261
|
-
const towardMeasure = measure(towardTabElement);
|
|
59262
|
-
// What one box of travel is worth in pixels of this row, signed so that
|
|
59263
|
-
// the trait is exactly on the other tab when the progress is at its own
|
|
59264
|
-
// end: the container counts +1 when the picture leans on a slide sitting
|
|
59265
|
-
// BEFORE the current one and -1 when it sits after.
|
|
59266
|
-
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
59267
|
-
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
59268
|
-
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
59269
|
-
towardSign = sign;
|
|
59270
|
-
towardColor = colorOf(towardTabElement);
|
|
59271
|
-
}
|
|
59272
|
-
const {
|
|
59273
|
-
style
|
|
59274
|
-
} = navElement;
|
|
59275
|
-
style.setProperty("--nav-indicator-position", currentMeasure.position);
|
|
59276
|
-
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
59277
|
-
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
59278
|
-
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
59279
|
-
style.setProperty("--nav-indicator-toward-sign", towardSign);
|
|
59280
|
-
writeColor("--nav-indicator-color", colorOf(tabElements[currentIndex]));
|
|
59281
|
-
writeColor("--nav-indicator-color-toward", towardColor);
|
|
58527
|
+
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardAreaNow);
|
|
58528
|
+
writeIndicatorGeometry(navElement, {
|
|
58529
|
+
baseTabElement: tabElements[currentIndex],
|
|
58530
|
+
towardTabElement: towardIndex !== -1 && towardIndex !== currentIndex ? tabElements[towardIndex] : null,
|
|
58531
|
+
towardIsAfter: towardIndex > currentIndex,
|
|
58532
|
+
vertical
|
|
58533
|
+
});
|
|
59282
58534
|
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
59283
58535
|
};
|
|
59284
58536
|
// Reached through a ref by everything watching the DOM below: those watchers
|
|
@@ -59286,6 +58538,60 @@ const Nav = ({
|
|
|
59286
58538
|
// about the row as it is now.
|
|
59287
58539
|
const paintIndicatorGeometryRef = useRef(null);
|
|
59288
58540
|
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
58541
|
+
|
|
58542
|
+
// The bar following a row of routes the browser scrolls, between the two tabs
|
|
58543
|
+
// of the pages it stands between. A row this nav holds no tab for is another
|
|
58544
|
+
// nav's to draw.
|
|
58545
|
+
useLayoutEffect(() => {
|
|
58546
|
+
if (!rowIndicatorPosition) {
|
|
58547
|
+
return undefined;
|
|
58548
|
+
}
|
|
58549
|
+
let paintedBase = null;
|
|
58550
|
+
let paintedToward = null;
|
|
58551
|
+
const letGo = () => {
|
|
58552
|
+
const navElement = navRef.current;
|
|
58553
|
+
paintedBase = null;
|
|
58554
|
+
paintedToward = null;
|
|
58555
|
+
navElement?.removeAttribute("data-nav-following-row");
|
|
58556
|
+
navElement?.removeAttribute("data-nav-indicator-measured");
|
|
58557
|
+
navElement?.style.removeProperty("--slide-travel-progress");
|
|
58558
|
+
};
|
|
58559
|
+
const stopFollowing = followRouteRows(move => {
|
|
58560
|
+
const navElement = navRef.current;
|
|
58561
|
+
if (!navElement) {
|
|
58562
|
+
return;
|
|
58563
|
+
}
|
|
58564
|
+
if (!move) {
|
|
58565
|
+
letGo();
|
|
58566
|
+
return;
|
|
58567
|
+
}
|
|
58568
|
+
const tabElements = Array.from(navElement.querySelectorAll(".navi_link"));
|
|
58569
|
+
const tabOf = path => tabElements.find(tabElement => tabElement.pathname === path);
|
|
58570
|
+
const baseTabElement = tabOf(move.basePath);
|
|
58571
|
+
if (!baseTabElement) {
|
|
58572
|
+
letGo();
|
|
58573
|
+
return;
|
|
58574
|
+
}
|
|
58575
|
+
const towardTabElement = move.towardPath ? tabOf(move.towardPath) : null;
|
|
58576
|
+
if (baseTabElement !== paintedBase || towardTabElement !== paintedToward) {
|
|
58577
|
+
paintedBase = baseTabElement;
|
|
58578
|
+
paintedToward = towardTabElement;
|
|
58579
|
+
writeIndicatorGeometry(navElement, {
|
|
58580
|
+
baseTabElement,
|
|
58581
|
+
towardTabElement,
|
|
58582
|
+
towardIsAfter: tabElements.indexOf(towardTabElement) > tabElements.indexOf(baseTabElement),
|
|
58583
|
+
vertical
|
|
58584
|
+
});
|
|
58585
|
+
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
58586
|
+
navElement.setAttribute("data-nav-following-row", "");
|
|
58587
|
+
}
|
|
58588
|
+
navElement.style.setProperty("--slide-travel-progress", move.progress);
|
|
58589
|
+
});
|
|
58590
|
+
return () => {
|
|
58591
|
+
stopFollowing();
|
|
58592
|
+
letGo();
|
|
58593
|
+
};
|
|
58594
|
+
}, [rowIndicatorPosition, vertical]);
|
|
59289
58595
|
// What the last paint measured for — see the memo at the top of
|
|
59290
58596
|
// paintIndicatorGeometry.
|
|
59291
58597
|
const indicatorPaintedForRef = useRef(null);
|
|
@@ -59644,7 +58950,7 @@ const Nav = ({
|
|
|
59644
58950
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
59645
58951
|
"data-vertical": vertical ? "" : undefined,
|
|
59646
58952
|
"data-panel-position": panelPosition,
|
|
59647
|
-
"data-nav-indicator": indicatorPosition ?? undefined
|
|
58953
|
+
"data-nav-indicator": indicatorPosition ?? rowIndicatorPosition ?? undefined
|
|
59648
58954
|
// "write your travel here too": a custom property cannot be read across
|
|
59649
58955
|
// the DOM, so the container paints its progress onto this element and the
|
|
59650
58956
|
// trait follows in CSS alone (see SlideContainer's followerElements).
|
|
@@ -59668,7 +58974,7 @@ const Nav = ({
|
|
|
59668
58974
|
"--nav-indicator-name": indicatorNameRef.current
|
|
59669
58975
|
} : props.style,
|
|
59670
58976
|
styleCSSVars: NavStyleCSSVars,
|
|
59671
|
-
children: [indicatorPosition && jsx("span", {
|
|
58977
|
+
children: [(indicatorPosition || rowIndicatorPosition) && jsx("span", {
|
|
59672
58978
|
className: "navi_nav_indicator"
|
|
59673
58979
|
}), jsx(NavContext.Provider, {
|
|
59674
58980
|
value: navContextValue,
|
|
@@ -60862,17 +60168,6 @@ const css$Y = /* css */`@layer navi {
|
|
|
60862
60168
|
left: calc(-1 * var(--navi-fixed-bar-border-width));
|
|
60863
60169
|
clip-path: inset(0 0 0 -100vw);
|
|
60864
60170
|
}
|
|
60865
|
-
|
|
60866
|
-
@supports (view-transition-name: match-element) {
|
|
60867
|
-
:root[data-navi-route-travel] .navi_fixed_bar_shadow, :root[data-navi-route-travel] [data-navi-fixed-bar-overhang] {
|
|
60868
|
-
view-transition-name: match-element;
|
|
60869
|
-
view-transition-class: navi_fixed_bar_over_pages;
|
|
60870
|
-
}
|
|
60871
|
-
}
|
|
60872
|
-
|
|
60873
|
-
:root[data-navi-route-travel-turned]::view-transition-group(.navi_fixed_bar_over_pages) {
|
|
60874
|
-
display: block;
|
|
60875
|
-
}
|
|
60876
60171
|
`;
|
|
60877
60172
|
const FixedBarStyleCSSVars = {
|
|
60878
60173
|
// width/height go through the vars the size calc reads: written as plain
|
|
@@ -60924,16 +60219,7 @@ const FixedBarStyleCSSVars = {
|
|
|
60924
60219
|
* `height`/`width`.
|
|
60925
60220
|
* @param {string} [props.shadow] - What the bar casts over the content, in
|
|
60926
60221
|
* `box-shadow` syntax, cut at the bar's box on every side but the content's.
|
|
60927
|
-
* Takes no room.
|
|
60928
|
-
* are covered by the pages while a `RouteTravel` plays, this one stays over
|
|
60929
|
-
* them.
|
|
60930
|
-
*
|
|
60931
|
-
* A child standing out of the bar's box (a raised button, the notch around it)
|
|
60932
|
-
* wears `data-navi-fixed-bar-overhang`: unmarked, the pages of a `RouteTravel`
|
|
60933
|
-
* cover it wherever they have paint behind it. Marked, it stays over them, and
|
|
60934
|
-
* like anything photographed it is deaf for the length of the travel —
|
|
60935
|
-
* `pressableDuringRouteTransition` on a control inside it keeps that control
|
|
60936
|
-
* answering. The rest of the bar stays live.
|
|
60222
|
+
* Takes no room.
|
|
60937
60223
|
*/
|
|
60938
60224
|
const FixedBar = ({
|
|
60939
60225
|
children,
|
|
@@ -64201,9 +63487,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
64201
63487
|
* button in a bar the two states share, standing exactly where its picture is
|
|
64202
63488
|
* drawn — must still close it. What `<Link pressableDuringRouteTransition>`
|
|
64203
63489
|
* says, for the other half of a toggle. Only for a control that does not
|
|
64204
|
-
* travel with the pages.
|
|
64205
|
-
* a `FixedBar`: the part it photographs (`data-navi-fixed-bar-overhang`) is
|
|
64206
|
-
* deaf for the whole travel, and the rest for its first frames.
|
|
63490
|
+
* travel with the pages.
|
|
64207
63491
|
* @param {Function} [action] What the press runs: a button holds no value, so
|
|
64208
63492
|
* every press is one call. Inside a `<Form>` or a group it carries the value
|
|
64209
63493
|
* around it, merged over the action's bound params — a value that is not an
|
|
@@ -66254,6 +65538,7 @@ const css$M = /* css */`@layer navi {
|
|
|
66254
65538
|
--thumb-size: 16px;
|
|
66255
65539
|
--thumb-width: var(--thumb-size);
|
|
66256
65540
|
--thumb-height: var(--thumb-size);
|
|
65541
|
+
--press-height: var(--thumb-height);
|
|
66257
65542
|
--thumb-border-radius: 100%;
|
|
66258
65543
|
--thumb-cursor: pointer;
|
|
66259
65544
|
--font-size: var(--navi-control-font-size);
|
|
@@ -66341,6 +65626,8 @@ const css$M = /* css */`@layer navi {
|
|
|
66341
65626
|
position: relative;
|
|
66342
65627
|
|
|
66343
65628
|
& .navi_control_input {
|
|
65629
|
+
width: 100%;
|
|
65630
|
+
height: var(--press-height);
|
|
66344
65631
|
opacity: 0;
|
|
66345
65632
|
--webkit-appearance: none;
|
|
66346
65633
|
min-width: inherit;
|
|
@@ -66350,13 +65637,23 @@ const css$M = /* css */`@layer navi {
|
|
|
66350
65637
|
|
|
66351
65638
|
&::-webkit-slider-thumb {
|
|
66352
65639
|
width: var(--thumb-width);
|
|
66353
|
-
height: var(--
|
|
65640
|
+
height: var(--press-height);
|
|
66354
65641
|
border-radius: var(--thumb-border-radius);
|
|
66355
65642
|
-webkit-appearance: none;
|
|
66356
65643
|
cursor: var(--x-thumb-cursor);
|
|
66357
65644
|
}
|
|
66358
65645
|
}
|
|
66359
65646
|
|
|
65647
|
+
@media (pointer: coarse) {
|
|
65648
|
+
& .navi_control_input {
|
|
65649
|
+
overflow: clip;
|
|
65650
|
+
|
|
65651
|
+
&::-webkit-slider-thumb {
|
|
65652
|
+
transform: scaleX(3);
|
|
65653
|
+
}
|
|
65654
|
+
}
|
|
65655
|
+
}
|
|
65656
|
+
|
|
66360
65657
|
& .navi_input_range_accent_probe {
|
|
66361
65658
|
background-color: var(--accent-color);
|
|
66362
65659
|
visibility: hidden;
|
|
@@ -66562,6 +65859,7 @@ const RangeNativeInput = props => {
|
|
|
66562
65859
|
});
|
|
66563
65860
|
};
|
|
66564
65861
|
const RangeStyleCSSVars = {
|
|
65862
|
+
"pressHeight": ["--press-height", "height"],
|
|
66565
65863
|
"outlineWidth": "--outline-width",
|
|
66566
65864
|
"borderRadius": "--border-radius",
|
|
66567
65865
|
"borderColor": "--border-color",
|
|
@@ -68068,6 +67366,7 @@ const InputPseudoElements = ["::-navi-loader"];
|
|
|
68068
67366
|
* boolean?: boolean,
|
|
68069
67367
|
* selectOnPress?: boolean,
|
|
68070
67368
|
* enterSendsNothing?: boolean,
|
|
67369
|
+
* pressHeight?: number | string,
|
|
68071
67370
|
* variant?: "checkbox" | "switch" | "icon" | "button",
|
|
68072
67371
|
* buttonVariant?: "discrete",
|
|
68073
67372
|
* actionVerb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
@@ -68117,6 +67416,14 @@ const InputPseudoElements = ["::-navi-loader"];
|
|
|
68117
67416
|
* street before its suggestion filled the city) and whose Enter would send
|
|
68118
67417
|
* an answer still missing its other half. The browser's own Enter is kept (a
|
|
68119
67418
|
* datalist choice), except in a `<Form>`, where it is the submission.
|
|
67419
|
+
* @param {number|string} [pressHeight] How tall a press counts on a range, the
|
|
67420
|
+
* thumb's height by default, so a thin line stays easy to hit. The press area
|
|
67421
|
+
* only: nothing moves and nothing is drawn. It overflows the range, centered
|
|
67422
|
+
* on its line, over whatever is next to it and takes the presses aimed there,
|
|
67423
|
+
* so leave it the room it needs (`marginY`). Different for a mouse and a
|
|
67424
|
+
* finger: `--press-height` in a `@media (pointer: coarse)` rule. On Safari for
|
|
67425
|
+
* iOS a drag only starts close to the thumb, within three times its width; a
|
|
67426
|
+
* tap further along the line moves the value.
|
|
68120
67427
|
*/
|
|
68121
67428
|
const Input = props => {
|
|
68122
67429
|
resolveInputProps(props);
|
|
@@ -68817,7 +68124,7 @@ const Editable = props => {
|
|
|
68817
68124
|
children: [children || jsx("span", {
|
|
68818
68125
|
children: value
|
|
68819
68126
|
}), jsx(Box, {
|
|
68820
|
-
|
|
68127
|
+
baseClassName: "navi_editable_wrapper",
|
|
68821
68128
|
ref: wrapperRef,
|
|
68822
68129
|
...wrapperProps,
|
|
68823
68130
|
// inert ensure input while not editing that:
|
|
@@ -69711,26 +69018,47 @@ const css$I = /* css */`@property --slide-travel-progress {
|
|
|
69711
69018
|
|
|
69712
69019
|
.navi_slide_container {
|
|
69713
69020
|
border-radius: inherit;
|
|
69021
|
+
overflow-anchor: none;
|
|
69022
|
+
scroll-snap-type: both mandatory;
|
|
69023
|
+
scrollbar-width: none;
|
|
69714
69024
|
flex: 0 auto;
|
|
69715
69025
|
min-width: 0;
|
|
69716
69026
|
min-height: 0;
|
|
69717
69027
|
display: grid;
|
|
69718
69028
|
position: relative;
|
|
69719
|
-
overflow:
|
|
69029
|
+
overflow: hidden;
|
|
69030
|
+
|
|
69031
|
+
&::-webkit-scrollbar {
|
|
69032
|
+
display: none;
|
|
69033
|
+
}
|
|
69034
|
+
|
|
69035
|
+
&[data-slide-travelling] {
|
|
69036
|
+
scroll-snap-type: none;
|
|
69037
|
+
}
|
|
69038
|
+
|
|
69039
|
+
&[data-slide-hand*="x"] {
|
|
69040
|
+
overscroll-behavior-x: contain;
|
|
69041
|
+
overflow-x: auto;
|
|
69042
|
+
}
|
|
69043
|
+
|
|
69044
|
+
&[data-slide-hand*="y"] {
|
|
69045
|
+
overscroll-behavior-y: contain;
|
|
69046
|
+
overflow-y: auto;
|
|
69047
|
+
}
|
|
69720
69048
|
|
|
69721
69049
|
&:focus {
|
|
69722
69050
|
outline: none;
|
|
69723
69051
|
}
|
|
69724
69052
|
|
|
69725
|
-
&[data-
|
|
69053
|
+
&[data-slide-no-touch="x"] {
|
|
69726
69054
|
touch-action: pan-y;
|
|
69727
69055
|
}
|
|
69728
69056
|
|
|
69729
|
-
&[data-
|
|
69057
|
+
&[data-slide-no-touch="y"] {
|
|
69730
69058
|
touch-action: pan-x;
|
|
69731
69059
|
}
|
|
69732
69060
|
|
|
69733
|
-
&[data-
|
|
69061
|
+
&[data-slide-no-touch="xy"] {
|
|
69734
69062
|
touch-action: none;
|
|
69735
69063
|
}
|
|
69736
69064
|
|
|
@@ -69750,15 +69078,19 @@ const css$I = /* css */`@property --slide-travel-progress {
|
|
|
69750
69078
|
display: grid;
|
|
69751
69079
|
|
|
69752
69080
|
& > [data-slide] {
|
|
69753
|
-
|
|
69754
|
-
|
|
69755
|
-
|
|
69081
|
+
top: var(--slide-top, 0);
|
|
69082
|
+
left: var(--slide-left, 0);
|
|
69083
|
+
scroll-snap-align: start;
|
|
69084
|
+
scroll-snap-stop: always;
|
|
69756
69085
|
border-radius: 0;
|
|
69757
69086
|
outline: none;
|
|
69758
69087
|
grid-area: 1 / 1;
|
|
69088
|
+
min-width: 0;
|
|
69089
|
+
min-height: 0;
|
|
69090
|
+
position: relative;
|
|
69759
69091
|
}
|
|
69760
69092
|
|
|
69761
|
-
& > [data-slide][data-slide-offstage] {
|
|
69093
|
+
& > [data-slide][data-slide-offstage], & > [data-slide][data-off-row] {
|
|
69762
69094
|
visibility: hidden;
|
|
69763
69095
|
}
|
|
69764
69096
|
}
|
|
@@ -69788,22 +69120,15 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
|
|
|
69788
69120
|
// Where the track stands right now, in pixels, read off the box it draws in
|
|
69789
69121
|
// rather than off the value that moves it: mid-animation the browser reports
|
|
69790
69122
|
// that value as a calc() of a percentage and a length ("calc(-43% - 119px)"),
|
|
69791
|
-
// which no simple parse survives
|
|
69792
|
-
//
|
|
69123
|
+
// which no simple parse survives. Against the box the track rests in, past the
|
|
69124
|
+
// border (clientLeft/clientTop), and plus what the box has scrolled, which the
|
|
69125
|
+
// track's rectangle carries and its translate does not.
|
|
69793
69126
|
const trackOffsetPx = (track, containerElement) => {
|
|
69794
69127
|
const trackRect = track.getBoundingClientRect();
|
|
69795
69128
|
const containerRect = containerElement.getBoundingClientRect();
|
|
69796
|
-
// Against the box the track RESTS in — the content box, past the border
|
|
69797
|
-
// (clientLeft/clientTop are the border widths; the box never has a
|
|
69798
|
-
// scrollbar nor padding, see the top-of-file comment on padding). Measured
|
|
69799
|
-
// against the border box, a border reads as that many pixels of translate
|
|
69800
|
-
// nothing ever asked for — and it COMPOUNDS: every travel interrupted
|
|
69801
|
-
// mid-flight re-measures the track and carries the error into its "from",
|
|
69802
|
-
// so rapid back-and-forth gestures stack a 1px border into a visible
|
|
69803
|
-
// vertical drift of the whole track.
|
|
69804
69129
|
return {
|
|
69805
|
-
x: trackRect.x - containerRect.x - containerElement.clientLeft,
|
|
69806
|
-
y: trackRect.y - containerRect.y - containerElement.clientTop
|
|
69130
|
+
x: trackRect.x - containerRect.x - containerElement.clientLeft + containerElement.scrollLeft,
|
|
69131
|
+
y: trackRect.y - containerRect.y - containerElement.clientTop + containerElement.scrollTop
|
|
69807
69132
|
};
|
|
69808
69133
|
};
|
|
69809
69134
|
const offsetToPx = (offset, box) => {
|
|
@@ -69980,7 +69305,7 @@ const causeOfEvent = event => {
|
|
|
69980
69305
|
const {
|
|
69981
69306
|
type
|
|
69982
69307
|
} = event;
|
|
69983
|
-
if (type === "
|
|
69308
|
+
if (type === "scrollend") {
|
|
69984
69309
|
return "drag";
|
|
69985
69310
|
}
|
|
69986
69311
|
if (type === "keydown" || type === "keyup") {
|
|
@@ -70205,19 +69530,19 @@ const areaHeldOn = slideElement => {
|
|
|
70205
69530
|
* go in it), so the surface says `data-slide-container-follows={id}` and the
|
|
70206
69531
|
* arrows keep walking wherever the keyboard is in it. See the paragraph above
|
|
70207
69532
|
* about what is drawn AROUND the travel.
|
|
70208
|
-
* @param {boolean|"x"|"y"|"xy"} [props.travelByDrag=true] - whether a
|
|
70209
|
-
*
|
|
70210
|
-
*
|
|
70211
|
-
*
|
|
70212
|
-
*
|
|
70213
|
-
* @param {boolean|"x"|"y"|"xy"} [props.travelByScroll="x"] - whether a wheel
|
|
70214
|
-
* pushing the box travels
|
|
70215
|
-
*
|
|
70216
|
-
*
|
|
70217
|
-
*
|
|
70218
|
-
*
|
|
70219
|
-
*
|
|
70220
|
-
*
|
|
69533
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByDrag=true] - whether a finger or
|
|
69534
|
+
* a pen pushing the slides travels: the browser pans them, a mouse does not.
|
|
69535
|
+
* Off where the gesture belongs to the content (a canvas one draws on, a map
|
|
69536
|
+
* one pans), or where the slides are steps of a form rather than a row one
|
|
69537
|
+
* browses.
|
|
69538
|
+
* @param {boolean|"x"|"y"|"xy"} [props.travelByScroll="x"] - whether a wheel or a
|
|
69539
|
+
* trackpad pushing the box travels. Sideways only by default, because a page is
|
|
69540
|
+
* hardly ever scrollable that way: a sideways scroll over the box can only have
|
|
69541
|
+
* been meant for the box. Down the page it is the page's OWN gesture, handed on
|
|
69542
|
+
* to what scrolls around the box, so a map that travels vertically has to be
|
|
69543
|
+
* told (`travelByScroll` / `"y"` / `"xy"`) before a wheel moves it.
|
|
69544
|
+
* @param {string} [props.duration="300ms"] - how long a slide change asked for
|
|
69545
|
+
* takes: a tab, a key, a command, a state. A hand goes at its own pace.
|
|
70221
69546
|
* @param {"largest"|"frozen"} [props.sizing="largest"] - what decides the size
|
|
70222
69547
|
* of the box. "largest": the largest slide, at every moment — the box follows
|
|
70223
69548
|
* whatever the slides do. "frozen": the box is measured once and kept at that
|
|
@@ -70336,10 +69661,6 @@ const SlideContainer = ({
|
|
|
70336
69661
|
// one being travelled TO while the track moves. What the next travel departs
|
|
70337
69662
|
// from, because it is what one is looking at.
|
|
70338
69663
|
const drawnAreaRef = useRef(undefined);
|
|
70339
|
-
// The movement the track is playing, while it plays: the slide it is leaving
|
|
70340
|
-
// and the slide it is going to. A hand landing on the box takes it over from
|
|
70341
|
-
// its origin (see onStart in createTravelHandlers).
|
|
70342
|
-
const movingRef = useRef(null);
|
|
70343
69664
|
// Which way the travel about to be drawn goes, when whatever asked for it
|
|
70344
69665
|
// knows: a window stepping off its last slide comes back on its first, and
|
|
70345
69666
|
// only the press says that is a step forward — the map, read between those
|
|
@@ -70353,17 +69674,6 @@ const SlideContainer = ({
|
|
|
70353
69674
|
// A focus transfer read off the interaction that asked for the travel, kept
|
|
70354
69675
|
// until the slide it is meant for holds its final DOM (see handOverFocus).
|
|
70355
69676
|
const focusHandOverRef = useRef(null);
|
|
70356
|
-
// The gesture in hand, from the pointer that started it to the slides it
|
|
70357
|
-
// brought alongside. Null when no finger is on the box.
|
|
70358
|
-
const dragRef = useRef(null);
|
|
70359
|
-
// Where the travel about to be drawn departs from, when that is not where the
|
|
70360
|
-
// track rests: a slide let go of halfway carries on from under the finger.
|
|
70361
|
-
// Read and dropped by the layout effect, which is the one drawing it.
|
|
70362
|
-
const travelFromRef = useRef(null);
|
|
70363
|
-
// The same fact for the indicator (--slide-travel-progress): how far the
|
|
70364
|
-
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
70365
|
-
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
70366
|
-
const travelProgressFromRef = useRef(null);
|
|
70367
69677
|
// One per element painting the progress: the box, plus everything following
|
|
70368
69678
|
// it (see followerElementsRef). All started together and with the same
|
|
70369
69679
|
// options, so they are one movement said in several places.
|
|
@@ -70374,6 +69684,23 @@ const SlideContainer = ({
|
|
|
70374
69684
|
// draw in CSS alone, at the pace of the travel and under the finger, with
|
|
70375
69685
|
// nothing measured per frame.
|
|
70376
69686
|
const followerElementsRef = useRef([]);
|
|
69687
|
+
// Whether a hand has the row away from the current slide. A render meanwhile
|
|
69688
|
+
// must not send it back.
|
|
69689
|
+
const handMovingRef = useRef(false);
|
|
69690
|
+
// The slide a hand has just landed on, for the render it asks for: it is
|
|
69691
|
+
// already there, so there is nothing to play.
|
|
69692
|
+
const handLandedRef = useRef(null);
|
|
69693
|
+
// Where the box rests on the row, and what the row starts from: a slide that
|
|
69694
|
+
// holds on to the user ends the row on that side, and a scroll range starts at
|
|
69695
|
+
// its first reachable slide (see writeRowAtRest).
|
|
69696
|
+
const restPlaceRef = useRef({
|
|
69697
|
+
x: 0,
|
|
69698
|
+
y: 0
|
|
69699
|
+
});
|
|
69700
|
+
const originRef = useRef({
|
|
69701
|
+
x: 0,
|
|
69702
|
+
y: 0
|
|
69703
|
+
});
|
|
70377
69704
|
// What the caller holds, however they hold it: a `current` they re-render
|
|
70378
69705
|
// themselves, or a `signal` this container also writes (see tellCurrentChange).
|
|
70379
69706
|
if (currentSignal && currentProp !== undefined) {
|
|
@@ -70632,10 +69959,10 @@ const SlideContainer = ({
|
|
|
70632
69959
|
// (areaTowards knows the holes and a loop's two ends).
|
|
70633
69960
|
const areasAlongside = area => {
|
|
70634
69961
|
const alongside = [];
|
|
70635
|
-
if (
|
|
69962
|
+
if (travelAxes?.includes("x")) {
|
|
70636
69963
|
alongside.push(areaTowards(-1, 0, area), areaTowards(1, 0, area));
|
|
70637
69964
|
}
|
|
70638
|
-
if (
|
|
69965
|
+
if (travelAxes?.includes("y")) {
|
|
70639
69966
|
alongside.push(areaTowards(0, -1, area), areaTowards(0, 1, area));
|
|
70640
69967
|
}
|
|
70641
69968
|
return alongside;
|
|
@@ -70664,13 +69991,87 @@ const SlideContainer = ({
|
|
|
70664
69991
|
return added.length ? [...previous, ...added] : previous;
|
|
70665
69992
|
});
|
|
70666
69993
|
};
|
|
70667
|
-
|
|
70668
|
-
|
|
70669
|
-
|
|
70670
|
-
|
|
70671
|
-
|
|
70672
|
-
|
|
70673
|
-
|
|
69994
|
+
const writeSlidePlace = (slideElement, {
|
|
69995
|
+
x,
|
|
69996
|
+
y
|
|
69997
|
+
}) => {
|
|
69998
|
+
slideElement.style.setProperty("--slide-left", `${x * 100}%`);
|
|
69999
|
+
slideElement.style.setProperty("--slide-top", `${y * 100}%`);
|
|
70000
|
+
};
|
|
70001
|
+
// Every slide where the map says it is, except what the slide on screen holds
|
|
70002
|
+
// the user from: stacked under it, so the row ends there on that side. The same
|
|
70003
|
+
// rule as the gate in goToArea. The row starts at its first reachable slide,
|
|
70004
|
+
// since a scroll range starts at 0 and what lies before cannot be scrolled to.
|
|
70005
|
+
const writeRowAtRest = (slideElements, placeOf, currentElement) => {
|
|
70006
|
+
const currentPlace = placeOf.get(readArea(currentElement)) || {
|
|
70007
|
+
x: 0,
|
|
70008
|
+
y: 0
|
|
70009
|
+
};
|
|
70010
|
+
const holdsNext = currentElement.hasAttribute("data-prevent-nav-next");
|
|
70011
|
+
const holdsPrevious = currentElement.hasAttribute("data-prevent-nav-previous");
|
|
70012
|
+
const isOutOfReach = ({
|
|
70013
|
+
x,
|
|
70014
|
+
y
|
|
70015
|
+
}) => {
|
|
70016
|
+
if (x === currentPlace.x && y === currentPlace.y) {
|
|
70017
|
+
return false;
|
|
70018
|
+
}
|
|
70019
|
+
const forward = y === currentPlace.y ? x > currentPlace.x : y > currentPlace.y;
|
|
70020
|
+
return forward ? holdsNext : holdsPrevious;
|
|
70021
|
+
};
|
|
70022
|
+
const origin = {
|
|
70023
|
+
x: currentPlace.x,
|
|
70024
|
+
y: currentPlace.y
|
|
70025
|
+
};
|
|
70026
|
+
for (const slideElement of slideElements) {
|
|
70027
|
+
const place = placeOf.get(readArea(slideElement)) || {
|
|
70028
|
+
x: 0,
|
|
70029
|
+
y: 0
|
|
70030
|
+
};
|
|
70031
|
+
if (isOutOfReach(place)) {
|
|
70032
|
+
continue;
|
|
70033
|
+
}
|
|
70034
|
+
if (place.x < origin.x) {
|
|
70035
|
+
origin.x = place.x;
|
|
70036
|
+
}
|
|
70037
|
+
if (place.y < origin.y) {
|
|
70038
|
+
origin.y = place.y;
|
|
70039
|
+
}
|
|
70040
|
+
}
|
|
70041
|
+
const rest = {
|
|
70042
|
+
x: currentPlace.x - origin.x,
|
|
70043
|
+
y: currentPlace.y - origin.y
|
|
70044
|
+
};
|
|
70045
|
+
for (const slideElement of slideElements) {
|
|
70046
|
+
const place = placeOf.get(readArea(slideElement)) || {
|
|
70047
|
+
x: 0,
|
|
70048
|
+
y: 0
|
|
70049
|
+
};
|
|
70050
|
+
const outOfReach = isOutOfReach(place);
|
|
70051
|
+
slideElement.toggleAttribute("data-off-row", outOfReach);
|
|
70052
|
+
writeSlidePlace(slideElement, outOfReach ? rest : {
|
|
70053
|
+
x: place.x - origin.x,
|
|
70054
|
+
y: place.y - origin.y
|
|
70055
|
+
});
|
|
70056
|
+
}
|
|
70057
|
+
originRef.current = origin;
|
|
70058
|
+
restPlaceRef.current = rest;
|
|
70059
|
+
};
|
|
70060
|
+
const sendRowToRest = currentElement => {
|
|
70061
|
+
const containerEl = containerRef.current;
|
|
70062
|
+
if (!restsOn(containerEl, currentElement)) {
|
|
70063
|
+
containerEl.scrollTo({
|
|
70064
|
+
left: currentElement.offsetLeft,
|
|
70065
|
+
top: currentElement.offsetTop,
|
|
70066
|
+
behavior: "instant"
|
|
70067
|
+
});
|
|
70068
|
+
}
|
|
70069
|
+
};
|
|
70070
|
+
// The travel is over: the stage is struck, every slide goes back where the map
|
|
70071
|
+
// says it is and the scroll position takes over from the track. Nothing is seen
|
|
70072
|
+
// moving for it, the slide on screen standing at the same place either way, as
|
|
70073
|
+
// long as it is all written in one go, which is why this is done here rather
|
|
70074
|
+
// than left to a render.
|
|
70674
70075
|
const settleTravel = () => {
|
|
70675
70076
|
const track = trackRef.current;
|
|
70676
70077
|
if (!track) {
|
|
@@ -70686,33 +70087,19 @@ const SlideContainer = ({
|
|
|
70686
70087
|
}
|
|
70687
70088
|
stageRef.current = null;
|
|
70688
70089
|
trackAnimationRef.current = null;
|
|
70689
|
-
movingRef.current = null;
|
|
70690
70090
|
// At rest the picture IS the current slide, whatever the last gesture wrote
|
|
70691
70091
|
// there: an indicator has nothing left to lean towards.
|
|
70692
|
-
paintTravelProgress(
|
|
70092
|
+
paintTravelProgress();
|
|
70093
|
+
writeRowAtRest(slideElements, placeOf, currentElement);
|
|
70693
70094
|
for (const slideElement of slideElements) {
|
|
70694
|
-
|
|
70695
|
-
x,
|
|
70696
|
-
y
|
|
70697
|
-
} = placeOf.get(readArea(slideElement)) || {
|
|
70698
|
-
x: 0,
|
|
70699
|
-
y: 0
|
|
70700
|
-
};
|
|
70701
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
70702
|
-
slideElement.toggleAttribute("data-slide-offstage", slideElement !== currentElement);
|
|
70095
|
+
slideElement.removeAttribute("data-slide-offstage");
|
|
70703
70096
|
}
|
|
70704
70097
|
const currentArea = readArea(currentElement);
|
|
70705
70098
|
drawnAreaRef.current = currentArea;
|
|
70706
|
-
|
|
70707
|
-
|
|
70708
|
-
|
|
70709
|
-
|
|
70710
|
-
x: 0,
|
|
70711
|
-
y: 0
|
|
70712
|
-
};
|
|
70713
|
-
const offset = `${-x * 100}% ${-y * 100}%`;
|
|
70714
|
-
offsetRef.current = offset;
|
|
70715
|
-
track.style.setProperty("--slide-container-offset", offset);
|
|
70099
|
+
offsetRef.current = "0% 0%";
|
|
70100
|
+
track.style.setProperty("--slide-container-offset", "0% 0%");
|
|
70101
|
+
containerRef.current.removeAttribute("data-slide-travelling");
|
|
70102
|
+
sendRowToRest(currentElement);
|
|
70716
70103
|
renderNear(currentArea, {
|
|
70717
70104
|
alongside: true
|
|
70718
70105
|
});
|
|
@@ -70765,11 +70152,12 @@ const SlideContainer = ({
|
|
|
70765
70152
|
if (landsWithPage && trackAnimationRef.current) {
|
|
70766
70153
|
trackAnimationRef.current.cancel();
|
|
70767
70154
|
trackAnimationRef.current = null;
|
|
70768
|
-
movingRef.current = null;
|
|
70769
70155
|
cancelTravelProgressAnimation();
|
|
70770
|
-
paintTravelProgress(
|
|
70156
|
+
paintTravelProgress();
|
|
70771
70157
|
}
|
|
70772
|
-
const
|
|
70158
|
+
const handLanded = handLandedRef.current === currentArea;
|
|
70159
|
+
handLandedRef.current = null;
|
|
70160
|
+
const still = noTravel || landsWithPage || handLanded;
|
|
70773
70161
|
// Nothing is travelling, so nothing is staged: the picture to paint is the
|
|
70774
70162
|
// map itself, and a stage left over from a travel that has just been given
|
|
70775
70163
|
// up on would be painted instead of it.
|
|
@@ -70835,19 +70223,30 @@ const SlideContainer = ({
|
|
|
70835
70223
|
// travel someone asked for has its own transfer waiting (focusHandOverRef),
|
|
70836
70224
|
// and that one knows what was pressed.
|
|
70837
70225
|
const focusIsLeaving = !focusHandOverRef.current && slideElements.some(slideElement => slideElement !== currentElement && slideElement.contains(document.activeElement));
|
|
70226
|
+
// Read before the row is written: what the slide holds decides where it ends.
|
|
70227
|
+
if (!stage) {
|
|
70228
|
+
writeRowAtRest(slideElements, placeOf, currentElement);
|
|
70229
|
+
}
|
|
70230
|
+
const onRow = ({
|
|
70231
|
+
x,
|
|
70232
|
+
y
|
|
70233
|
+
}) => ({
|
|
70234
|
+
x: x - originRef.current.x,
|
|
70235
|
+
y: y - originRef.current.y
|
|
70236
|
+
});
|
|
70838
70237
|
for (const slideElement of slideElements) {
|
|
70839
70238
|
const area = readArea(slideElement);
|
|
70840
|
-
const {
|
|
70841
|
-
x,
|
|
70842
|
-
y
|
|
70843
|
-
} = placeOfArea(area);
|
|
70844
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
70845
70239
|
const isCurrent = slideElement === currentElement;
|
|
70846
70240
|
slideElement.toggleAttribute("data-current", isCurrent);
|
|
70847
70241
|
slideElement.toggleAttribute("data-slide-displaced", !isCurrent);
|
|
70848
|
-
//
|
|
70849
|
-
//
|
|
70850
|
-
|
|
70242
|
+
// Off stage only while a travel asked for plays: at rest a hand can bring in
|
|
70243
|
+
// any of them, and it would find them unpainted.
|
|
70244
|
+
const onStage = Boolean(stage) && stage.placeByArea.has(area);
|
|
70245
|
+
slideElement.toggleAttribute("data-slide-offstage", Boolean(stage) && !onStage);
|
|
70246
|
+
if (onStage) {
|
|
70247
|
+
slideElement.removeAttribute("data-off-row");
|
|
70248
|
+
writeSlidePlace(slideElement, onRow(stage.placeByArea.get(area)));
|
|
70249
|
+
}
|
|
70851
70250
|
if (isCurrent) {
|
|
70852
70251
|
// Reachable again first, so the focus below has somewhere to land: an
|
|
70853
70252
|
// inert element cannot take it.
|
|
@@ -70855,12 +70254,14 @@ const SlideContainer = ({
|
|
|
70855
70254
|
}
|
|
70856
70255
|
}
|
|
70857
70256
|
const track = trackRef.current;
|
|
70858
|
-
|
|
70257
|
+
containerRef.current.toggleAttribute("data-slide-travelling", Boolean(stage));
|
|
70258
|
+
// How far the track stands from where the scroll position rests: none at rest.
|
|
70259
|
+
const rowPlace = onRow(currentPlace);
|
|
70260
|
+
const restPlace = restPlaceRef.current;
|
|
70261
|
+
const offset = `${(restPlace.x - rowPlace.x) * 100}% ${(restPlace.y - rowPlace.y) * 100}%`;
|
|
70859
70262
|
// The travel that was ASKED for is still one box, whatever is left of it to
|
|
70860
|
-
// cover:
|
|
70861
|
-
// the duration rather than taking a full one over a few pixels.
|
|
70263
|
+
// cover: what is left takes what is left of the duration.
|
|
70862
70264
|
const offsetTargetBefore = offsetRef.current;
|
|
70863
|
-
const targetMoves = offset !== offsetTargetBefore;
|
|
70864
70265
|
// Where the track IS, read before anything is written: a travel asked for
|
|
70865
70266
|
// while another is still playing must carry on from what is on screen. The
|
|
70866
70267
|
// previous TARGET is where the last travel was going, not where it got to —
|
|
@@ -70879,35 +70280,16 @@ const SlideContainer = ({
|
|
|
70879
70280
|
const onScreenPx = trackOffsetPx(track, containerRef.current);
|
|
70880
70281
|
offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
|
|
70881
70282
|
}
|
|
70882
|
-
|
|
70883
|
-
// said by the gesture that put it there: the track is at rest as far as any
|
|
70884
|
-
// animation is concerned, so nothing else could tell. Taken by the render
|
|
70885
|
-
// that moves the track and by no other: a `current` held outside catches up
|
|
70886
|
-
// with the gesture a render late (a parent's state, set from a handler that
|
|
70887
|
-
// runs after the address is written and read back), and a render still
|
|
70888
|
-
// standing on the slide being left would otherwise carry the finger's
|
|
70889
|
-
// offset back to it — a travel home nobody asked for.
|
|
70890
|
-
const offsetDragged = targetMoves ? travelFromRef.current : null;
|
|
70891
|
-
if (targetMoves) {
|
|
70892
|
-
travelFromRef.current = null;
|
|
70893
|
-
}
|
|
70894
|
-
const offsetBefore = offsetDragged ?? offsetOnScreen ?? offsetRef.current;
|
|
70283
|
+
const offsetBefore = offsetOnScreen ?? offsetRef.current;
|
|
70895
70284
|
offsetRef.current = offset;
|
|
70896
|
-
|
|
70897
|
-
// there, and when it is over this is what holds. Except under a hand-off
|
|
70898
|
-
// still waiting for its travel: the track stays where the finger left it,
|
|
70899
|
-
// until the render that moves it or the frame after which the gesture gives
|
|
70900
|
-
// up on it (see settleTowards in onEnd).
|
|
70901
|
-
if (!travelFromRef.current) {
|
|
70902
|
-
track.style.setProperty("--slide-container-offset", offset);
|
|
70903
|
-
}
|
|
70285
|
+
track.style.setProperty("--slide-container-offset", offset);
|
|
70904
70286
|
// A travel to this very offset is already playing: left to play. Same
|
|
70905
70287
|
// courtesy as the stage above — a re-render in the middle of a travel (a
|
|
70906
70288
|
// signal read higher up answering) changes nothing here, and setting off
|
|
70907
70289
|
// again from the on-screen position would replay what is left over a full
|
|
70908
70290
|
// duration (the ask is zero, so ratioOfOneTravel answers 1), slowing the
|
|
70909
70291
|
// travel at every re-render.
|
|
70910
|
-
const sameTravelPlaying = travelInFlight &&
|
|
70292
|
+
const sameTravelPlaying = travelInFlight && offsetTargetBefore === offset;
|
|
70911
70293
|
const travels = !sameTravelPlaying && !still && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
70912
70294
|
if (travels) {
|
|
70913
70295
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
@@ -70917,10 +70299,9 @@ const SlideContainer = ({
|
|
|
70917
70299
|
// this only ever shortens it (a longer travel is not made slower, which
|
|
70918
70300
|
// would make a two-box move drag).
|
|
70919
70301
|
const travelRatio = ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
|
|
70920
|
-
// Already moving
|
|
70921
|
-
//
|
|
70922
|
-
|
|
70923
|
-
const easing = travelInFlight || offsetDragged ? "ease-out" : "ease";
|
|
70302
|
+
// Already moving, so there is no ease-in to play: it would stall the track
|
|
70303
|
+
// for an instant right where the eye is following it.
|
|
70304
|
+
const easing = travelInFlight ? "ease-out" : "ease";
|
|
70924
70305
|
// Cancelled rather than layered: two animations on the same property
|
|
70925
70306
|
// would blend, and what one sees then is neither of the two moves.
|
|
70926
70307
|
trackAnimationRef.current?.cancel();
|
|
@@ -70932,14 +70313,8 @@ const SlideContainer = ({
|
|
|
70932
70313
|
duration: durationMs * travelRatio,
|
|
70933
70314
|
easing
|
|
70934
70315
|
});
|
|
70935
|
-
|
|
70936
|
-
|
|
70937
|
-
to: currentArea
|
|
70938
|
-
};
|
|
70939
|
-
// The trait travels with the slides: from where the gesture left it when
|
|
70940
|
-
// there was one, from a whole box away when the travel was asked for.
|
|
70941
|
-
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
70942
|
-
travelProgressFromRef.current = null;
|
|
70316
|
+
// The trait travels with the slides, from a whole box away.
|
|
70317
|
+
const progressFrom = travelStep ? travelStep.x || travelStep.y : 0;
|
|
70943
70318
|
// The slide the picture leans on for the length of it is the one being
|
|
70944
70319
|
// LEFT: it is the second one in the frame until the travel is over.
|
|
70945
70320
|
animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
|
|
@@ -70956,12 +70331,11 @@ const SlideContainer = ({
|
|
|
70956
70331
|
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
70957
70332
|
// cancelled by the next travel — that one carries the stage on
|
|
70958
70333
|
});
|
|
70959
|
-
} else if (stage &&
|
|
70334
|
+
} else if (stage && trackAnimationRef.current?.playState !== "running") {
|
|
70960
70335
|
// Staged with nothing left to play: a travel that was drawn and then had
|
|
70961
70336
|
// its animation taken away (a duration set to 0, a re-render landing
|
|
70962
70337
|
// between the two). Struck at once rather than left standing, since the
|
|
70963
|
-
// thing it was standing for is over.
|
|
70964
|
-
// standing for something — the finger still holding it.
|
|
70338
|
+
// thing it was standing for is over.
|
|
70965
70339
|
settleTravel();
|
|
70966
70340
|
}
|
|
70967
70341
|
// A window waiting for its travel to be over (see goToArea's own loop
|
|
@@ -71021,6 +70395,11 @@ const SlideContainer = ({
|
|
|
71021
70395
|
renderNear(settlingArea, {
|
|
71022
70396
|
alongside: !stageRef.current
|
|
71023
70397
|
});
|
|
70398
|
+
// Something moved the row under it at rest: an opening, a page changing, a
|
|
70399
|
+
// slide added before it. Under a hand it stays where the hand has it.
|
|
70400
|
+
if (!stageRef.current && !handMovingRef.current) {
|
|
70401
|
+
sendRowToRest(currentElement);
|
|
70402
|
+
}
|
|
71024
70403
|
// Said last, and after data-current has been written: what a travel would do
|
|
71025
70404
|
// is read off the map from the slide that is now current, and off the locks
|
|
71026
70405
|
// that slide is wearing in this very commit.
|
|
@@ -71029,9 +70408,6 @@ const SlideContainer = ({
|
|
|
71029
70408
|
// — a single announcement for a render that may have changed several of the
|
|
71030
70409
|
// facts at once.
|
|
71031
70410
|
announceState();
|
|
71032
|
-
// The finger has the last word: everything above drew the map at rest, and
|
|
71033
|
-
// where the track actually is right now is where the gesture put it.
|
|
71034
|
-
paintDrag();
|
|
71035
70411
|
});
|
|
71036
70412
|
|
|
71037
70413
|
/**
|
|
@@ -71044,7 +70420,8 @@ const SlideContainer = ({
|
|
|
71044
70420
|
value,
|
|
71045
70421
|
released,
|
|
71046
70422
|
dx = 0,
|
|
71047
|
-
dy = 0
|
|
70423
|
+
dy = 0,
|
|
70424
|
+
landed
|
|
71048
70425
|
} = {}) => {
|
|
71049
70426
|
// A window mid-roll cannot travel yet: it is on its way somewhere and the
|
|
71050
70427
|
// content that goes with it has not moved, so a second travel would be
|
|
@@ -71172,7 +70549,7 @@ const SlideContainer = ({
|
|
|
71172
70549
|
// A caller writing the change somewhere that keeps a trace — the URL, a
|
|
71173
70550
|
// history — needs it to know whether a place was aimed at or browsed past.
|
|
71174
70551
|
const cause = causeOfEvent(event);
|
|
71175
|
-
if (commit === "rest") {
|
|
70552
|
+
if (commit === "rest" && !landed) {
|
|
71176
70553
|
// The travel first, the change once it is over: a caller putting it
|
|
71177
70554
|
// somewhere expensive or visible (the URL, the address bar, a server)
|
|
71178
70555
|
// must not be asked for it sixty times a second, and the picture must not
|
|
@@ -71557,22 +70934,152 @@ const SlideContainer = ({
|
|
|
71557
70934
|
return area;
|
|
71558
70935
|
};
|
|
71559
70936
|
|
|
71560
|
-
// Where
|
|
71561
|
-
//
|
|
71562
|
-
|
|
71563
|
-
|
|
71564
|
-
|
|
71565
|
-
// Nothing to paint for a pointer that is only resting on the box: until it
|
|
71566
|
-
// has an axis a gesture has moved nothing and knows no geometry.
|
|
71567
|
-
if (!drag || !drag.axis || !track) {
|
|
70937
|
+
// Where a hand has the row, in the terms of --slide-travel-progress: from the
|
|
70938
|
+
// current slide while it stands next to it, and past that between the two it
|
|
70939
|
+
// stands between, as far as the hand carries it.
|
|
70940
|
+
const paintNativeRow = () => {
|
|
70941
|
+
if (trackAnimationRef.current?.playState === "running") {
|
|
71568
70942
|
return;
|
|
71569
70943
|
}
|
|
71570
|
-
const
|
|
71571
|
-
const
|
|
71572
|
-
|
|
71573
|
-
|
|
71574
|
-
|
|
70944
|
+
const containerEl = containerRef.current;
|
|
70945
|
+
const slideElements = Array.from(trackRef.current.children);
|
|
70946
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
70947
|
+
if (!currentElement) {
|
|
70948
|
+
return;
|
|
70949
|
+
}
|
|
70950
|
+
if (restsOn(containerEl, currentElement)) {
|
|
70951
|
+
if (handMovingRef.current) {
|
|
70952
|
+
paintTravelProgress();
|
|
70953
|
+
}
|
|
70954
|
+
return;
|
|
70955
|
+
}
|
|
70956
|
+
handMovingRef.current = true;
|
|
70957
|
+
const {
|
|
70958
|
+
base,
|
|
70959
|
+
toward,
|
|
70960
|
+
fraction,
|
|
70961
|
+
forward
|
|
70962
|
+
} = readRowStretch(containerEl, slideElements, currentElement);
|
|
70963
|
+
const baseArea = readArea(base);
|
|
70964
|
+
for (const element of travelPainters()) {
|
|
70965
|
+
if (base === currentElement) {
|
|
70966
|
+
element.removeAttribute(SLIDE_BASE_ATTRIBUTE);
|
|
70967
|
+
} else {
|
|
70968
|
+
element.setAttribute(SLIDE_BASE_ATTRIBUTE, baseArea);
|
|
70969
|
+
}
|
|
70970
|
+
element.style.setProperty("--slide-travel-progress", forward ? -fraction : fraction);
|
|
70971
|
+
}
|
|
70972
|
+
paintTravelToward(toward ? readArea(toward) : null);
|
|
70973
|
+
// What the hand reaches ahead of the neighbours built at rest is asked for on
|
|
70974
|
+
// the way: an empty slide goes by otherwise.
|
|
70975
|
+
renderNear(baseArea, {
|
|
70976
|
+
alongside: true
|
|
70977
|
+
});
|
|
71575
70978
|
};
|
|
70979
|
+
const landNativeRow = scrollEndEvent => {
|
|
70980
|
+
if (!handMovingRef.current) {
|
|
70981
|
+
return;
|
|
70982
|
+
}
|
|
70983
|
+
const containerEl = containerRef.current;
|
|
70984
|
+
const {
|
|
70985
|
+
slideElements,
|
|
70986
|
+
placeOf
|
|
70987
|
+
} = readMap();
|
|
70988
|
+
const landedElement = pageAtRest(containerEl, slideElements);
|
|
70989
|
+
if (!landedElement) {
|
|
70990
|
+
return;
|
|
70991
|
+
}
|
|
70992
|
+
handMovingRef.current = false;
|
|
70993
|
+
paintTravelProgress();
|
|
70994
|
+
const area = readArea(landedElement);
|
|
70995
|
+
const currentArea = containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
70996
|
+
if (area === currentArea) {
|
|
70997
|
+
settleTravel();
|
|
70998
|
+
return;
|
|
70999
|
+
}
|
|
71000
|
+
const event = scrollEndEvent ?? new Event("scrollend");
|
|
71001
|
+
if (loop) {
|
|
71002
|
+
// A window does not change slide, it rolls: back at rest in the render the
|
|
71003
|
+
// content moves in, the two being the same picture.
|
|
71004
|
+
const from = placeOf.get(currentArea) || {
|
|
71005
|
+
x: 0,
|
|
71006
|
+
y: 0
|
|
71007
|
+
};
|
|
71008
|
+
const to = placeOf.get(area) || {
|
|
71009
|
+
x: 0,
|
|
71010
|
+
y: 0
|
|
71011
|
+
};
|
|
71012
|
+
setNoTravel(true);
|
|
71013
|
+
onLoop?.({
|
|
71014
|
+
area,
|
|
71015
|
+
dx: Math.sign(to.x - from.x),
|
|
71016
|
+
dy: Math.sign(to.y - from.y),
|
|
71017
|
+
event
|
|
71018
|
+
});
|
|
71019
|
+
return;
|
|
71020
|
+
}
|
|
71021
|
+
handLandedRef.current = area;
|
|
71022
|
+
if (goToArea(area, {
|
|
71023
|
+
event,
|
|
71024
|
+
landed: true
|
|
71025
|
+
})) {
|
|
71026
|
+
return;
|
|
71027
|
+
}
|
|
71028
|
+
// Refused: the row is put back in view of the slide that stays.
|
|
71029
|
+
handLandedRef.current = null;
|
|
71030
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
71031
|
+
containerEl.scrollTo({
|
|
71032
|
+
left: currentElement.offsetLeft,
|
|
71033
|
+
top: currentElement.offsetTop,
|
|
71034
|
+
behavior: "smooth"
|
|
71035
|
+
});
|
|
71036
|
+
};
|
|
71037
|
+
|
|
71038
|
+
// Reached through a ref, and the listeners are never rebuilt for it: a landing
|
|
71039
|
+
// changes the current slide, and one is often already on its way when it does.
|
|
71040
|
+
const nativeRowHandlersRef = useRef(null);
|
|
71041
|
+
nativeRowHandlersRef.current = {
|
|
71042
|
+
paintNativeRow,
|
|
71043
|
+
landNativeRow
|
|
71044
|
+
};
|
|
71045
|
+
useLayoutEffect(() => {
|
|
71046
|
+
return watchRowScroll(containerRef.current, {
|
|
71047
|
+
onScroll: () => nativeRowHandlersRef.current.paintNativeRow(),
|
|
71048
|
+
onLand: event => nativeRowHandlersRef.current.landNativeRow(event)
|
|
71049
|
+
});
|
|
71050
|
+
}, []);
|
|
71051
|
+
|
|
71052
|
+
// A wheel on an axis only a finger travels (travelByScroll) is not this box's,
|
|
71053
|
+
// though a scroller takes it all the same: it goes to what scrolls inside, or
|
|
71054
|
+
// on to what scrolls around, which is where it was headed.
|
|
71055
|
+
const wheelRefusedAxes = Array.from(dragAxes ?? "").filter(axis => !scrollAxes?.includes(axis)).join("");
|
|
71056
|
+
useLayoutEffect(() => {
|
|
71057
|
+
if (!wheelRefusedAxes) {
|
|
71058
|
+
return undefined;
|
|
71059
|
+
}
|
|
71060
|
+
const containerEl = containerRef.current;
|
|
71061
|
+
const onWheel = wheelEvent => {
|
|
71062
|
+
const axis = Math.abs(wheelEvent.deltaX) > Math.abs(wheelEvent.deltaY) ? "x" : "y";
|
|
71063
|
+
if (!wheelRefusedAxes.includes(axis)) {
|
|
71064
|
+
return;
|
|
71065
|
+
}
|
|
71066
|
+
const delta = axis === "x" ? wheelEvent.deltaX : wheelEvent.deltaY;
|
|
71067
|
+
if (!delta || scrollRoomTowards(wheelEvent.target, containerEl, axis, delta > 0 ? -1 : 1)) {
|
|
71068
|
+
return;
|
|
71069
|
+
}
|
|
71070
|
+
wheelEvent.preventDefault();
|
|
71071
|
+
getScrollContainer(containerEl.parentElement)?.scrollBy({
|
|
71072
|
+
[axis === "x" ? "left" : "top"]: delta,
|
|
71073
|
+
behavior: "instant"
|
|
71074
|
+
});
|
|
71075
|
+
};
|
|
71076
|
+
containerEl.addEventListener("wheel", onWheel, {
|
|
71077
|
+
passive: false
|
|
71078
|
+
});
|
|
71079
|
+
return () => {
|
|
71080
|
+
containerEl.removeEventListener("wheel", onWheel);
|
|
71081
|
+
};
|
|
71082
|
+
}, [wheelRefusedAxes]);
|
|
71576
71083
|
|
|
71577
71084
|
// Everything that draws this travel: the box itself and whoever follows it.
|
|
71578
71085
|
const travelPainters = () => {
|
|
@@ -71705,13 +71212,12 @@ const SlideContainer = ({
|
|
|
71705
71212
|
// current slide changes under it.
|
|
71706
71213
|
const paintTravelProgress = (progress, area) => {
|
|
71707
71214
|
for (const element of travelPainters()) {
|
|
71708
|
-
|
|
71709
|
-
element.style.setProperty("--slide-travel-progress", progress);
|
|
71710
|
-
} else {
|
|
71215
|
+
{
|
|
71711
71216
|
element.style.removeProperty("--slide-travel-progress");
|
|
71217
|
+
element.removeAttribute(SLIDE_BASE_ATTRIBUTE);
|
|
71712
71218
|
}
|
|
71713
71219
|
}
|
|
71714
|
-
paintTravelToward(
|
|
71220
|
+
paintTravelToward(null);
|
|
71715
71221
|
};
|
|
71716
71222
|
const cancelTravelProgressAnimation = () => {
|
|
71717
71223
|
for (const animation of progressAnimationsRef.current) {
|
|
@@ -71731,7 +71237,7 @@ const SlideContainer = ({
|
|
|
71731
71237
|
// draw is the one about to start.
|
|
71732
71238
|
followerElementsRef.current = readFollowerElements(containerRef.current);
|
|
71733
71239
|
cancelTravelProgressAnimation();
|
|
71734
|
-
paintTravelProgress(
|
|
71240
|
+
paintTravelProgress();
|
|
71735
71241
|
const painters = travelPainters();
|
|
71736
71242
|
if (!painters.length || !from || !durationMs) {
|
|
71737
71243
|
return;
|
|
@@ -71752,598 +71258,8 @@ const SlideContainer = ({
|
|
|
71752
71258
|
// cancelled by the next travel — that one says where the trait goes
|
|
71753
71259
|
});
|
|
71754
71260
|
};
|
|
71755
|
-
|
|
71756
|
-
// The two slides the gesture can bring in, placed one box either side of the
|
|
71757
|
-
// one being dragged — the same stage a travel builds, except that both ends
|
|
71758
|
-
// are set up at once because the finger has not said yet which way it goes.
|
|
71759
|
-
const stageDrag = (drag, currentArea) => {
|
|
71760
|
-
const {
|
|
71761
|
-
slideElements,
|
|
71762
|
-
placeOf
|
|
71763
|
-
} = readMap();
|
|
71764
|
-
const step = drag.axis === "x" ? {
|
|
71765
|
-
x: 1,
|
|
71766
|
-
y: 0
|
|
71767
|
-
} : {
|
|
71768
|
-
x: 0,
|
|
71769
|
-
y: 1
|
|
71770
|
-
};
|
|
71771
|
-
const placeByArea = new Map();
|
|
71772
|
-
placeByArea.set(drag.area, drag.basePlace);
|
|
71773
|
-
if (drag.areaBack) {
|
|
71774
|
-
placeByArea.set(drag.areaBack, {
|
|
71775
|
-
x: drag.basePlace.x - step.x,
|
|
71776
|
-
y: drag.basePlace.y - step.y
|
|
71777
|
-
});
|
|
71778
|
-
}
|
|
71779
|
-
if (drag.areaOn) {
|
|
71780
|
-
placeByArea.set(drag.areaOn, {
|
|
71781
|
-
x: drag.basePlace.x + step.x,
|
|
71782
|
-
y: drag.basePlace.y + step.y
|
|
71783
|
-
});
|
|
71784
|
-
}
|
|
71785
|
-
stageRef.current = {
|
|
71786
|
-
placeByArea,
|
|
71787
|
-
area: currentArea
|
|
71788
|
-
};
|
|
71789
|
-
for (const slideElement of slideElements) {
|
|
71790
|
-
const area = readArea(slideElement);
|
|
71791
|
-
const {
|
|
71792
|
-
x,
|
|
71793
|
-
y
|
|
71794
|
-
} = placeByArea.get(area) || placeOf.get(area) || {
|
|
71795
|
-
x: 0,
|
|
71796
|
-
y: 0
|
|
71797
|
-
};
|
|
71798
|
-
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
71799
|
-
slideElement.toggleAttribute("data-slide-offstage", !placeByArea.has(area));
|
|
71800
|
-
}
|
|
71801
|
-
};
|
|
71802
|
-
|
|
71803
|
-
// Let go of, and the slides go where the gesture leaves them: back on the
|
|
71804
|
-
// slide being dragged when nothing travels, on to the slide arriving when a
|
|
71805
|
-
// travel caught in flight is let carry on. Over the distance left to cover,
|
|
71806
|
-
// so a slide barely moved snaps back and one dragged most of the way there
|
|
71807
|
-
// takes its time.
|
|
71808
|
-
const settleTowards = (drag, area) => {
|
|
71809
|
-
const track = trackRef.current;
|
|
71810
|
-
if (!track) {
|
|
71811
|
-
return;
|
|
71812
|
-
}
|
|
71813
|
-
const stage = stageRef.current;
|
|
71814
|
-
const {
|
|
71815
|
-
placeOf
|
|
71816
|
-
} = readMap();
|
|
71817
|
-
const place = stage?.placeByArea.get(area) || placeOf.get(area) || {
|
|
71818
|
-
x: 0,
|
|
71819
|
-
y: 0
|
|
71820
|
-
};
|
|
71821
|
-
const restPx = {
|
|
71822
|
-
x: -place.x * drag.box.width,
|
|
71823
|
-
y: -place.y * drag.box.height
|
|
71824
|
-
};
|
|
71825
|
-
const nowPx = {
|
|
71826
|
-
x: drag.baseOffset.x + drag.pull.x,
|
|
71827
|
-
y: drag.baseOffset.y + drag.pull.y
|
|
71828
|
-
};
|
|
71829
|
-
const restOffset = `${restPx.x}px ${restPx.y}px`;
|
|
71830
|
-
const durationMs = durationToMs(duration);
|
|
71831
|
-
const size = drag.axis === "x" ? drag.box.width : drag.box.height;
|
|
71832
|
-
const left = Math.abs(restPx[drag.axis] - nowPx[drag.axis]);
|
|
71833
|
-
trackAnimationRef.current?.cancel();
|
|
71834
|
-
trackAnimationRef.current = null;
|
|
71835
|
-
track.style.setProperty("--slide-container-offset", restOffset);
|
|
71836
|
-
if (!durationMs || !left) {
|
|
71837
|
-
paintTravelProgress(0);
|
|
71838
|
-
settleTravel();
|
|
71839
|
-
return;
|
|
71840
|
-
}
|
|
71841
|
-
// Which other slide the picture leans on while it arrives: the one being
|
|
71842
|
-
// pulled in when it comes back on the dragged slide, the dragged slide
|
|
71843
|
-
// itself when it goes on to the one arriving — and the trait is then a
|
|
71844
|
-
// box short of that one, which is what it has to close.
|
|
71845
|
-
const arrivesOnBase = area === drag.area;
|
|
71846
|
-
const sign = drag.pull[drag.axis] > 0 ? 1 : -1;
|
|
71847
|
-
const progressFrom = arrivesOnBase ? drag.progress : drag.progress - sign;
|
|
71848
|
-
const otherArea = arrivesOnBase ? drag.areaPulled : drag.area;
|
|
71849
|
-
movingRef.current = {
|
|
71850
|
-
from: otherArea,
|
|
71851
|
-
to: area
|
|
71852
|
-
};
|
|
71853
|
-
animateTravelProgress(progressFrom, durationMs * (left / size), "ease-out", otherArea);
|
|
71854
|
-
const animation = track.animate([{
|
|
71855
|
-
translate: `${nowPx.x}px ${nowPx.y}px`
|
|
71856
|
-
}, {
|
|
71857
|
-
translate: restOffset
|
|
71858
|
-
}], {
|
|
71859
|
-
duration: durationMs * (left / size),
|
|
71860
|
-
easing: "ease-out"
|
|
71861
|
-
});
|
|
71862
|
-
trackAnimationRef.current = animation;
|
|
71863
|
-
animation.finished.then(settleTravel, () => {
|
|
71864
|
-
// cancelled by a travel asked for since — that one carries the stage on
|
|
71865
|
-
});
|
|
71866
|
-
};
|
|
71867
|
-
|
|
71868
|
-
// A travel that is playing when a gesture takes hold of it is STOPPED where it
|
|
71869
|
-
// stands, at the moment the hand holds it (see readTravelInFlight for when
|
|
71870
|
-
// that is): the track stays exactly under the eye, and the hand answers it
|
|
71871
|
-
// from there.
|
|
71872
|
-
// A gesture that turns out to be nothing lets the travel carry on from where
|
|
71873
|
-
// it was caught (see onGiveUp).
|
|
71874
|
-
const catchTravelInFlight = () => {
|
|
71875
|
-
const trackElement = trackRef.current;
|
|
71876
|
-
const travelCaught = trackAnimationRef.current;
|
|
71877
|
-
if (!travelCaught || travelCaught.playState !== "running") {
|
|
71878
|
-
return null;
|
|
71879
|
-
}
|
|
71880
|
-
const onScreenPx = trackOffsetPx(trackElement, containerRef.current);
|
|
71881
|
-
const offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
|
|
71882
|
-
travelCaught.cancel();
|
|
71883
|
-
// Where it was, held: cancelling an animation puts the track back on the
|
|
71884
|
-
// value underneath it, which is the far end of the travel — the very jump
|
|
71885
|
-
// this is about.
|
|
71886
|
-
trackElement.style.setProperty("--slide-container-offset", offsetOnScreen);
|
|
71887
|
-
const moving = movingRef.current || {};
|
|
71888
|
-
return {
|
|
71889
|
-
trackElement,
|
|
71890
|
-
onScreenPx,
|
|
71891
|
-
offsetOnScreen,
|
|
71892
|
-
offsetTarget: offsetRef.current,
|
|
71893
|
-
from: moving.from,
|
|
71894
|
-
to: moving.to
|
|
71895
|
-
};
|
|
71896
|
-
};
|
|
71897
|
-
// Which way a travel still playing is going, read without touching it: a
|
|
71898
|
-
// hand landing on something moving has no axis left to decide (a first pixel
|
|
71899
|
-
// of tremor read as one gives the gesture up), and which WAY along it the
|
|
71900
|
-
// travel goes is what tells the next swipe from a catch. `way` is the sign a
|
|
71901
|
-
// pull going the same way would have.
|
|
71902
|
-
const readTravelInFlight = () => {
|
|
71903
|
-
const trackElement = trackRef.current;
|
|
71904
|
-
const animation = trackAnimationRef.current;
|
|
71905
|
-
if (!animation || animation.playState !== "running") {
|
|
71906
|
-
return null;
|
|
71907
|
-
}
|
|
71908
|
-
const onScreenPx = trackOffsetPx(trackElement, containerRef.current);
|
|
71909
|
-
const targetPx = offsetToPx(offsetRef.current, trackElement.getBoundingClientRect());
|
|
71910
|
-
const towardsX = targetPx.x - onScreenPx.x;
|
|
71911
|
-
const towardsY = targetPx.y - onScreenPx.y;
|
|
71912
|
-
const axis = Math.abs(towardsX) >= Math.abs(towardsY) ? "x" : "y";
|
|
71913
|
-
const towards = axis === "x" ? towardsX : towardsY;
|
|
71914
|
-
if (Math.abs(towards) < 1) {
|
|
71915
|
-
// On its last pixel: nothing left to catch or to push.
|
|
71916
|
-
return null;
|
|
71917
|
-
}
|
|
71918
|
-
return {
|
|
71919
|
-
axis,
|
|
71920
|
-
way: towards > 0 ? 1 : -1
|
|
71921
|
-
};
|
|
71922
|
-
};
|
|
71923
|
-
|
|
71924
|
-
// What a travel gesture does to the slides, whoever asked for it: a pointer
|
|
71925
|
-
// dragging the box and a wheel pushing it sideways ask for the same travel,
|
|
71926
|
-
// so they are answered by the same callbacks and only the reading of the
|
|
71927
|
-
// input differs (see drag_to_travel.js). The rules of the gesture are read
|
|
71928
|
-
// there, the geometry of the slides here.
|
|
71929
|
-
const createTravelHandlers = caughtAtStart => {
|
|
71930
|
-
let caughtTravel = caughtAtStart;
|
|
71931
|
-
// The travel in hand, as the slides see it: where the one being dragged
|
|
71932
|
-
// stands, what is either side of it, and how far the gesture has taken it.
|
|
71933
|
-
const drag = {
|
|
71934
|
-
axis: null,
|
|
71935
|
-
area: null,
|
|
71936
|
-
areaBack: null,
|
|
71937
|
-
areaOn: null,
|
|
71938
|
-
areaPulled: null,
|
|
71939
|
-
box: null,
|
|
71940
|
-
basePlace: null,
|
|
71941
|
-
baseOffset: null,
|
|
71942
|
-
pull: {
|
|
71943
|
-
x: 0,
|
|
71944
|
-
y: 0
|
|
71945
|
-
},
|
|
71946
|
-
progress: 0,
|
|
71947
|
-
offset: null,
|
|
71948
|
-
gesture: null
|
|
71949
|
-
};
|
|
71950
|
-
return {
|
|
71951
|
-
drag,
|
|
71952
|
-
onStart: ({
|
|
71953
|
-
axis,
|
|
71954
|
-
sign,
|
|
71955
|
-
target,
|
|
71956
|
-
caught
|
|
71957
|
-
}) => {
|
|
71958
|
-
// A travel left playing under the press, which the hand has now taken
|
|
71959
|
-
// hold of: stopped here, where it stands at this moment.
|
|
71960
|
-
if (caught) {
|
|
71961
|
-
caughtTravel = catchTravelInFlight();
|
|
71962
|
-
}
|
|
71963
|
-
// Everything positional is read HERE rather than when the pointer
|
|
71964
|
-
// landed: the travel that was playing then may have arrived since, and
|
|
71965
|
-
// it is what the slides are doing at the moment the gesture takes them
|
|
71966
|
-
// over that the gesture must carry on from.
|
|
71967
|
-
const track = trackRef.current;
|
|
71968
|
-
const {
|
|
71969
|
-
slideElements,
|
|
71970
|
-
placeOf
|
|
71971
|
-
} = readMap();
|
|
71972
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
71973
|
-
const box = track.getBoundingClientRect();
|
|
71974
|
-
if (!currentElement || !box.width || !box.height) {
|
|
71975
|
-
return false;
|
|
71976
|
-
}
|
|
71977
|
-
const currentArea = readArea(currentElement);
|
|
71978
|
-
const stage = stageRef.current;
|
|
71979
|
-
// Where a slide stands: where the stage put it while a travel is
|
|
71980
|
-
// playing, its place on the map otherwise.
|
|
71981
|
-
const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
|
|
71982
|
-
x: 0,
|
|
71983
|
-
y: 0
|
|
71984
|
-
};
|
|
71985
|
-
// A travel caught in flight is measured from the slide it SET OFF from,
|
|
71986
|
-
// with the slide it was going to as the one thing the hand can bring
|
|
71987
|
-
// in — the way drag_to_travel reads a `slack`: how far the box already
|
|
71988
|
-
// sits from its resting place, towards what is being pulled in. That is
|
|
71989
|
-
// what makes its answer "carry on" mean arriving. Based on the slide
|
|
71990
|
-
// arriving instead, the slack would point back at the origin, and a
|
|
71991
|
-
// hand that merely touched the box would send it all the way back.
|
|
71992
|
-
// A movement with no origin of its own (the track re-placed without a
|
|
71993
|
-
// travel) is dragged from the slide shown, like any slide at rest.
|
|
71994
|
-
const origin = caughtTravel && caughtTravel.from !== caughtTravel.to ? caughtTravel.from : null;
|
|
71995
|
-
let area;
|
|
71996
|
-
let areaBack;
|
|
71997
|
-
let areaOn;
|
|
71998
|
-
if (origin) {
|
|
71999
|
-
area = origin;
|
|
72000
|
-
const towardsEnd = axis === "x" ? placeOfArea(currentArea).x > placeOfArea(origin).x : placeOfArea(currentArea).y > placeOfArea(origin).y;
|
|
72001
|
-
areaBack = towardsEnd ? undefined : currentArea;
|
|
72002
|
-
areaOn = towardsEnd ? currentArea : undefined;
|
|
72003
|
-
} else {
|
|
72004
|
-
area = currentArea;
|
|
72005
|
-
areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
|
|
72006
|
-
areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
|
|
72007
|
-
// The hold goToArea reads at the release, read again HERE, off the
|
|
72008
|
-
// same slide and the same attribute: a slide that will refuse the
|
|
72009
|
-
// arrival must not offer the journey. A locked direction simply has
|
|
72010
|
-
// nowhere to go for the length of this gesture — the one case the
|
|
72011
|
-
// gesture already knows, being the last slide of a walk. The hand
|
|
72012
|
-
// then gets the wall it can lean on and never walk through (see
|
|
72013
|
-
// drag_to_travel), the slide behind it stays offstage instead of
|
|
72014
|
-
// being read on the way, and the release has nothing left to refuse.
|
|
72015
|
-
// Nothing here about `released` (--navi-done): that is one particular
|
|
72016
|
-
// departure letting go, decided as it happens, and a gesture armed
|
|
72017
|
-
// before it has no such thing to read — the attribute as rendered is
|
|
72018
|
-
// what the hand is answered from.
|
|
72019
|
-
if (currentElement.hasAttribute("data-prevent-nav-previous")) {
|
|
72020
|
-
areaBack = undefined;
|
|
72021
|
-
}
|
|
72022
|
-
if (currentElement.hasAttribute("data-prevent-nav-next")) {
|
|
72023
|
-
areaOn = undefined;
|
|
72024
|
-
}
|
|
72025
|
-
if (!areaBack && !areaOn ||
|
|
72026
|
-
// Something else with a better claim on the gesture: a scroller
|
|
72027
|
-
// between the finger and the slide, with room left that way.
|
|
72028
|
-
scrollRoomTowards(target, currentElement, axis, sign)) {
|
|
72029
|
-
return false;
|
|
72030
|
-
}
|
|
72031
|
-
}
|
|
72032
|
-
const basePlace = placeOfArea(area);
|
|
72033
|
-
const baseOffset = {
|
|
72034
|
-
x: -basePlace.x * box.width,
|
|
72035
|
-
y: -basePlace.y * box.height
|
|
72036
|
-
};
|
|
72037
|
-
// Where the track IS: a travel grabbed mid-flight was already stopped
|
|
72038
|
-
// where the eye saw it, at the press — this only reads that place, so
|
|
72039
|
-
// the gesture starts from it rather than from where the map rests.
|
|
72040
|
-
const onScreen = caughtTravel ? caughtTravel.onScreenPx : baseOffset;
|
|
72041
|
-
caughtTravel = null;
|
|
72042
|
-
trackAnimationRef.current?.cancel();
|
|
72043
|
-
trackAnimationRef.current = null;
|
|
72044
|
-
cancelTravelProgressAnimation();
|
|
72045
|
-
followerElementsRef.current = readFollowerElements(containerRef.current);
|
|
72046
|
-
drag.axis = axis;
|
|
72047
|
-
drag.areaBack = areaBack;
|
|
72048
|
-
drag.areaOn = areaOn;
|
|
72049
|
-
drag.area = area;
|
|
72050
|
-
drag.box = box;
|
|
72051
|
-
drag.basePlace = basePlace;
|
|
72052
|
-
drag.baseOffset = baseOffset;
|
|
72053
|
-
const size = axis === "x" ? box.width : box.height;
|
|
72054
|
-
// Where the track was when the gesture took it over — nowhere, unless
|
|
72055
|
-
// it was travelling. Every pull is measured from it.
|
|
72056
|
-
const slack = axis === "x" ? onScreen.x - baseOffset.x : onScreen.y - baseOffset.y;
|
|
72057
|
-
drag.pull = {
|
|
72058
|
-
x: 0,
|
|
72059
|
-
y: 0,
|
|
72060
|
-
[axis]: slack
|
|
72061
|
-
};
|
|
72062
|
-
drag.progress = slack / size;
|
|
72063
|
-
drag.areaPulled = slack > 0 ? areaBack : slack < 0 ? areaOn : null;
|
|
72064
|
-
stageDrag(drag, currentArea);
|
|
72065
|
-
// From here the box is busy, whichever input asked: a wheel gesture and
|
|
72066
|
-
// a press must not both be moving the same track.
|
|
72067
|
-
dragRef.current = drag;
|
|
72068
|
-
return {
|
|
72069
|
-
size,
|
|
72070
|
-
slack,
|
|
72071
|
-
travelBack: Boolean(areaBack),
|
|
72072
|
-
travelOn: Boolean(areaOn)
|
|
72073
|
-
};
|
|
72074
|
-
},
|
|
72075
|
-
onPull: ({
|
|
72076
|
-
axis,
|
|
72077
|
-
pulled,
|
|
72078
|
-
progress
|
|
72079
|
-
}) => {
|
|
72080
|
-
drag.pull = {
|
|
72081
|
-
...drag.pull,
|
|
72082
|
-
[axis]: pulled
|
|
72083
|
-
};
|
|
72084
|
-
drag.progress = progress;
|
|
72085
|
-
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
72086
|
-
paintDrag();
|
|
72087
|
-
},
|
|
72088
|
-
// The slide being brought in walked whole and the hand still going, or a
|
|
72089
|
-
// travel caught on its way thrown on: either way the hand asks for the
|
|
72090
|
-
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
72091
|
-
// for that — the slide reached becomes the one dragged, the one the hand
|
|
72092
|
-
// came from stays next to it for a hand that turns around, and the slide
|
|
72093
|
-
// past it is put on the other side.
|
|
72094
|
-
onEdge: ({
|
|
72095
|
-
axis,
|
|
72096
|
-
sign
|
|
72097
|
-
}) => {
|
|
72098
|
-
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
72099
|
-
const {
|
|
72100
|
-
slideElements,
|
|
72101
|
-
placeOf
|
|
72102
|
-
} = readMap();
|
|
72103
|
-
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
72104
|
-
if (!reachedElement) {
|
|
72105
|
-
return false;
|
|
72106
|
-
}
|
|
72107
|
-
// The gate goToArea reads at the release, asked of the slide the hand
|
|
72108
|
-
// is now in: a slide holding on to the user is not walked through.
|
|
72109
|
-
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
72110
|
-
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
72111
|
-
if (!past) {
|
|
72112
|
-
return false;
|
|
72113
|
-
}
|
|
72114
|
-
// Two slides from the one at rest, so possibly never rendered: asked
|
|
72115
|
-
// for now, and built before the frame that brings it in is painted.
|
|
72116
|
-
renderNear(past, {
|
|
72117
|
-
alongside: false
|
|
72118
|
-
});
|
|
72119
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
72120
|
-
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
72121
|
-
x: 0,
|
|
72122
|
-
y: 0
|
|
72123
|
-
};
|
|
72124
|
-
drag.areaBack = sign > 0 ? past : drag.area;
|
|
72125
|
-
drag.areaOn = sign > 0 ? drag.area : past;
|
|
72126
|
-
drag.area = reached;
|
|
72127
|
-
drag.basePlace = basePlace;
|
|
72128
|
-
drag.baseOffset = {
|
|
72129
|
-
x: -basePlace.x * drag.box.width,
|
|
72130
|
-
y: -basePlace.y * drag.box.height
|
|
72131
|
-
};
|
|
72132
|
-
stageDrag(drag, readArea(currentElement));
|
|
72133
|
-
return {
|
|
72134
|
-
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
72135
|
-
travelBack: Boolean(drag.areaBack),
|
|
72136
|
-
travelOn: Boolean(drag.areaOn)
|
|
72137
|
-
};
|
|
72138
|
-
},
|
|
72139
|
-
onEnd: ({
|
|
72140
|
-
axis,
|
|
72141
|
-
sign,
|
|
72142
|
-
travels,
|
|
72143
|
-
event
|
|
72144
|
-
}) => {
|
|
72145
|
-
dragRef.current = null;
|
|
72146
|
-
// Where the gesture leaves the slides: on the one it was pulling in
|
|
72147
|
-
// when it says so, back on the one it was dragging otherwise.
|
|
72148
|
-
const destination = travels ? sign > 0 ? drag.areaBack : drag.areaOn : drag.area;
|
|
72149
|
-
const {
|
|
72150
|
-
slideElements,
|
|
72151
|
-
placeOf
|
|
72152
|
-
} = readMap();
|
|
72153
|
-
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
72154
|
-
const currentArea = currentElement && readArea(currentElement);
|
|
72155
|
-
if (!destination || destination === currentArea) {
|
|
72156
|
-
// Nothing to ask of the state: the slide it names is already the one
|
|
72157
|
-
// shown — a drag let go of short of a travel, a travel caught in
|
|
72158
|
-
// flight and let carry on. The track goes there on its own.
|
|
72159
|
-
settleTowards(drag, destination || drag.area);
|
|
72160
|
-
return;
|
|
72161
|
-
}
|
|
72162
|
-
// Where the slide is being left, for the travel to depart from instead
|
|
72163
|
-
// of from the map.
|
|
72164
|
-
travelFromRef.current = drag.offset;
|
|
72165
|
-
// …and where the indicator is being left, said about the slide that is
|
|
72166
|
-
// ARRIVING: a box short of the slide being pulled in, right where the
|
|
72167
|
-
// finger left it when going back to the slide being dragged.
|
|
72168
|
-
travelProgressFromRef.current = travels ? drag.progress - sign : drag.progress;
|
|
72169
|
-
// Which way the travel goes, read off where the two slides stand
|
|
72170
|
-
// rather than off the pull: a travel caught in flight and put back
|
|
72171
|
-
// goes the way it came, whatever the finger did meanwhile.
|
|
72172
|
-
const stage = stageRef.current;
|
|
72173
|
-
const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
|
|
72174
|
-
x: 0,
|
|
72175
|
-
y: 0
|
|
72176
|
-
};
|
|
72177
|
-
const towards = Math.sign(placeOfArea(destination)[axis] - placeOfArea(currentArea)[axis]);
|
|
72178
|
-
const moved = goToArea(destination, {
|
|
72179
|
-
forward: towards > 0,
|
|
72180
|
-
event,
|
|
72181
|
-
dx: axis === "x" ? towards : 0,
|
|
72182
|
-
dy: axis === "y" ? towards : 0
|
|
72183
|
-
});
|
|
72184
|
-
if (!moved) {
|
|
72185
|
-
// Nowhere to go after all — a slide holding on to the user
|
|
72186
|
-
// (preventNav), or a caller that refused the change.
|
|
72187
|
-
travelFromRef.current = null;
|
|
72188
|
-
travelProgressFromRef.current = null;
|
|
72189
|
-
settleTowards(drag, currentArea);
|
|
72190
|
-
return;
|
|
72191
|
-
}
|
|
72192
|
-
// A container whose `current` is held outside and was not moved: no
|
|
72193
|
-
// render moved the track, so the hand-off is still waiting and the
|
|
72194
|
-
// track still under where the finger left it. One frame is all it
|
|
72195
|
-
// takes to know.
|
|
72196
|
-
requestAnimationFrame(() => {
|
|
72197
|
-
if (travelFromRef.current) {
|
|
72198
|
-
travelFromRef.current = null;
|
|
72199
|
-
travelProgressFromRef.current = null;
|
|
72200
|
-
settleTowards(drag, currentArea);
|
|
72201
|
-
}
|
|
72202
|
-
});
|
|
72203
|
-
},
|
|
72204
|
-
onGiveUp: () => {
|
|
72205
|
-
dragRef.current = null;
|
|
72206
|
-
// A press that never became a gesture: what it stopped goes on its way,
|
|
72207
|
-
// from where the finger caught it and over what is left of the travel.
|
|
72208
|
-
if (!caughtTravel) {
|
|
72209
|
-
return;
|
|
72210
|
-
}
|
|
72211
|
-
const {
|
|
72212
|
-
trackElement: trackCaught,
|
|
72213
|
-
offsetOnScreen,
|
|
72214
|
-
offsetTarget,
|
|
72215
|
-
onScreenPx: caughtOnScreenPx,
|
|
72216
|
-
from,
|
|
72217
|
-
to
|
|
72218
|
-
} = caughtTravel;
|
|
72219
|
-
caughtTravel = null;
|
|
72220
|
-
movingRef.current = {
|
|
72221
|
-
from,
|
|
72222
|
-
to
|
|
72223
|
-
};
|
|
72224
|
-
if (offsetOnScreen === offsetTarget) {
|
|
72225
|
-
settleTravel();
|
|
72226
|
-
return;
|
|
72227
|
-
}
|
|
72228
|
-
const durationMs = durationToMs(duration);
|
|
72229
|
-
// What is LEFT of it, at the pace it had: a travel caught nine tenths
|
|
72230
|
-
// of the way there and let go of does not start its duration again.
|
|
72231
|
-
const boxRect = trackCaught.getBoundingClientRect();
|
|
72232
|
-
const targetPx = offsetToPx(offsetTarget, boxRect);
|
|
72233
|
-
const leftToCover = Math.abs(drag.axis === "y" ? targetPx.y - caughtOnScreenPx.y : targetPx.x - caughtOnScreenPx.x);
|
|
72234
|
-
const size = drag.axis === "y" ? boxRect.height : boxRect.width;
|
|
72235
|
-
const travelRatio = size ? leftToCover / size : 1;
|
|
72236
|
-
trackCaught.style.setProperty("--slide-container-offset", offsetTarget);
|
|
72237
|
-
trackAnimationRef.current = trackCaught.animate([{
|
|
72238
|
-
translate: offsetOnScreen
|
|
72239
|
-
}, {
|
|
72240
|
-
translate: offsetTarget
|
|
72241
|
-
}], {
|
|
72242
|
-
duration: durationMs * travelRatio,
|
|
72243
|
-
easing: "ease-out"
|
|
72244
|
-
});
|
|
72245
|
-
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
72246
|
-
// taken over by a travel asked for since
|
|
72247
|
-
});
|
|
72248
|
-
}
|
|
72249
|
-
};
|
|
72250
|
-
};
|
|
72251
|
-
|
|
72252
|
-
// Whether a gesture may begin at all: one travel at a time, and a window
|
|
72253
|
-
// mid-roll has nothing to drag yet — it is on its way somewhere and the
|
|
72254
|
-
// content that goes with it has not moved (see goToArea).
|
|
72255
|
-
const canStartTravel = () => dragAxes && !dragRef.current && !rollingRef.current;
|
|
72256
|
-
const startDrag = pointerDownEvent => {
|
|
72257
|
-
if (!canStartTravel()) {
|
|
72258
|
-
return;
|
|
72259
|
-
}
|
|
72260
|
-
const travelInFlight = readTravelInFlight();
|
|
72261
|
-
// Left playing when the hand can take it: the press is then far more
|
|
72262
|
-
// likely the next swipe than a catch, and stopping the travel under it is
|
|
72263
|
-
// what that swipe feels as a stall — the gesture listens to the hand first
|
|
72264
|
-
// and says which it was (see inFlight in drag_to_travel.js). On an axis the
|
|
72265
|
-
// hand cannot drag, it is caught at the press, as a travel under a finger
|
|
72266
|
-
// that starts something else.
|
|
72267
|
-
const leftPlaying = Boolean(travelInFlight && dragAxes.includes(travelInFlight.axis));
|
|
72268
|
-
const caughtTravel = travelInFlight && !leftPlaying ? catchTravelInFlight() : null;
|
|
72269
|
-
const handlers = createTravelHandlers(caughtTravel);
|
|
72270
|
-
const gesture = startDragToTravel(pointerDownEvent, {
|
|
72271
|
-
element: containerRef.current,
|
|
72272
|
-
axes: dragAxes,
|
|
72273
|
-
inFlight: leftPlaying ? travelInFlight : false,
|
|
72274
|
-
// The next swipe over a travel still playing: one slide further, and the
|
|
72275
|
-
// travel goes on there from where it is — the way a second arrow press
|
|
72276
|
-
// sends it — without ever having stopped under the hand.
|
|
72277
|
-
onPushOn: ({
|
|
72278
|
-
axis,
|
|
72279
|
-
sign,
|
|
72280
|
-
event
|
|
72281
|
-
}) => {
|
|
72282
|
-
if (trackAnimationRef.current?.playState !== "running") {
|
|
72283
|
-
return false;
|
|
72284
|
-
}
|
|
72285
|
-
// Nothing is held: the box is free for the travel that follows.
|
|
72286
|
-
dragRef.current = null;
|
|
72287
|
-
if (axis === "x") {
|
|
72288
|
-
move(-sign, 0, event);
|
|
72289
|
-
} else {
|
|
72290
|
-
move(0, -sign, event);
|
|
72291
|
-
}
|
|
72292
|
-
return true;
|
|
72293
|
-
},
|
|
72294
|
-
...handlers
|
|
72295
|
-
});
|
|
72296
|
-
if (!gesture) {
|
|
72297
|
-
// Not a press this box can be about — something that reads the pointer
|
|
72298
|
-
// itself, a box below it that travels the same way. Whatever the press
|
|
72299
|
-
// stopped on its way in goes back on its way.
|
|
72300
|
-
handlers.onGiveUp();
|
|
72301
|
-
return;
|
|
72302
|
-
}
|
|
72303
|
-
handlers.drag.gesture = gesture;
|
|
72304
|
-
dragRef.current = handlers.drag;
|
|
72305
|
-
};
|
|
72306
|
-
|
|
72307
|
-
// A wheel pushing the box sideways asks for a SLIDE, not for a place between
|
|
72308
|
-
// two: one push, one slide — the same thing an arrow key asks for, and it
|
|
72309
|
-
// plays at its own pace rather than under a hand (see watchWheelTravel).
|
|
72310
|
-
// Watched for the whole life of the box because such a gesture has no press
|
|
72311
|
-
// to start it: it begins with its first event.
|
|
72312
|
-
//
|
|
72313
|
-
// Reached through a ref and never rebuilt for it: a travel CHANGES which
|
|
72314
|
-
// slide is current, so a watcher listening on that would be torn down halfway
|
|
72315
|
-
// through the very gesture moving it.
|
|
72316
|
-
const travelOneStepRef = useRef(null);
|
|
72317
|
-
travelOneStepRef.current = ({
|
|
72318
|
-
axis,
|
|
72319
|
-
sign,
|
|
72320
|
-
event
|
|
72321
|
-
}) => {
|
|
72322
|
-
if (dragRef.current) {
|
|
72323
|
-
// A hand is holding the slides. They are its until it lets go.
|
|
72324
|
-
return;
|
|
72325
|
-
}
|
|
72326
|
-
if (axis === "x") {
|
|
72327
|
-
move(-sign, 0, event);
|
|
72328
|
-
} else {
|
|
72329
|
-
move(0, -sign, event);
|
|
72330
|
-
}
|
|
72331
|
-
};
|
|
72332
|
-
useLayoutEffect(() => {
|
|
72333
|
-
if (!scrollAxes) {
|
|
72334
|
-
return undefined;
|
|
72335
|
-
}
|
|
72336
|
-
return watchWheelTravel(containerRef.current, {
|
|
72337
|
-
axes: scrollAxes,
|
|
72338
|
-
onStep: detail => travelOneStepRef.current(detail)
|
|
72339
|
-
});
|
|
72340
|
-
}, [scrollAxes]);
|
|
72341
|
-
|
|
72342
|
-
// A gesture is listening on things that outlive this component.
|
|
72343
71261
|
useLayoutEffect(() => {
|
|
72344
71262
|
return () => {
|
|
72345
|
-
dragRef.current?.gesture?.stop();
|
|
72346
|
-
dragRef.current = null;
|
|
72347
71263
|
cancelTravelProgressAnimation();
|
|
72348
71264
|
};
|
|
72349
71265
|
}, []);
|
|
@@ -72472,31 +71388,22 @@ const SlideContainer = ({
|
|
|
72472
71388
|
// names, and to write the place this box stands on (see `signal`).
|
|
72473
71389
|
,
|
|
72474
71390
|
"data-slide-shares-state": sharesState ? "" : undefined
|
|
72475
|
-
// Which axes
|
|
72476
|
-
//
|
|
72477
|
-
// has seen the gesture — and it is also what a box HOLDING this one reads
|
|
72478
|
-
// to know the gesture is not its own (see drag_to_travel.js).
|
|
72479
|
-
,
|
|
72480
|
-
"data-travel-by-drag": dragAxes ?? undefined
|
|
72481
|
-
// …and said as a listener too, from the same render: a touch this box may
|
|
72482
|
-
// take has to be refusable before the finger lands, and being registered
|
|
72483
|
-
// is the whole of it — see keepTouchRefusable for why a JSX prop is
|
|
72484
|
-
// enough here (an element-level touchmove listener is non-passive).
|
|
72485
|
-
// Without it, a swipe the box has taken can be cancelled mid-gesture by
|
|
72486
|
-
// the browser scrolling the page along the axis touch-action leaves it.
|
|
72487
|
-
,
|
|
72488
|
-
onTouchMove: dragAxes ? keepTouchRefusable : undefined
|
|
72489
|
-
// The same fact for a wheel, and only for that second reason: this box
|
|
72490
|
-
// takes the push, whatever the box around it also travels on.
|
|
71391
|
+
// Which axes the row scrolls on under a hand, and which of them a finger may
|
|
71392
|
+
// not take (see the css above).
|
|
72491
71393
|
,
|
|
72492
|
-
"data-
|
|
72493
|
-
|
|
72494
|
-
//
|
|
72495
|
-
//
|
|
71394
|
+
"data-slide-hand": travelAxes ?? undefined,
|
|
71395
|
+
"data-slide-no-touch": Array.from(scrollAxes ?? "").filter(axis => !dragAxes?.includes(axis)).join("") || undefined
|
|
71396
|
+
// Read by what stands around the box: a press landing here may become a
|
|
71397
|
+
// scroll, so a control acting on the press waits for its click, and a box
|
|
71398
|
+
// around that a finger pushes leaves this axis alone (see drag_to_travel.js).
|
|
72496
71399
|
,
|
|
72497
|
-
"data-drag
|
|
71400
|
+
"data-travel-by-drag": dragAxes ?? undefined,
|
|
72498
71401
|
onPointerDown: e => {
|
|
72499
|
-
|
|
71402
|
+
// A finger on a travel still playing: it is the row's from here, and the
|
|
71403
|
+
// row is where the travel is going.
|
|
71404
|
+
if (e.pointerType !== "mouse") {
|
|
71405
|
+
trackAnimationRef.current?.finish();
|
|
71406
|
+
}
|
|
72500
71407
|
rest.onPointerDown?.(e);
|
|
72501
71408
|
}
|
|
72502
71409
|
// The focusable one, and a Tab stop: a slide is not (see Slide), so the
|
|
@@ -74173,7 +73080,7 @@ const css$H = /* css */`@property --x-popup-group-top {
|
|
|
74173
73080
|
transition: none;
|
|
74174
73081
|
}
|
|
74175
73082
|
|
|
74176
|
-
:
|
|
73083
|
+
:root[data-navi-route-transition] .navi_popup_group:modal {
|
|
74177
73084
|
view-transition-class: navi_popup;
|
|
74178
73085
|
}
|
|
74179
73086
|
|
|
@@ -74181,10 +73088,6 @@ const css$H = /* css */`@property --x-popup-group-top {
|
|
|
74181
73088
|
:root[data-navi-route-transition]:not([data-navi-route-transition-target="area"]) .navi_popup_group:modal {
|
|
74182
73089
|
view-transition-name: match-element;
|
|
74183
73090
|
}
|
|
74184
|
-
|
|
74185
|
-
:root[data-navi-route-travel] .navi_popup_group:modal {
|
|
74186
|
-
view-transition-name: match-element;
|
|
74187
|
-
}
|
|
74188
73091
|
}
|
|
74189
73092
|
`;
|
|
74190
73093
|
|
|
@@ -76142,8 +75045,7 @@ const popupCss = /* css */ `
|
|
|
76142
75045
|
}
|
|
76143
75046
|
}
|
|
76144
75047
|
|
|
76145
|
-
/* While the PAGES are the ones moving
|
|
76146
|
-
an open popup takes a picture of its own. The pictures of such a movement
|
|
75048
|
+
/* 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
|
|
76147
75049
|
are drawn in the top layer, above everything the document paints, and a
|
|
76148
75050
|
popup lives in the top layer too: uncaptured, it is simply covered for the
|
|
76149
75051
|
length of the movement, so it disappears the instant its page starts to
|
|
@@ -76153,9 +75055,7 @@ const popupCss = /* css */ `
|
|
|
76153
75055
|
The class is how navi reaches that picture afterwards: its clock below,
|
|
76154
75056
|
and its place above the pages (the z-order in nav/route_transition.jsx). */
|
|
76155
75057
|
:root[data-navi-route-transition] .navi_popover[aria-expanded="true"],
|
|
76156
|
-
:root[data-navi-route-transition] .navi_dialog[aria-expanded="true"]
|
|
76157
|
-
:root[data-navi-route-travel] .navi_popover[aria-expanded="true"],
|
|
76158
|
-
:root[data-navi-route-travel] .navi_dialog[aria-expanded="true"] {
|
|
75058
|
+
:root[data-navi-route-transition] .navi_dialog[aria-expanded="true"] {
|
|
76159
75059
|
view-transition-class: navi_popup;
|
|
76160
75060
|
}
|
|
76161
75061
|
|
|
@@ -76179,9 +75079,7 @@ const popupCss = /* css */ `
|
|
|
76179
75079
|
:root[data-navi-route-transition]:not(
|
|
76180
75080
|
[data-navi-route-transition-target="area"]
|
|
76181
75081
|
)
|
|
76182
|
-
.navi_dialog[aria-expanded="true"]
|
|
76183
|
-
:root[data-navi-route-travel] .navi_popover[aria-expanded="true"],
|
|
76184
|
-
:root[data-navi-route-travel] .navi_dialog[aria-expanded="true"] {
|
|
75082
|
+
.navi_dialog[aria-expanded="true"] {
|
|
76185
75083
|
view-transition-name: match-element;
|
|
76186
75084
|
}
|
|
76187
75085
|
}
|
|
@@ -76193,13 +75091,192 @@ const popupCss = /* css */ `
|
|
|
76193
75091
|
:root[data-navi-route-transition]::view-transition-new(.navi_popup) {
|
|
76194
75092
|
animation-duration: var(--navi-route-transition-duration, 300ms);
|
|
76195
75093
|
}
|
|
76196
|
-
:root[data-navi-route-travel]::view-transition-group(.navi_popup),
|
|
76197
|
-
:root[data-navi-route-travel]::view-transition-old(.navi_popup),
|
|
76198
|
-
:root[data-navi-route-travel]::view-transition-new(.navi_popup) {
|
|
76199
|
-
animation-duration: var(--navi-route-travel-duration, 300ms);
|
|
76200
|
-
}
|
|
76201
75094
|
`;
|
|
76202
75095
|
|
|
75096
|
+
/**
|
|
75097
|
+
* A modal dialog covered by another modal dialog is not in use, so it does not
|
|
75098
|
+
* answer the keyboard: a sheet raising one leaves what is behind it where it
|
|
75099
|
+
* is, under the keyboard like anything else behind that sheet.
|
|
75100
|
+
*
|
|
75101
|
+
* Two things make an open dialog follow the keyboard, and both are held here
|
|
75102
|
+
* for as long as it is covered:
|
|
75103
|
+
* - its ceiling (--dialog-maxmax-height), which CSS keeps tracking
|
|
75104
|
+
* --navi-app-height. Written back resolved, inline. `freezeSize` cannot do it:
|
|
75105
|
+
* a max-* wins over the width/height it writes;
|
|
75106
|
+
* - its placement, which reads the live viewport. Deferred, see
|
|
75107
|
+
* deferPlacementWhileCovered.
|
|
75108
|
+
*
|
|
75109
|
+
* A dialog back on top answers the screen as it is, at once, with one exception:
|
|
75110
|
+
* a keyboard on its way out. It is still counted for a while after the sheet
|
|
75111
|
+
* closes (the browser takes its time, then --navi-vvh waits for the resize to
|
|
75112
|
+
* settle, see responsive.js), and answering that makes the dialog jump up and
|
|
75113
|
+
* back down. A keyboard belongs to a focused field, so the focus says which case
|
|
75114
|
+
* this is: on a field the keyboard stays, anywhere else it is leaving and the
|
|
75115
|
+
* dialog waits for the viewport to change. No timer: how long a keyboard takes
|
|
75116
|
+
* to leave is unknown, and a duration would also delay a keyboard that stays.
|
|
75117
|
+
*
|
|
75118
|
+
* Only modal dialogs stack here. What sits under a non-modal one is still
|
|
75119
|
+
* usable, and keeps following the keyboard.
|
|
75120
|
+
*/
|
|
75121
|
+
|
|
75122
|
+
|
|
75123
|
+
const CEILING = "--dialog-maxmax-height";
|
|
75124
|
+
|
|
75125
|
+
const modalStack = [];
|
|
75126
|
+
|
|
75127
|
+
/**
|
|
75128
|
+
* @param {HTMLDialogElement} dialogEl - The modal dialog being shown.
|
|
75129
|
+
* @param {object} params
|
|
75130
|
+
* @param {() => void} params.place - Places the dialog against the viewport as
|
|
75131
|
+
* it is. Called on a dialog that was held once it answers the viewport again.
|
|
75132
|
+
* @returns {() => void} To call when the dialog closes.
|
|
75133
|
+
*/
|
|
75134
|
+
const enterModalStack = (dialogEl, { place }) => {
|
|
75135
|
+
const entry = { dialogEl, place, hold: null };
|
|
75136
|
+
const entryBelow = modalStack.at(-1);
|
|
75137
|
+
if (entryBelow) {
|
|
75138
|
+
holdEntry(entryBelow);
|
|
75139
|
+
}
|
|
75140
|
+
modalStack.push(entry);
|
|
75141
|
+
return () => {
|
|
75142
|
+
const index = modalStack.indexOf(entry);
|
|
75143
|
+
const wasOnTop = index === modalStack.length - 1;
|
|
75144
|
+
modalStack.splice(index, 1);
|
|
75145
|
+
dropHold(entry);
|
|
75146
|
+
if (wasOnTop && modalStack.length > 0) {
|
|
75147
|
+
releaseBackOnTop(modalStack.at(-1), { closedDialogEl: dialogEl });
|
|
75148
|
+
}
|
|
75149
|
+
};
|
|
75150
|
+
};
|
|
75151
|
+
|
|
75152
|
+
/**
|
|
75153
|
+
* Answers true when the dialog must not be placed right now, and remembers to
|
|
75154
|
+
* place it on release.
|
|
75155
|
+
*
|
|
75156
|
+
* Only while the room differs from the one it was covered with: the placement
|
|
75157
|
+
* reads the live viewport whatever triggered it, so with the same room it lands
|
|
75158
|
+
* where the dialog already is, and content growing under the sheet is answered
|
|
75159
|
+
* as usual.
|
|
75160
|
+
*/
|
|
75161
|
+
const deferPlacementWhileCovered = (dialogEl) => {
|
|
75162
|
+
const entry = modalStack.find((candidate) => candidate.dialogEl === dialogEl);
|
|
75163
|
+
if (!entry || !entry.hold) {
|
|
75164
|
+
return false;
|
|
75165
|
+
}
|
|
75166
|
+
if (readRoom() === entry.hold.room) {
|
|
75167
|
+
return false;
|
|
75168
|
+
}
|
|
75169
|
+
entry.hold.placementOwed = true;
|
|
75170
|
+
return true;
|
|
75171
|
+
};
|
|
75172
|
+
|
|
75173
|
+
const holdEntry = (entry) => {
|
|
75174
|
+
if (entry.hold) {
|
|
75175
|
+
// Covered again before the keyboard had left: the ceiling it holds is still
|
|
75176
|
+
// the one it had before all of this.
|
|
75177
|
+
entry.hold.stopWaiting?.();
|
|
75178
|
+
entry.hold.stopWaiting = null;
|
|
75179
|
+
return;
|
|
75180
|
+
}
|
|
75181
|
+
const { dialogEl, place } = entry;
|
|
75182
|
+
holdCeiling(dialogEl);
|
|
75183
|
+
entry.hold = {
|
|
75184
|
+
room: readRoom(),
|
|
75185
|
+
placementOwed: false,
|
|
75186
|
+
stopWaiting: null,
|
|
75187
|
+
// The keyboard does not fire this one (see safe_area.md): this is the
|
|
75188
|
+
// window itself changing, a rotation, which a covered dialog has to fit.
|
|
75189
|
+
stopWatchingWindow: subscribeWindowResizeSettled(() => {
|
|
75190
|
+
holdCeiling(dialogEl);
|
|
75191
|
+
entry.hold.room = readRoom();
|
|
75192
|
+
entry.hold.placementOwed = false;
|
|
75193
|
+
place();
|
|
75194
|
+
}),
|
|
75195
|
+
};
|
|
75196
|
+
};
|
|
75197
|
+
|
|
75198
|
+
const releaseBackOnTop = (entry, { closedDialogEl }) => {
|
|
75199
|
+
const { hold } = entry;
|
|
75200
|
+
if (!hold) {
|
|
75201
|
+
return;
|
|
75202
|
+
}
|
|
75203
|
+
// Nothing taken away, or a keyboard that stays: the room read here is the
|
|
75204
|
+
// screen as it is. A field in the dialog that closed is one still leaving: a
|
|
75205
|
+
// dialog leaving the tree only gives the focus back after the commit.
|
|
75206
|
+
const { activeElement } = document;
|
|
75207
|
+
if (
|
|
75208
|
+
readRoom() >= hold.room ||
|
|
75209
|
+
(isEditableTarget(activeElement) && !closedDialogEl.contains(activeElement))
|
|
75210
|
+
) {
|
|
75211
|
+
release(entry);
|
|
75212
|
+
return;
|
|
75213
|
+
}
|
|
75214
|
+
// Any change, not the room it was covered with: the bars of a mobile browser
|
|
75215
|
+
// may come back different, and the wait would never end.
|
|
75216
|
+
const roomWithKeyboardLeaving = readRoom();
|
|
75217
|
+
const releaseIfViewportChanged = () => {
|
|
75218
|
+
if (readRoom() !== roomWithKeyboardLeaving) {
|
|
75219
|
+
release(entry);
|
|
75220
|
+
}
|
|
75221
|
+
};
|
|
75222
|
+
// The focus can come back after the close (a popup leaving the tree gives it
|
|
75223
|
+
// back once the commit is over), and the keyboard then stays after all.
|
|
75224
|
+
const releaseIfFieldFocused = () => {
|
|
75225
|
+
if (isEditableTarget(document.activeElement)) {
|
|
75226
|
+
release(entry);
|
|
75227
|
+
}
|
|
75228
|
+
};
|
|
75229
|
+
document.addEventListener("focusin", releaseIfFieldFocused, {
|
|
75230
|
+
capture: true,
|
|
75231
|
+
});
|
|
75232
|
+
const unsubscribes = [
|
|
75233
|
+
visualViewportHeightSignal.subscribe(releaseIfViewportChanged),
|
|
75234
|
+
subscribeVirtualKeyboardGeometryChange(releaseIfViewportChanged),
|
|
75235
|
+
];
|
|
75236
|
+
hold.stopWaiting = () => {
|
|
75237
|
+
document.removeEventListener("focusin", releaseIfFieldFocused, {
|
|
75238
|
+
capture: true,
|
|
75239
|
+
});
|
|
75240
|
+
for (const unsubscribe of unsubscribes) {
|
|
75241
|
+
unsubscribe();
|
|
75242
|
+
}
|
|
75243
|
+
};
|
|
75244
|
+
};
|
|
75245
|
+
|
|
75246
|
+
const release = (entry) => {
|
|
75247
|
+
const { placementOwed } = entry.hold;
|
|
75248
|
+
dropHold(entry);
|
|
75249
|
+
if (placementOwed) {
|
|
75250
|
+
entry.place();
|
|
75251
|
+
}
|
|
75252
|
+
};
|
|
75253
|
+
|
|
75254
|
+
const dropHold = (entry) => {
|
|
75255
|
+
const { hold, dialogEl } = entry;
|
|
75256
|
+
if (!hold) {
|
|
75257
|
+
return;
|
|
75258
|
+
}
|
|
75259
|
+
hold.stopWaiting?.();
|
|
75260
|
+
hold.stopWatchingWindow();
|
|
75261
|
+
dialogEl.style.removeProperty(CEILING);
|
|
75262
|
+
entry.hold = null;
|
|
75263
|
+
};
|
|
75264
|
+
|
|
75265
|
+
// The declared ceiling rather than the used max-height: it is the one cap that
|
|
75266
|
+
// follows the keyboard, and the other caps (maxHeight) keep their say. Its
|
|
75267
|
+
// computed value has every var() resolved, so it stops following once written
|
|
75268
|
+
// back. Registered with inherits: false, it does not reach a dialog nested
|
|
75269
|
+
// inside this one.
|
|
75270
|
+
const holdCeiling = (dialogEl) => {
|
|
75271
|
+
dialogEl.style.removeProperty(CEILING);
|
|
75272
|
+
const ceiling = getComputedStyle(dialogEl).getPropertyValue(CEILING);
|
|
75273
|
+
dialogEl.style.setProperty(CEILING, ceiling);
|
|
75274
|
+
};
|
|
75275
|
+
|
|
75276
|
+
// An overlaying keyboard leaves the viewport alone and arrives on its own.
|
|
75277
|
+
const readRoom = () =>
|
|
75278
|
+
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
75279
|
+
|
|
76203
75280
|
/**
|
|
76204
75281
|
* Pushing a popup docked to an edge (a side panel, a bottom sheet) back the way
|
|
76205
75282
|
* it came in closes it: it follows the finger while it travels, and letting go
|
|
@@ -78606,6 +77683,9 @@ const useDialogProps = props => {
|
|
|
78606
77683
|
const rectEffect = visibleRectEffect(positionedAncestor, (visibleRect, {
|
|
78607
77684
|
event
|
|
78608
77685
|
}) => {
|
|
77686
|
+
if (deferPlacementWhileCovered(dialogEl)) {
|
|
77687
|
+
return;
|
|
77688
|
+
}
|
|
78609
77689
|
positionDialog(event);
|
|
78610
77690
|
// Only for what can have taken height away from the dialog — a
|
|
78611
77691
|
// scroll never does, and re-scrolling on one would fight the finger
|
|
@@ -78625,6 +77705,17 @@ const useDialogProps = props => {
|
|
|
78625
77705
|
freezeSize(dialogEl);
|
|
78626
77706
|
}
|
|
78627
77707
|
rectEffect.observeSize(dialogEl);
|
|
77708
|
+
if (isModal) {
|
|
77709
|
+
addCleanup(enterModalStack(dialogEl, {
|
|
77710
|
+
// "resize": what it was held from is a viewport change, and it answers
|
|
77711
|
+
// the way it answers one, animated. Called directly because the
|
|
77712
|
+
// effect's dedup already saw that viewport go by.
|
|
77713
|
+
place: () => {
|
|
77714
|
+
positionDialog(new CustomEvent("resize"));
|
|
77715
|
+
keepFocusedElementVisible(dialogEl);
|
|
77716
|
+
}
|
|
77717
|
+
}));
|
|
77718
|
+
}
|
|
78628
77719
|
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
78629
77720
|
// says so, after the placement read it: placed again on the spot, where
|
|
78630
77721
|
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
@@ -82418,6 +81509,13 @@ const PickerCustom = props => {
|
|
|
82418
81509
|
};
|
|
82419
81510
|
},
|
|
82420
81511
|
keyDown: e => {
|
|
81512
|
+
// Pressed in a popup this picker holds: the key is that popup's, and
|
|
81513
|
+
// `false` keeps it from the default reaction too, which would refuse
|
|
81514
|
+
// it on a read-only picker and take a Space from the button it was
|
|
81515
|
+
// pressed on.
|
|
81516
|
+
if (isPressOnLayerOver(e.target, e.currentTarget)) {
|
|
81517
|
+
return false;
|
|
81518
|
+
}
|
|
82421
81519
|
// A shortcut that applies has been through the gate already, under
|
|
82422
81520
|
// its own name and intent: the key is answered. Handed back as the
|
|
82423
81521
|
// reaction, its boolean would be asked again, unnamed — as a write,
|
|
@@ -85927,7 +85025,7 @@ const css$w = /* css */`@property --list-group-label-height {
|
|
|
85927
85025
|
}
|
|
85928
85026
|
|
|
85929
85027
|
@supports (view-transition-group: contain) {
|
|
85930
|
-
:root:not([data-navi-route-transition]
|
|
85028
|
+
:root:not([data-navi-route-transition]) .navi_list_container[data-item-transition] {
|
|
85931
85029
|
view-transition-name: match-element;
|
|
85932
85030
|
view-transition-class: navi_list_transition;
|
|
85933
85031
|
view-transition-group: contain;
|
|
@@ -87501,8 +86599,8 @@ const useListScrollSync = ({
|
|
|
87501
86599
|
return undefined;
|
|
87502
86600
|
}
|
|
87503
86601
|
return routePage.leavingSignal.subscribe(leaving => {
|
|
86602
|
+
pageLeftRef.current = leaving;
|
|
87504
86603
|
if (leaving) {
|
|
87505
|
-
pageLeftRef.current = true;
|
|
87506
86604
|
letGo();
|
|
87507
86605
|
}
|
|
87508
86606
|
});
|
|
@@ -93413,7 +92511,7 @@ const Color = ({
|
|
|
93413
92511
|
const color = children || undefined;
|
|
93414
92512
|
return jsx(Box, {
|
|
93415
92513
|
as: "span",
|
|
93416
|
-
|
|
92514
|
+
baseClassName: "navi_color",
|
|
93417
92515
|
"navi-color-empty": color ? undefined : ""
|
|
93418
92516
|
// propsCSSVars={COLOR_PROP_CSS_VAR}
|
|
93419
92517
|
,
|
|
@@ -98164,7 +97262,7 @@ const InputDuration = props => {
|
|
|
98164
97262
|
delete basePseudoState[":-navi-loading"];
|
|
98165
97263
|
return jsxs(Box, {
|
|
98166
97264
|
ref: groupRef,
|
|
98167
|
-
|
|
97265
|
+
baseClassName: "navi_input_duration",
|
|
98168
97266
|
"data-callout-arrow-x": "center",
|
|
98169
97267
|
width: "fit-content",
|
|
98170
97268
|
flex: true,
|
|
@@ -98678,8 +97776,7 @@ const Select = ({
|
|
|
98678
97776
|
as: "span",
|
|
98679
97777
|
inline: true,
|
|
98680
97778
|
flex: true,
|
|
98681
|
-
baseClassName: "navi_input",
|
|
98682
|
-
className: "navi_select",
|
|
97779
|
+
baseClassName: "navi_input navi_select",
|
|
98683
97780
|
...rootProps,
|
|
98684
97781
|
basePseudoState: basePseudoState,
|
|
98685
97782
|
styleCSSVars: InputStyleCSSVars,
|
|
@@ -106603,7 +105700,7 @@ const ButtonCopyToClipboard = ({
|
|
|
106603
105700
|
import.meta.css = [css$b, "@jsenv/navi/src/control/input/button_copy_to_clipboard.jsx"];
|
|
106604
105701
|
const [copied, copy] = useCopyToClipboard(children);
|
|
106605
105702
|
return jsxs(Box, {
|
|
106606
|
-
|
|
105703
|
+
baseClassName: "navi_clipboard_container",
|
|
106607
105704
|
...props,
|
|
106608
105705
|
children: [jsx(Box, {
|
|
106609
105706
|
className: "navi_copied_notif",
|
|
@@ -108883,7 +107980,7 @@ const css = /* css */`.navi_side_panel {
|
|
|
108883
107980
|
--dialog-maxmax-width: var(--navi-app-width);
|
|
108884
107981
|
|
|
108885
107982
|
&[navi-side="left"], &[navi-side="right"] {
|
|
108886
|
-
height: var(--navi-side-panel-height, var(--
|
|
107983
|
+
height: var(--navi-side-panel-height, var(--dialog-maxmax-height));
|
|
108887
107984
|
}
|
|
108888
107985
|
|
|
108889
107986
|
&[navi-side="top"], &[navi-side="bottom"] {
|
|
@@ -109195,7 +108292,7 @@ const SidePanelHead = props => jsx(Box, {
|
|
|
109195
108292
|
* @param {import("preact").ComponentChildren} props.children
|
|
109196
108293
|
*/
|
|
109197
108294
|
const SidePanelFoot = props => jsx(Box, {
|
|
109198
|
-
|
|
108295
|
+
baseClassName: "navi_side_panel_foot",
|
|
109199
108296
|
...props
|
|
109200
108297
|
});
|
|
109201
108298
|
SidePanel.Head = SidePanelHead;
|