@jsenv/navi 0.29.44 → 0.29.46
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/jsenv_navi.js +1070 -783
- package/dist/jsenv_navi.js.map +49 -33
- package/dist/jsenv_navi_side_effects.js +111 -3
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +12 -2
- package/docs/actions.md +22 -0
- package/docs/control_value.md +6 -0
- package/docs/create_and_edit.md +468 -0
- package/docs/css_architecture.md +66 -0
- package/docs/form_changed.md +52 -0
- package/docs/navigation.md +15 -0
- package/docs/scroll.md +14 -5
- package/docs/z_index.md +21 -8
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
|
-
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal,
|
|
5
|
+
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
+
export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
7
|
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
7
8
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
8
9
|
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
@@ -49,17 +50,28 @@ const css$10 = /* css */`
|
|
|
49
50
|
--navi-z-index-control-focused: 2;
|
|
50
51
|
|
|
51
52
|
/* Kept stuck while something scrolls under it: a list header, the head
|
|
52
|
-
and foot of a side panel, a table's sticky cells
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sticky part
|
|
61
|
-
it
|
|
62
|
-
|
|
53
|
+
and foot of a side panel, a table's sticky cells, the header and
|
|
54
|
+
footer of any scrolling Box. Above raised controls — a control
|
|
55
|
+
scrolling past must go under the header that pins the column it
|
|
56
|
+
belongs to, never over it.
|
|
57
|
+
|
|
58
|
+
A sticky element is a positioned one, so it already wins against
|
|
59
|
+
everything in the flow — the band is what it takes to also win against
|
|
60
|
+
what the page positioned itself, which loses to DOM order otherwise
|
|
61
|
+
(a sticky part is written before what scrolls under it). Box applies
|
|
62
|
+
it by default, isolated, and lets a call site write auto back:
|
|
63
|
+
--box-header-z-index / --box-footer-z-index.
|
|
64
|
+
|
|
65
|
+
"While stuck" is the condition the name states, and it costs something
|
|
66
|
+
to ignore: a sticky part at rest is a block in the flow with nothing
|
|
67
|
+
passing under it, and the band there is what slices whatever a
|
|
68
|
+
neighbouring row lets out of its box. CSS cannot express the
|
|
69
|
+
condition — an element cannot read its own stuck state — so it takes
|
|
70
|
+
measuring, which List does (it marks its parts with a navi-stuck
|
|
71
|
+
attribute against its own scroller and applies the band only there,
|
|
72
|
+
see --list-*-z-index in list.jsx) and Box does not: a generic
|
|
73
|
+
scrolling area does not know what it was given to scroll, and dropping
|
|
74
|
+
to auto there loses to a single position: relative. */
|
|
63
75
|
--navi-z-index-sticky: 10;
|
|
64
76
|
|
|
65
77
|
/* Pinned to the viewport, over the whole page: FixedBar. A decade of its
|
|
@@ -17237,15 +17249,26 @@ const isSizeSpacingKey = (key) => {
|
|
|
17237
17249
|
// "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
|
|
17238
17250
|
// shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
|
|
17239
17251
|
// they are what a popup meant to stay clear of the keyboard should use.
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
const
|
|
17252
|
+
// "appw"/"apph" are the same thing narrowed to the app's own screen: identical
|
|
17253
|
+
// to vvw/vvh until the app declares --navi-app-max-width, and a share of that
|
|
17254
|
+
// width afterwards. A gap meant to read as "a small margin" must use these —
|
|
17255
|
+
// 3vvw on a 1500px window is a 45px gap around a 600px app.
|
|
17256
|
+
// Functions rather than the signals themselves: appw/apph are not a signal to
|
|
17257
|
+
// read but a value to compute (a signal, then a CSS var read back). Reading the
|
|
17258
|
+
// signal inside still registers the same dependency for a caller doing this
|
|
17259
|
+
// during a render.
|
|
17260
|
+
const VIEWPORT_UNIT_VALUES = {
|
|
17261
|
+
appw: getAppWidth,
|
|
17262
|
+
apph: getAppHeight,
|
|
17263
|
+
vvw: () => visualViewportWidthSignal.value,
|
|
17264
|
+
vvh: () => visualViewportHeightSignal.value,
|
|
17265
|
+
vw: () => windowWidthSignal.value,
|
|
17266
|
+
vh: () => windowHeightSignal.value,
|
|
17267
|
+
dvw: () => windowWidthSignal.value,
|
|
17268
|
+
dvh: () => windowHeightSignal.value,
|
|
17269
|
+
};
|
|
17270
|
+
const VIEWPORT_LENGTH_REGEX =
|
|
17271
|
+
/^(-?\d+(?:\.\d+)?)(appw|apph|vvw|vvh|dvw|dvh|vw|vh)$/;
|
|
17249
17272
|
const resolveViewportLength = (size) => {
|
|
17250
17273
|
if (typeof size !== "string") {
|
|
17251
17274
|
return null;
|
|
@@ -17255,7 +17278,7 @@ const resolveViewportLength = (size) => {
|
|
|
17255
17278
|
return null;
|
|
17256
17279
|
}
|
|
17257
17280
|
const [, amount, unit] = match;
|
|
17258
|
-
return (parseFloat(amount) / 100) *
|
|
17281
|
+
return (parseFloat(amount) / 100) * VIEWPORT_UNIT_VALUES[unit]();
|
|
17259
17282
|
};
|
|
17260
17283
|
|
|
17261
17284
|
// "3cqw"/"2cqh" — a share of the container the given element lives in, the way
|
|
@@ -19019,14 +19042,24 @@ import.meta.css = [/* css */`
|
|
|
19019
19042
|
|
|
19020
19043
|
[data-scrollable] {
|
|
19021
19044
|
overflow: var(--x-scrollable-overflow, auto);
|
|
19045
|
+
--box-header-z-index: var(--navi-z-index-sticky);
|
|
19046
|
+
--box-footer-z-index: var(--navi-z-index-sticky);
|
|
19047
|
+
/* The band stays inside this box: without a stacking context here, "in
|
|
19048
|
+
front of my body" would be read as "in front of everything on the page"
|
|
19049
|
+
and a header would reach past a bar or a popup — which is exactly what
|
|
19050
|
+
the decades in navi_z_indexes.js are there to prevent. See
|
|
19051
|
+
docs/z_index.md. */
|
|
19052
|
+
isolation: isolate;
|
|
19022
19053
|
|
|
19023
19054
|
&[data-scrollable-overflow="scroll"] {
|
|
19024
19055
|
--x-scrollable-overflow: scroll;
|
|
19025
19056
|
}
|
|
19026
19057
|
|
|
19027
|
-
/* box-shadow
|
|
19028
|
-
|
|
19029
|
-
|
|
19058
|
+
/* A real border and not a box-shadow: a shadow is drawn outside the box, so
|
|
19059
|
+
it lands on top of whatever comes next in the painting order and loses to
|
|
19060
|
+
it — a body painting its own background over the line that was meant to
|
|
19061
|
+
separate them. The border belongs to the part itself and is always
|
|
19062
|
+
visible; the pixel it adds shifts nothing, these parts never shrink. */
|
|
19030
19063
|
/* The corners are the container's, not the part's: a header sitting at the
|
|
19031
19064
|
top of a rounded box has to follow that curve or it paints square over
|
|
19032
19065
|
it (a dark header in a rounded popup is where this shows). inherit and
|
|
@@ -19034,18 +19067,18 @@ import.meta.css = [/* css */`
|
|
|
19034
19067
|
> [data-header] {
|
|
19035
19068
|
position: sticky;
|
|
19036
19069
|
top: 0;
|
|
19037
|
-
z-index:
|
|
19070
|
+
z-index: var(--box-header-z-index);
|
|
19071
|
+
border-bottom: 1px solid var(--navi-separator-color-default);
|
|
19038
19072
|
border-top-left-radius: inherit;
|
|
19039
19073
|
border-top-right-radius: inherit;
|
|
19040
|
-
box-shadow: 0 1px 0 var(--navi-separator-color-default);
|
|
19041
19074
|
}
|
|
19042
19075
|
> [data-footer] {
|
|
19043
19076
|
position: sticky;
|
|
19044
19077
|
bottom: 0;
|
|
19045
|
-
z-index:
|
|
19078
|
+
z-index: var(--box-footer-z-index);
|
|
19079
|
+
border-top: 1px solid var(--navi-separator-color-default);
|
|
19046
19080
|
border-bottom-right-radius: inherit;
|
|
19047
19081
|
border-bottom-left-radius: inherit;
|
|
19048
|
-
box-shadow: 0 -1px 0 var(--navi-separator-color-default);
|
|
19049
19082
|
}
|
|
19050
19083
|
|
|
19051
19084
|
&:has(> [data-body]) {
|
|
@@ -19059,8 +19092,11 @@ import.meta.css = [/* css */`
|
|
|
19059
19092
|
|
|
19060
19093
|
> [data-header],
|
|
19061
19094
|
> [data-footer] {
|
|
19095
|
+
/* Nothing scrolls under them here — the body does that, next to them —
|
|
19096
|
+
so they are back to being blocks in the flow, and stacking is not
|
|
19097
|
+
their business anymore. */
|
|
19062
19098
|
position: static;
|
|
19063
|
-
z-index:
|
|
19099
|
+
z-index: auto;
|
|
19064
19100
|
flex-shrink: 0;
|
|
19065
19101
|
}
|
|
19066
19102
|
|
|
@@ -21546,6 +21582,564 @@ document.body.addEventListener(
|
|
|
21546
21582
|
{ capture: true },
|
|
21547
21583
|
);
|
|
21548
21584
|
|
|
21585
|
+
const documentStateSignal = signal(null);
|
|
21586
|
+
const useDocumentState = () => {
|
|
21587
|
+
return documentStateSignal.value;
|
|
21588
|
+
};
|
|
21589
|
+
const updateDocumentState = (value) => {
|
|
21590
|
+
documentStateSignal.value = value;
|
|
21591
|
+
};
|
|
21592
|
+
|
|
21593
|
+
/**
|
|
21594
|
+
* A navigation is ABOUT to be applied — said before its very first write.
|
|
21595
|
+
*
|
|
21596
|
+
* Everything else a router says arrives once the change is made: a route
|
|
21597
|
+
* announces that it matches, an action that it is running. That is too late for
|
|
21598
|
+
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
21599
|
+
* transitions are exactly that kind of reader — the picture they keep of the
|
|
21600
|
+
* page being left is taken at the next frame, and a render answering a signal
|
|
21601
|
+
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
21602
|
+
*
|
|
21603
|
+
* So this is the one moment where nothing has moved yet. It is published
|
|
21604
|
+
* synchronously, from the top of the navigation, and whoever listens runs
|
|
21605
|
+
* before the URL, the visited set, or any route has changed.
|
|
21606
|
+
*
|
|
21607
|
+
* The other end is published too, and for the same kind of reader: whoever
|
|
21608
|
+
* held something across the change and has nobody to hand it to gets a moment
|
|
21609
|
+
* to let go of it that does not depend on guessing how long the change takes.
|
|
21610
|
+
*/
|
|
21611
|
+
|
|
21612
|
+
|
|
21613
|
+
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
21614
|
+
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
21615
|
+
|
|
21616
|
+
const setupBrowserIntegrationViaHistory = ({
|
|
21617
|
+
applyActions,
|
|
21618
|
+
applyRouting,
|
|
21619
|
+
isRouting,
|
|
21620
|
+
}) => {
|
|
21621
|
+
const { history } = window;
|
|
21622
|
+
|
|
21623
|
+
let globalAbortController = new AbortController();
|
|
21624
|
+
const triggerGlobalAbort = (reason) => {
|
|
21625
|
+
globalAbortController.abort(reason);
|
|
21626
|
+
globalAbortController = new AbortController();
|
|
21627
|
+
};
|
|
21628
|
+
|
|
21629
|
+
const dispatchActions = (params) => {
|
|
21630
|
+
const { requestedResult } = applyActions({
|
|
21631
|
+
globalAbortSignal: globalAbortController.signal,
|
|
21632
|
+
abortSignal: new AbortController().signal,
|
|
21633
|
+
...params,
|
|
21634
|
+
});
|
|
21635
|
+
return requestedResult;
|
|
21636
|
+
};
|
|
21637
|
+
setActionDispatcher(dispatchActions);
|
|
21638
|
+
|
|
21639
|
+
const getDocumentState = () => {
|
|
21640
|
+
return window.history.state ? { ...window.history.state } : null;
|
|
21641
|
+
};
|
|
21642
|
+
|
|
21643
|
+
const historyStartAtStart = getDocumentState();
|
|
21644
|
+
const visitedUrlSet = historyStartAtStart
|
|
21645
|
+
? new Set(historyStartAtStart.jsenv_visited_urls || [])
|
|
21646
|
+
: new Set();
|
|
21647
|
+
|
|
21648
|
+
// Create a signal that tracks visited URLs for reactive updates
|
|
21649
|
+
// Using a counter instead of the Set directly for better performance
|
|
21650
|
+
// Links will check isVisited() when this signal changes
|
|
21651
|
+
const visitedUrlsSignal = signal(0);
|
|
21652
|
+
|
|
21653
|
+
const isVisited = (url) => {
|
|
21654
|
+
url = new URL(url, window.location.href).href;
|
|
21655
|
+
return visitedUrlSet.has(url);
|
|
21656
|
+
};
|
|
21657
|
+
const markUrlAsVisited = (url) => {
|
|
21658
|
+
if (visitedUrlSet.has(url)) {
|
|
21659
|
+
return;
|
|
21660
|
+
}
|
|
21661
|
+
visitedUrlSet.add(url);
|
|
21662
|
+
visitedUrlsSignal.value++;
|
|
21663
|
+
};
|
|
21664
|
+
|
|
21665
|
+
let abortController = null;
|
|
21666
|
+
const handleRoutingTask = (url, options) => {
|
|
21667
|
+
// Before anything is written: the visited set, the URL and every route are
|
|
21668
|
+
// about to change, and this is the last moment the page still stands as it
|
|
21669
|
+
// was. And after, whichever way the change went out — so that whoever took
|
|
21670
|
+
// something at the first announcement has a definite place to give it back.
|
|
21671
|
+
publishBeforeRouting({ url, ...options });
|
|
21672
|
+
try {
|
|
21673
|
+
return applyRoutingTask(url, options);
|
|
21674
|
+
} finally {
|
|
21675
|
+
publishAfterRouting({ url, ...options });
|
|
21676
|
+
}
|
|
21677
|
+
};
|
|
21678
|
+
|
|
21679
|
+
const applyRoutingTask = (url, options) => {
|
|
21680
|
+
const isSameUrl = url === window.location.href;
|
|
21681
|
+
const {
|
|
21682
|
+
reason,
|
|
21683
|
+
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
21684
|
+
state,
|
|
21685
|
+
} = options;
|
|
21686
|
+
|
|
21687
|
+
if (navigationType === "push" || navigationType === "replace") {
|
|
21688
|
+
markUrlAsVisited(url);
|
|
21689
|
+
// undefined → inherit current state (link click, neutral navigation)
|
|
21690
|
+
// null → explicit reset (no nav-state keys carried over)
|
|
21691
|
+
// {...} → explicit state from enter()/leave(), already built from currentState
|
|
21692
|
+
// When state is given it's responsability of the caller to ensure it inherits document state (or not, you want it 99% of the time)
|
|
21693
|
+
let effectiveState;
|
|
21694
|
+
const sharedState = {
|
|
21695
|
+
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
21696
|
+
};
|
|
21697
|
+
if (state === undefined) {
|
|
21698
|
+
effectiveState = {
|
|
21699
|
+
...(getDocumentState() || {}),
|
|
21700
|
+
...sharedState,
|
|
21701
|
+
};
|
|
21702
|
+
} else if (state === null) {
|
|
21703
|
+
effectiveState = sharedState;
|
|
21704
|
+
} else if (state) {
|
|
21705
|
+
effectiveState = {
|
|
21706
|
+
...state,
|
|
21707
|
+
...sharedState,
|
|
21708
|
+
};
|
|
21709
|
+
}
|
|
21710
|
+
if (navigationType === "push") {
|
|
21711
|
+
window.history.pushState(effectiveState, null, url);
|
|
21712
|
+
} else {
|
|
21713
|
+
window.history.replaceState(effectiveState, null, url);
|
|
21714
|
+
}
|
|
21715
|
+
updateDocumentUrl(url);
|
|
21716
|
+
updateDocumentState(effectiveState);
|
|
21717
|
+
} else {
|
|
21718
|
+
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
21719
|
+
markUrlAsVisited(url);
|
|
21720
|
+
updateDocumentUrl(url);
|
|
21721
|
+
updateDocumentState(state);
|
|
21722
|
+
}
|
|
21723
|
+
|
|
21724
|
+
// Skip route matching for state-only changes: push/replace to the same URL
|
|
21725
|
+
// (e.g. useNavState updating document state without changing the route).
|
|
21726
|
+
// Do NOT apply for "traverse" — window.location.href is already updated by
|
|
21727
|
+
// the browser before the popstate handler runs, so isSameUrl is always true
|
|
21728
|
+
// for back/forward navigation regardless of whether the URL actually changed.
|
|
21729
|
+
if (
|
|
21730
|
+
isSameUrl &&
|
|
21731
|
+
(navigationType === "push" || navigationType === "replace")
|
|
21732
|
+
) {
|
|
21733
|
+
return undefined;
|
|
21734
|
+
}
|
|
21735
|
+
|
|
21736
|
+
if (abortController) {
|
|
21737
|
+
abortController.abort(`navigating to ${url}`);
|
|
21738
|
+
}
|
|
21739
|
+
abortController = new AbortController();
|
|
21740
|
+
const abortSignal = abortController.signal;
|
|
21741
|
+
const { allResult, requestedResult } = applyRouting(url, {
|
|
21742
|
+
globalAbortSignal: globalAbortController.signal,
|
|
21743
|
+
abortSignal,
|
|
21744
|
+
reason,
|
|
21745
|
+
navigationType,
|
|
21746
|
+
isVisited,
|
|
21747
|
+
state,
|
|
21748
|
+
});
|
|
21749
|
+
executeWithCleanup(
|
|
21750
|
+
() => allResult,
|
|
21751
|
+
() => {
|
|
21752
|
+
abortController = undefined;
|
|
21753
|
+
},
|
|
21754
|
+
);
|
|
21755
|
+
return requestedResult;
|
|
21756
|
+
};
|
|
21757
|
+
|
|
21758
|
+
// Browser event handlers
|
|
21759
|
+
window.addEventListener(
|
|
21760
|
+
"click",
|
|
21761
|
+
(e) => {
|
|
21762
|
+
if (e.button !== 0) {
|
|
21763
|
+
// Ignore non-left clicks
|
|
21764
|
+
return;
|
|
21765
|
+
}
|
|
21766
|
+
if (e.metaKey) {
|
|
21767
|
+
// Ignore clicks with meta key (e.g. open in new tab)
|
|
21768
|
+
return;
|
|
21769
|
+
}
|
|
21770
|
+
if (e.defaultPrevented) {
|
|
21771
|
+
return;
|
|
21772
|
+
}
|
|
21773
|
+
const linkElement = e.target.closest("a");
|
|
21774
|
+
if (!linkElement) {
|
|
21775
|
+
return;
|
|
21776
|
+
}
|
|
21777
|
+
if (linkElement.hasAttribute("data-readonly")) {
|
|
21778
|
+
return;
|
|
21779
|
+
}
|
|
21780
|
+
const href = linkElement.href;
|
|
21781
|
+
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
21782
|
+
getHrefTargetInfo(href);
|
|
21783
|
+
if (isEmpty || !isSameOrigin) {
|
|
21784
|
+
// Let link to other origins be handled by the browser
|
|
21785
|
+
return;
|
|
21786
|
+
}
|
|
21787
|
+
if (isAnchor) {
|
|
21788
|
+
// Fragment navigation belongs to the browser: it owns the indicated
|
|
21789
|
+
// part of the document, and taking it over would cost `:target` and the
|
|
21790
|
+
// focus handling that come with it.
|
|
21791
|
+
if (isCurrent) {
|
|
21792
|
+
// Except this one, which the browser answers with a scroll and
|
|
21793
|
+
// nothing else: same pathname, same hash, so no event and no url
|
|
21794
|
+
// change reaches whoever is waiting on the designated element.
|
|
21795
|
+
rearmUrlTarget();
|
|
21796
|
+
}
|
|
21797
|
+
return;
|
|
21798
|
+
}
|
|
21799
|
+
// Nothing here declared a route, so there is nothing to route to: the
|
|
21800
|
+
// page is a plain document and a link in it is a plain link. Taking it
|
|
21801
|
+
// over anyway would push the url and then have nothing to show for it —
|
|
21802
|
+
// the address bar moves and the page does not (see applyRouting's own
|
|
21803
|
+
// "not called yet" branch, which is where that used to end up).
|
|
21804
|
+
if (!isRouting()) {
|
|
21805
|
+
return;
|
|
21806
|
+
}
|
|
21807
|
+
e.preventDefault();
|
|
21808
|
+
handleRoutingTask(href, {
|
|
21809
|
+
reason: `"click" on a[href="${href}"]`,
|
|
21810
|
+
navigationType: "push",
|
|
21811
|
+
});
|
|
21812
|
+
},
|
|
21813
|
+
{ capture: true },
|
|
21814
|
+
);
|
|
21815
|
+
|
|
21816
|
+
window.addEventListener(
|
|
21817
|
+
"submit",
|
|
21818
|
+
() => {
|
|
21819
|
+
// Handle form submissions?
|
|
21820
|
+
// Not needed yet
|
|
21821
|
+
},
|
|
21822
|
+
{ capture: true },
|
|
21823
|
+
);
|
|
21824
|
+
|
|
21825
|
+
window.addEventListener("popstate", (popstateEvent) => {
|
|
21826
|
+
const url = window.location.href;
|
|
21827
|
+
const state = popstateEvent.state;
|
|
21828
|
+
handleRoutingTask(url, {
|
|
21829
|
+
reason: `"popstate" event for ${url}`,
|
|
21830
|
+
navigationType: "traverse",
|
|
21831
|
+
state,
|
|
21832
|
+
});
|
|
21833
|
+
});
|
|
21834
|
+
|
|
21835
|
+
// A fragment navigation is left to the browser (see the click handler above):
|
|
21836
|
+
// it owns the indicated part of the document, and taking it over would cost
|
|
21837
|
+
// `:target` and the focus handling that come with it. The document url still
|
|
21838
|
+
// has to follow it — nothing else here would notice that it moved.
|
|
21839
|
+
window.addEventListener("hashchange", () => {
|
|
21840
|
+
updateDocumentUrl(window.location.href);
|
|
21841
|
+
});
|
|
21842
|
+
|
|
21843
|
+
const navTo = async (url, { replace, state } = {}) => {
|
|
21844
|
+
handleRoutingTask(url, {
|
|
21845
|
+
reason: `navTo called with "${url}"`,
|
|
21846
|
+
navigationType: replace ? "replace" : "push",
|
|
21847
|
+
state,
|
|
21848
|
+
});
|
|
21849
|
+
};
|
|
21850
|
+
|
|
21851
|
+
const stop = (reason = "stop called") => {
|
|
21852
|
+
triggerGlobalAbort(reason);
|
|
21853
|
+
};
|
|
21854
|
+
|
|
21855
|
+
const reload = () => {
|
|
21856
|
+
const url = window.location.href;
|
|
21857
|
+
const state = history.state;
|
|
21858
|
+
handleRoutingTask(url, {
|
|
21859
|
+
reason: "reload called",
|
|
21860
|
+
navigationType: "reload",
|
|
21861
|
+
state,
|
|
21862
|
+
});
|
|
21863
|
+
};
|
|
21864
|
+
|
|
21865
|
+
const navBack = () => {
|
|
21866
|
+
window.history.back();
|
|
21867
|
+
};
|
|
21868
|
+
|
|
21869
|
+
const navForward = () => {
|
|
21870
|
+
window.history.forward();
|
|
21871
|
+
};
|
|
21872
|
+
|
|
21873
|
+
const init = () => {
|
|
21874
|
+
const url = window.location.href;
|
|
21875
|
+
const state = history.state;
|
|
21876
|
+
handleRoutingTask(url, {
|
|
21877
|
+
reason: "routing initialization",
|
|
21878
|
+
navigationType: "load",
|
|
21879
|
+
state,
|
|
21880
|
+
});
|
|
21881
|
+
};
|
|
21882
|
+
|
|
21883
|
+
return {
|
|
21884
|
+
integration: "browser_history_api",
|
|
21885
|
+
init,
|
|
21886
|
+
navTo,
|
|
21887
|
+
stop,
|
|
21888
|
+
reload,
|
|
21889
|
+
navBack,
|
|
21890
|
+
navForward,
|
|
21891
|
+
getDocumentState,
|
|
21892
|
+
isVisited,
|
|
21893
|
+
visitedUrlsSignal,
|
|
21894
|
+
};
|
|
21895
|
+
};
|
|
21896
|
+
|
|
21897
|
+
let updateRoutes;
|
|
21898
|
+
|
|
21899
|
+
const applyActions = (params) => {
|
|
21900
|
+
const updateActionsResult = updateActions(params);
|
|
21901
|
+
const { allResult, runningActionSet } = updateActionsResult;
|
|
21902
|
+
const pendingTaskNameArray = [];
|
|
21903
|
+
for (const runningAction of runningActionSet) {
|
|
21904
|
+
pendingTaskNameArray.push(runningAction.name);
|
|
21905
|
+
}
|
|
21906
|
+
workingWhile(() => allResult, pendingTaskNameArray);
|
|
21907
|
+
return updateActionsResult;
|
|
21908
|
+
};
|
|
21909
|
+
const applyRouting = (
|
|
21910
|
+
url,
|
|
21911
|
+
{
|
|
21912
|
+
globalAbortSignal,
|
|
21913
|
+
abortSignal,
|
|
21914
|
+
// state
|
|
21915
|
+
navigationType,
|
|
21916
|
+
isVisited,
|
|
21917
|
+
reason,
|
|
21918
|
+
},
|
|
21919
|
+
) => {
|
|
21920
|
+
if (!updateRoutes) {
|
|
21921
|
+
// .init() not called yet
|
|
21922
|
+
// likely because code does not uses routing at all
|
|
21923
|
+
return {};
|
|
21924
|
+
}
|
|
21925
|
+
const {
|
|
21926
|
+
loadSet,
|
|
21927
|
+
reloadSet,
|
|
21928
|
+
abortSignalMap,
|
|
21929
|
+
routeLoadRequestedMap,
|
|
21930
|
+
activeRouteSet,
|
|
21931
|
+
} = updateRoutes(url, {
|
|
21932
|
+
navigationType,
|
|
21933
|
+
isVisited,
|
|
21934
|
+
// state,
|
|
21935
|
+
});
|
|
21936
|
+
if (
|
|
21937
|
+
(!loadSet || loadSet.size === 0) &&
|
|
21938
|
+
(!reloadSet || reloadSet.size === 0)
|
|
21939
|
+
) {
|
|
21940
|
+
return {
|
|
21941
|
+
allResult: undefined,
|
|
21942
|
+
requestedResult: undefined,
|
|
21943
|
+
activeRouteSet: new Set(),
|
|
21944
|
+
};
|
|
21945
|
+
}
|
|
21946
|
+
const updateActionsResult = updateActions({
|
|
21947
|
+
globalAbortSignal,
|
|
21948
|
+
abortSignal,
|
|
21949
|
+
runSet: loadSet,
|
|
21950
|
+
rerunSet: reloadSet,
|
|
21951
|
+
abortSignalMap,
|
|
21952
|
+
reason,
|
|
21953
|
+
isReplace: navigationType === "replace",
|
|
21954
|
+
});
|
|
21955
|
+
const { allResult, runningActionSet } = updateActionsResult;
|
|
21956
|
+
const pendingTaskNameArray = [];
|
|
21957
|
+
for (const [route, routeAction] of routeLoadRequestedMap) {
|
|
21958
|
+
if (runningActionSet.has(routeAction)) {
|
|
21959
|
+
pendingTaskNameArray.push(`${route.relativeUrl} -> ${routeAction.name}`);
|
|
21960
|
+
}
|
|
21961
|
+
}
|
|
21962
|
+
routingWhile(() => allResult, pendingTaskNameArray);
|
|
21963
|
+
return { ...updateActionsResult, activeRouteSet };
|
|
21964
|
+
};
|
|
21965
|
+
|
|
21966
|
+
const browserIntegration = setupBrowserIntegrationViaHistory({
|
|
21967
|
+
applyActions,
|
|
21968
|
+
applyRouting,
|
|
21969
|
+
// Routes are declared by the consumer and registered through
|
|
21970
|
+
// setOnAllRouteReady below, so "does this document route at all?" is only
|
|
21971
|
+
// answerable once that has run — hence a function, read at click time rather
|
|
21972
|
+
// than a value read at setup time.
|
|
21973
|
+
isRouting: () => Boolean(updateRoutes),
|
|
21974
|
+
});
|
|
21975
|
+
|
|
21976
|
+
setOnAllRouteReady((v) => {
|
|
21977
|
+
updateRoutes = v;
|
|
21978
|
+
browserIntegration.init();
|
|
21979
|
+
});
|
|
21980
|
+
setRouteIntegration(browserIntegration);
|
|
21981
|
+
|
|
21982
|
+
const navIntegratedVia = browserIntegration.integration;
|
|
21983
|
+
const navTo = (target, options) => {
|
|
21984
|
+
const url = new URL(target, window.location.href).href;
|
|
21985
|
+
const currentUrl = documentUrlSignal.peek();
|
|
21986
|
+
if (url === currentUrl) {
|
|
21987
|
+
if (options?.state === undefined) {
|
|
21988
|
+
return null;
|
|
21989
|
+
}
|
|
21990
|
+
// State-only update on same URL: skip if state is identical to current.
|
|
21991
|
+
const currentState = browserIntegration.getDocumentState();
|
|
21992
|
+
if (compareTwoJsValues(options.state, currentState)) {
|
|
21993
|
+
return null;
|
|
21994
|
+
}
|
|
21995
|
+
}
|
|
21996
|
+
return browserIntegration.navTo(url, options);
|
|
21997
|
+
};
|
|
21998
|
+
const stopLoad = (reason = "stopLoad() called") => {
|
|
21999
|
+
const windowIsLoading = windowIsLoadingSignal.value;
|
|
22000
|
+
if (windowIsLoading) {
|
|
22001
|
+
window.stop();
|
|
22002
|
+
}
|
|
22003
|
+
const documentIsBusy = documentIsBusySignal.value;
|
|
22004
|
+
if (documentIsBusy) {
|
|
22005
|
+
browserIntegration.stop(reason);
|
|
22006
|
+
}
|
|
22007
|
+
};
|
|
22008
|
+
const reload = browserIntegration.reload;
|
|
22009
|
+
const navBack = browserIntegration.navBack;
|
|
22010
|
+
const navForward = browserIntegration.navForward;
|
|
22011
|
+
const isVisited = browserIntegration.isVisited;
|
|
22012
|
+
const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
|
|
22013
|
+
browserIntegration.handleActionTask;
|
|
22014
|
+
|
|
22015
|
+
// Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where
|
|
22016
|
+
// the mask is derived from render order within the nearest root/async
|
|
22017
|
+
// boundary — stable across re-renders of the *same* mount, but not across a
|
|
22018
|
+
// reload (render order can differ) or even across two mounts on the same
|
|
22019
|
+
// page (two components hitting useId() in the same relative order get the
|
|
22020
|
+
// same string). Storing one of these under type: "push" bakes it into a
|
|
22021
|
+
// history entry: reload the page and the entry's key may now belong to a
|
|
22022
|
+
// completely different component (or none), silently auto-opening whatever
|
|
22023
|
+
// happens to render at that same position instead.
|
|
22024
|
+
const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
|
|
22025
|
+
const isLikelyPreactGeneratedId = (id) => PREACT_GENERATED_ID_REGEX.test(id);
|
|
22026
|
+
|
|
22027
|
+
const NO_OP = () => {};
|
|
22028
|
+
const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
|
|
22029
|
+
const useNavStateBasic = (
|
|
22030
|
+
id,
|
|
22031
|
+
{ debug, type = "replace", onLeave, defaultValue } = {},
|
|
22032
|
+
) => {
|
|
22033
|
+
// Hooks must be called unconditionally — before the !id early return.
|
|
22034
|
+
const state = documentStateSignal.value;
|
|
22035
|
+
// Key presence is the flag — the value may be anything, including undefined.
|
|
22036
|
+
const keyInState = Boolean(id && state && Object.hasOwn(state, id));
|
|
22037
|
+
const onLeaveRef = useRef(onLeave);
|
|
22038
|
+
onLeaveRef.current = onLeave;
|
|
22039
|
+
const prevKeyInStateRef = useRef(keyInState);
|
|
22040
|
+
// enteredRef tracks whether enter() was called without a matching leave() yet.
|
|
22041
|
+
// It lets the effect distinguish an external disappearance (back button → fire onLeave)
|
|
22042
|
+
// from a programmatic one (leave() already set it to false before the state updates).
|
|
22043
|
+
const enteredRef = useRef(false);
|
|
22044
|
+
useEffect(() => {
|
|
22045
|
+
const prevKeyInState = prevKeyInStateRef.current;
|
|
22046
|
+
prevKeyInStateRef.current = keyInState;
|
|
22047
|
+
if (prevKeyInState && !keyInState && enteredRef.current) {
|
|
22048
|
+
enteredRef.current = false;
|
|
22049
|
+
onLeaveRef.current?.();
|
|
22050
|
+
}
|
|
22051
|
+
}, [keyInState]);
|
|
22052
|
+
|
|
22053
|
+
if (!id) {
|
|
22054
|
+
return NO_ID_GIVEN;
|
|
22055
|
+
}
|
|
22056
|
+
|
|
22057
|
+
let effectiveType = type;
|
|
22058
|
+
if (type === "push" && isLikelyPreactGeneratedId(id)) {
|
|
22059
|
+
effectiveType = "replace";
|
|
22060
|
+
}
|
|
22061
|
+
|
|
22062
|
+
const currentValue = keyInState ? state[id] : defaultValue;
|
|
22063
|
+
|
|
22064
|
+
if (debug) {
|
|
22065
|
+
console.debug(`useNavState(${id}) current value is ${currentValue}`);
|
|
22066
|
+
}
|
|
22067
|
+
|
|
22068
|
+
// enter(value): navigate TO this state (push or replace depending on type).
|
|
22069
|
+
// Calling enter() without a value stores "on" — the mere presence of the key
|
|
22070
|
+
// in the document state is enough to match; the value just allows associating
|
|
22071
|
+
// extra data with the entry when needed.
|
|
22072
|
+
const enter = (value = "on") => {
|
|
22073
|
+
enteredRef.current = true;
|
|
22074
|
+
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
22075
|
+
if (Object.hasOwn(currentStateCopy, id) && currentStateCopy[id] === value) {
|
|
22076
|
+
return;
|
|
22077
|
+
}
|
|
22078
|
+
currentStateCopy[id] = value;
|
|
22079
|
+
navTo(window.location.href, {
|
|
22080
|
+
replace: effectiveType !== "push",
|
|
22081
|
+
state: currentStateCopy,
|
|
22082
|
+
});
|
|
22083
|
+
};
|
|
22084
|
+
|
|
22085
|
+
// leave(): navigate AWAY FROM this state (navBack in push mode, replace in replace mode).
|
|
22086
|
+
// isBack: when true (cancel close in push mode), call history.back() to restore the
|
|
22087
|
+
// pre-open state — discards any in-progress edits.
|
|
22088
|
+
// When false (confirmed close), replace the pushed entry instead: preserves the
|
|
22089
|
+
// current URL state (e.g. a new picker value) while removing the popup key.
|
|
22090
|
+
const leave = ({ isBack } = {}) => {
|
|
22091
|
+
enteredRef.current = false;
|
|
22092
|
+
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
22093
|
+
if (!Object.hasOwn(currentStateCopy, id)) {
|
|
22094
|
+
return;
|
|
22095
|
+
}
|
|
22096
|
+
if (effectiveType === "push" && isBack) {
|
|
22097
|
+
browserIntegration.navBack();
|
|
22098
|
+
} else {
|
|
22099
|
+
delete currentStateCopy[id];
|
|
22100
|
+
navTo(window.location.href, {
|
|
22101
|
+
replace: true,
|
|
22102
|
+
state: currentStateCopy,
|
|
22103
|
+
});
|
|
22104
|
+
}
|
|
22105
|
+
};
|
|
22106
|
+
|
|
22107
|
+
return [currentValue, enter, leave];
|
|
22108
|
+
};
|
|
22109
|
+
|
|
22110
|
+
/**
|
|
22111
|
+
* Stores a named value in the browser's document state and returns it reactively.
|
|
22112
|
+
* The component re-renders whenever the value changes (navigation, back/forward button).
|
|
22113
|
+
*
|
|
22114
|
+
* @param {string} id
|
|
22115
|
+
* Unique key used to store the value in document state. Must be stable across renders.
|
|
22116
|
+
*
|
|
22117
|
+
* @param {object} [options]
|
|
22118
|
+
* @param {"push"|"replace"} [options.type="replace"]
|
|
22119
|
+
* Controls how enter() adds the state to browser history.
|
|
22120
|
+
* - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
|
|
22121
|
+
* - "replace": updates the current history entry — no extra history entry is created.
|
|
22122
|
+
* Silently downgraded to "replace" (with a dev-only console.warn) when `id`
|
|
22123
|
+
* looks auto-generated (e.g. preact's own useId()) — an unstable id baked
|
|
22124
|
+
* into a pushed history entry won't survive a reload correctly, and could
|
|
22125
|
+
* even collide with a different component's own auto-generated id. Pass a
|
|
22126
|
+
* stable, explicit id to actually get "push" behavior.
|
|
22127
|
+
* @param {() => void} [options.onLeave]
|
|
22128
|
+
* Called when the state key disappears **externally** — e.g. the user presses the browser
|
|
22129
|
+
* back button. Not called when leave() is invoked programmatically.
|
|
22130
|
+
* @param {*} [options.defaultValue]
|
|
22131
|
+
* Value returned when `id` is absent from document state. Defaults to `undefined`.
|
|
22132
|
+
*
|
|
22133
|
+
* @returns {[value, enter, leave]}
|
|
22134
|
+
* - `value`: current value from document state, or `defaultValue` when the key is absent.
|
|
22135
|
+
* - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
|
|
22136
|
+
* Calling without an argument stores `"on"` — the presence of the key is enough to match;
|
|
22137
|
+
* the value allows associating extra data when needed.
|
|
22138
|
+
* - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
|
|
22139
|
+
* or goes back in history when `type` is "push").
|
|
22140
|
+
*/
|
|
22141
|
+
const useNavState = useNavStateBasic;
|
|
22142
|
+
|
|
21549
22143
|
/**
|
|
21550
22144
|
* @param {Element} element The element asking — the command's source, and the
|
|
21551
22145
|
* anchor a popup opens on unless `anchor` says otherwise.
|
|
@@ -21953,15 +22547,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21953
22547
|
requester = firstButtonSubmitting;
|
|
21954
22548
|
}
|
|
21955
22549
|
}
|
|
21956
|
-
// Read here rather than above: it depends on the requester, which is only
|
|
21957
|
-
// known now — Enter in a field sends through the first submit button, and
|
|
21958
|
-
// what follows the send is that button's answer.
|
|
21959
|
-
const afterSend = resolveAfterSend(target, requester);
|
|
21960
22550
|
// Nothing is committed when a constraint fails, so nothing is decided
|
|
21961
22551
|
// and the popup must stay open — with the form still in front of the
|
|
21962
22552
|
// user, showing what it is waiting for.
|
|
21963
22553
|
let invalid = false;
|
|
22554
|
+
// What follows the send is read at the moment it runs, never before it:
|
|
22555
|
+
// it depends on the requester (Enter in a field sends through the first
|
|
22556
|
+
// submit button, and what follows is that button's answer), and on
|
|
22557
|
+
// anything the send itself decided — an action that learned where to go
|
|
22558
|
+
// from the response writes it on the form while it runs
|
|
22559
|
+
// (data-after-send), and this is what picks it up.
|
|
21964
22560
|
const runAfterSend = () => {
|
|
22561
|
+
const afterSend = resolveAfterSend(target, requester);
|
|
22562
|
+
if (!afterSend) {
|
|
22563
|
+
return;
|
|
22564
|
+
}
|
|
21965
22565
|
triggerNaviCommand(source, afterSend, event, { optional: true });
|
|
21966
22566
|
};
|
|
21967
22567
|
const {
|
|
@@ -21996,7 +22596,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21996
22596
|
requester,
|
|
21997
22597
|
}),
|
|
21998
22598
|
);
|
|
21999
|
-
if (sent === false || invalid
|
|
22599
|
+
if (sent === false || invalid) {
|
|
22000
22600
|
return sent;
|
|
22001
22601
|
}
|
|
22002
22602
|
if (isRunning) {
|
|
@@ -22188,6 +22788,30 @@ registerNaviCommand("--navi-back", (source, event) => {
|
|
|
22188
22788
|
};
|
|
22189
22789
|
});
|
|
22190
22790
|
|
|
22791
|
+
// Where a press takes the user. The destination is the command's argument
|
|
22792
|
+
// because it says WHAT the command does — "--navi-nav-to:/games/42" — which is how
|
|
22793
|
+
// it can also be what follows a form submission: the form has answered its
|
|
22794
|
+
// question, and the answer to "what now" is a page.
|
|
22795
|
+
//
|
|
22796
|
+
// A destination fixed at the call site, so it is for a page known before the
|
|
22797
|
+
// send — which is what a form needs, since it must also know where to go when
|
|
22798
|
+
// the press had nothing to send. A destination the response decides (a
|
|
22799
|
+
// creation, whose id comes back with it) is the action's own business: it
|
|
22800
|
+
// navigates itself.
|
|
22801
|
+
registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
|
|
22802
|
+
if (!argument) {
|
|
22803
|
+
console.warn(
|
|
22804
|
+
`[navi] "--navi-nav-to" needs a destination: --navi-nav-to:/the/url (relative to the current page, or absolute).`,
|
|
22805
|
+
);
|
|
22806
|
+
return undefined;
|
|
22807
|
+
}
|
|
22808
|
+
const target = resolveExplicitTarget(source) || source;
|
|
22809
|
+
return {
|
|
22810
|
+
target,
|
|
22811
|
+
implementation: () => navTo(argument),
|
|
22812
|
+
};
|
|
22813
|
+
});
|
|
22814
|
+
|
|
22191
22815
|
registerNaviCommand("--navi-toggle", (source, event, { anchor } = {}) => {
|
|
22192
22816
|
const target =
|
|
22193
22817
|
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
@@ -24272,6 +24896,7 @@ const useUIGroupStateController = (
|
|
|
24272
24896
|
pendingChangeRef.current = null;
|
|
24273
24897
|
const batchedEvent = new CustomEvent(
|
|
24274
24898
|
`${controlType}_batched_ui_state_update`,
|
|
24899
|
+
{ detail: {} },
|
|
24275
24900
|
);
|
|
24276
24901
|
chainEvent(batchedEvent, pendingChange.e);
|
|
24277
24902
|
scope._onChange(batchedEvent, {
|
|
@@ -24841,6 +25466,13 @@ const useControlProps = (props, {
|
|
|
24841
25466
|
const onButtonInteractionAllowed = e => {
|
|
24842
25467
|
triggerUIAction(e);
|
|
24843
25468
|
const control = ref.current;
|
|
25469
|
+
if (!control) {
|
|
25470
|
+
// What the button just did took the button away: a command that
|
|
25471
|
+
// navigates, a popup closing over it. There is no control left to
|
|
25472
|
+
// ask for an action, and nothing is lost by not asking — what the
|
|
25473
|
+
// press was for has already happened.
|
|
25474
|
+
return;
|
|
25475
|
+
}
|
|
24844
25476
|
tryActionAfterInteractionAllowed(control, {
|
|
24845
25477
|
event: e,
|
|
24846
25478
|
action: boundAction,
|
|
@@ -28218,15 +28850,26 @@ const css$V = /* css */`
|
|
|
28218
28850
|
|
|
28219
28851
|
Capping the *size* here rather than only offsetting the position is
|
|
28220
28852
|
what makes a centered dialog follow the mobile virtual keyboard for
|
|
28221
|
-
free: --navi-
|
|
28222
|
-
reflows the dialog itself as the keyboard opens.
|
|
28223
|
-
|
|
28224
|
-
|
|
28853
|
+
free: --navi-app-width/--navi-app-height track the visual viewport, so
|
|
28854
|
+
the browser reflows the dialog itself as the keyboard opens.
|
|
28855
|
+
|
|
28856
|
+
A share of the app's own screen, not of the window (hence
|
|
28857
|
+
--navi-app-width rather than 3vvw): the gap must read as a small
|
|
28858
|
+
margin around the dialog, and 3% of a 1500px window is a 45px gap
|
|
28859
|
+
around a 600px app. Identical to 3vvw until the app declares
|
|
28860
|
+
--navi-app-max-width. */
|
|
28861
|
+
--x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
|
|
28862
|
+
|
|
28863
|
+
/* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
|
|
28864
|
+
the app's own screen, which is the viewport unless the app declared a
|
|
28865
|
+
narrower one (see navi_css_vars.js). An app-width cap alone never
|
|
28866
|
+
costs the gap below — it is subtracted from whichever of the two ends
|
|
28867
|
+
up smaller. */
|
|
28225
28868
|
--dialog-maxmax-width: calc(
|
|
28226
|
-
var(--navi-
|
|
28869
|
+
var(--navi-app-width) - 2 * var(--x-dialog-container-spacing)
|
|
28227
28870
|
);
|
|
28228
28871
|
--dialog-maxmax-height: calc(
|
|
28229
|
-
var(--navi-
|
|
28872
|
+
var(--navi-app-height) - 2 * var(--x-dialog-container-spacing)
|
|
28230
28873
|
);
|
|
28231
28874
|
|
|
28232
28875
|
--dialog-border-radius: var(--navi-popup-border-radius);
|
|
@@ -28558,17 +29201,19 @@ const css$V = /* css */`
|
|
|
28558
29201
|
* shown via the non-modal `.show()` instead, staying in normal document
|
|
28559
29202
|
* flow inside its own positioned ancestor — confined to (and clipped by)
|
|
28560
29203
|
* that container instead of the whole viewport.
|
|
28561
|
-
* @param {boolean} [props.
|
|
28562
|
-
* (docked flush to the bottom edge, full width)
|
|
28563
|
-
* and leaves it alone otherwise. For a dialog meant to be interacted
|
|
28564
|
-
* rather than merely read:
|
|
28565
|
-
* the screen and a centered box ends up both cramped and out of thumb
|
|
28566
|
-
*
|
|
28567
|
-
*
|
|
28568
|
-
*
|
|
28569
|
-
*
|
|
28570
|
-
*
|
|
28571
|
-
*
|
|
29204
|
+
* @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
|
|
29205
|
+
* bottom sheet (docked flush to the bottom edge, full width) on a small touch
|
|
29206
|
+
* screen, and leaves it alone otherwise. For a dialog meant to be interacted
|
|
29207
|
+
* with rather than merely read: on a phone the keyboard owns the bottom of
|
|
29208
|
+
* the screen and a centered box ends up both cramped and out of thumb reach,
|
|
29209
|
+
* while under a mouse the centered box is already the right shape. Both
|
|
29210
|
+
* halves of the name matter (`smallTouchScreenSignal`): touch alone would
|
|
29211
|
+
* dock a big touch screen — a tablet, a kiosk panel — a whole screen away
|
|
29212
|
+
* from where the finger just tapped, and size alone would dock a narrow
|
|
29213
|
+
* desktop window, which is still a mouse. It supplies defaults for
|
|
29214
|
+
* `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
|
|
29215
|
+
* any of them can still be pinned explicitly. Re-resolves live as the pointer
|
|
29216
|
+
* type or the window size changes.
|
|
28572
29217
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
28573
29218
|
* within its container (the viewport for `layer="top"`, the positioned
|
|
28574
29219
|
* ancestor for `layer="local"`) — Dialog is never anchored to a real
|
|
@@ -28581,17 +29226,19 @@ const css$V = /* css */`
|
|
|
28581
29226
|
* `inset(top)`) for the overlapping variant.
|
|
28582
29227
|
* @param {boolean} [props.expand] - Shorthand for both `expandX` and `expandY`.
|
|
28583
29228
|
* @param {boolean} [props.expandX] - Stretches the dialog to the full width its
|
|
28584
|
-
* container allows (`--dialog-maxmax-width`). Set by
|
|
28585
|
-
* touch
|
|
29229
|
+
* container allows (`--dialog-maxmax-width`). Set by
|
|
29230
|
+
* `dockedOnSmallTouchScreen` on a small touch screen.
|
|
28586
29231
|
* @param {boolean} [props.expandY] - Same, vertically
|
|
28587
29232
|
* (`--dialog-maxmax-height`).
|
|
28588
|
-
* @param {string|number} [props.marginWithContainer="
|
|
29233
|
+
* @param {string|number} [props.marginWithContainer="3appw"] - Minimum gap kept
|
|
28589
29234
|
* between the dialog and the edges of its container, whatever its
|
|
28590
29235
|
* `positionArea`: it both caps the dialog's own size (via
|
|
28591
29236
|
* `--x-dialog-container-spacing`, written from this prop) and offsets a docked
|
|
28592
29237
|
* one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
|
|
28593
|
-
* number of pixels, or a viewport length — "
|
|
28594
|
-
* viewport,
|
|
29238
|
+
* number of pixels, or a viewport length — "appw"/"apph" being the app's own
|
|
29239
|
+
* screen (the visual viewport, or the narrower one the app declared with
|
|
29240
|
+
* --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
|
|
29241
|
+
* shrinks when the mobile keyboard opens. Pass 0 for a dialog
|
|
28595
29242
|
* meant to sit flush (a side panel).
|
|
28596
29243
|
* @param {"close"|"cancel"|"capture"|"none"} [props.pointerInteractionOutsideEffect="close"]
|
|
28597
29244
|
* - `"close"` closes the dialog on an outside click. `"capture"`/`"none"`
|
|
@@ -28610,7 +29257,7 @@ const css$V = /* css */`
|
|
|
28610
29257
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
28611
29258
|
* scroll while open (its backdrop only covers the scrollport, so scrolling
|
|
28612
29259
|
* there would reveal uncovered content); this prop extends the lock to the
|
|
28613
|
-
* whole page. Defaults to `true` for a dialog docked by `
|
|
29260
|
+
* whole page. Defaults to `true` for a dialog docked by `dockedOnSmallTouchScreen`.
|
|
28614
29261
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
|
|
28615
29262
|
* - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
|
|
28616
29263
|
* or a concrete `"slide-from-*"` direction otherwise. Any other explicit
|
|
@@ -28793,10 +29440,10 @@ const DialogLocal = props => {
|
|
|
28793
29440
|
* contentProps]` — `backdropProps` is `null` for the via-attribute renderer
|
|
28794
29441
|
* (its own backdrop is native, not a real element).
|
|
28795
29442
|
*/
|
|
28796
|
-
// What a dialog turns into
|
|
28797
|
-
// the dialog in the zone a
|
|
28798
|
-
//
|
|
28799
|
-
//
|
|
29443
|
+
// What a dialog turns into on a small touch screen. "bottom" is not a taste:
|
|
29444
|
+
// it puts the dialog in the zone a phone is actually operated from — where the
|
|
29445
|
+
// thumbs rest and where the virtual keyboard comes up — instead of the middle
|
|
29446
|
+
// of the screen, which is the farthest point from both.
|
|
28800
29447
|
// Only defaults: an explicitly passed prop still wins, so the docked shape can
|
|
28801
29448
|
// be adjusted one axis at a time instead of being all-or-nothing.
|
|
28802
29449
|
const DOCKED = {
|
|
@@ -28834,7 +29481,7 @@ const useDialogProps = props => {
|
|
|
28834
29481
|
// .show() instead, staying in normal document flow, position: absolute
|
|
28835
29482
|
// relative to its own positioned ancestor. See this file's top comment.
|
|
28836
29483
|
layer = "top",
|
|
28837
|
-
|
|
29484
|
+
dockedOnSmallTouchScreen,
|
|
28838
29485
|
// Same grammar as Popover's own positionArea — see this file's top
|
|
28839
29486
|
// comment and popup_shared.js's parsePositionArea.
|
|
28840
29487
|
positionArea: positionAreaProp,
|
|
@@ -28887,16 +29534,18 @@ const useDialogProps = props => {
|
|
|
28887
29534
|
});
|
|
28888
29535
|
const isModal = layer === "top";
|
|
28889
29536
|
const ref = props.ref;
|
|
28890
|
-
// Only touch changes anything:
|
|
28891
|
-
//
|
|
28892
|
-
|
|
29537
|
+
// Only a small touch screen changes anything: on a mouse — and on a touch
|
|
29538
|
+
// screen too big to reach the bottom edge of — a dialog already wants to be
|
|
29539
|
+
// the centered box it is by default, so there is nothing to resolve.
|
|
29540
|
+
const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value;
|
|
28893
29541
|
const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
|
|
28894
29542
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
28895
|
-
// A share of whatever holds the dialog: the
|
|
28896
|
-
// one — where
|
|
28897
|
-
//
|
|
28898
|
-
//
|
|
28899
|
-
|
|
29543
|
+
// A share of whatever holds the dialog: the app's own screen for a
|
|
29544
|
+
// top-layer one — where appw is exactly "3% of the container", the
|
|
29545
|
+
// container being that screen (the viewport, unless the app declared a
|
|
29546
|
+
// narrower one) — and the positioned ancestor for a local one, where
|
|
29547
|
+
// reading 3% of the screen gives an absurd gap inside a small box.
|
|
29548
|
+
isModal ? "3appw" : "3cqw");
|
|
28900
29549
|
// "expand || expandX", the shorthand semantics Popup used to apply before
|
|
28901
29550
|
// handing them over — the docked default only applies when neither was said
|
|
28902
29551
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
@@ -29133,7 +29782,7 @@ const useDialogProps = props => {
|
|
|
29133
29782
|
// A value only CSS could evaluate (a spacing token resolving to a var(),
|
|
29134
29783
|
// a percentage…) — the placement below needs a real number, and letting
|
|
29135
29784
|
// it through would put the dialog at NaN.
|
|
29136
|
-
console.warn(`Dialog: marginWithContainer="${marginWithContainer}" cannot be resolved to pixels. Use a number, a viewport length ("3vvw", "2vvh") or a container length ("3cqw", "2cqh").`);
|
|
29785
|
+
console.warn(`Dialog: marginWithContainer="${marginWithContainer}" cannot be resolved to pixels. Use a number, a viewport length ("3appw", "3vvw", "2vvh") or a container length ("3cqw", "2cqh").`);
|
|
29137
29786
|
marginWithContainerInPixels = 0;
|
|
29138
29787
|
}
|
|
29139
29788
|
// The size caps read the same gap in CSS as the placement below applies
|
|
@@ -29560,8 +30209,10 @@ const css$U = /* css */`
|
|
|
29560
30209
|
rather than a value so an outer component can bridge its own prop into
|
|
29561
30210
|
--popover-max-height without having to restate 300px (see picker). */
|
|
29562
30211
|
--popover-max-height-default: 300px;
|
|
29563
|
-
--
|
|
29564
|
-
|
|
30212
|
+
/* --navi-app-*, not --navi-vvw/vvh: the app's own screen, which is the
|
|
30213
|
+
viewport unless the app declared a narrower one (navi_css_vars.js). */
|
|
30214
|
+
--popover-maxmax-height: calc(0.95 * var(--navi-app-height));
|
|
30215
|
+
--popover-maxmax-width: calc(0.95 * var(--navi-app-width));
|
|
29565
30216
|
|
|
29566
30217
|
--popover-box-shadow: var(--navi-popup-box-shadow);
|
|
29567
30218
|
--popover-border-radius: var(--navi-popup-border-radius);
|
|
@@ -31029,8 +31680,8 @@ const css$T = /* css */`
|
|
|
31029
31680
|
* @property {"close"|"cancel"|"capture"|"none"} [pointerInteractionOutsideEffect]
|
|
31030
31681
|
* - What a click outside does. `"capture"`/`"none"` force an explicit answer
|
|
31031
31682
|
* by refusing to treat a click elsewhere as one.
|
|
31032
|
-
* @property {boolean} [
|
|
31033
|
-
* into a bottom sheet
|
|
31683
|
+
* @property {boolean} [dockedOnSmallTouchScreen] - `"dialog"` mode only: turn
|
|
31684
|
+
* the popup into a bottom sheet on a small touch screen.
|
|
31034
31685
|
* @property {(params: { message: import("ignore:preact").ComponentChildren }) => import("ignore:preact").ComponentChildren} [renderContent]
|
|
31035
31686
|
* - Replaces the popup body — the question and the two buttons — for every
|
|
31036
31687
|
* confirmation at once. The per-button `confirmPopupContent` prop is the same
|
|
@@ -31048,7 +31699,7 @@ const confirmPopupOptions = {
|
|
|
31048
31699
|
animationDuration: undefined,
|
|
31049
31700
|
positionArea: undefined,
|
|
31050
31701
|
pointerInteractionOutsideEffect: "close",
|
|
31051
|
-
|
|
31702
|
+
dockedOnSmallTouchScreen: false,
|
|
31052
31703
|
renderContent: undefined
|
|
31053
31704
|
};
|
|
31054
31705
|
|
|
@@ -31130,7 +31781,7 @@ const ConfirmPopup = ({
|
|
|
31130
31781
|
animationDuration,
|
|
31131
31782
|
positionArea,
|
|
31132
31783
|
pointerInteractionOutsideEffect,
|
|
31133
|
-
|
|
31784
|
+
dockedOnSmallTouchScreen,
|
|
31134
31785
|
renderContent
|
|
31135
31786
|
} = confirmPopupOptions;
|
|
31136
31787
|
|
|
@@ -31166,7 +31817,7 @@ const ConfirmPopup = ({
|
|
|
31166
31817
|
if (mode === "dialog") {
|
|
31167
31818
|
return jsx(Dialog, {
|
|
31168
31819
|
className: "navi_confirm_popup",
|
|
31169
|
-
|
|
31820
|
+
dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
|
|
31170
31821
|
...popupProps,
|
|
31171
31822
|
children: body
|
|
31172
31823
|
});
|
|
@@ -31421,6 +32072,16 @@ const debounceSignal = (
|
|
|
31421
32072
|
* The action will not fire while the user is actively changing filters; it fires once
|
|
31422
32073
|
* they pause for half a second.
|
|
31423
32074
|
*/
|
|
32075
|
+
// The run is not awaited here, and a rejection nobody waits for is an unhandled
|
|
32076
|
+
// one — in dev, an error overlay thrown over a page that is already saying what
|
|
32077
|
+
// went wrong. Nothing is lost by dropping it: the failure is held by the action
|
|
32078
|
+
// itself, and whoever reads it (useAsyncData, <Button action>) is what shows it.
|
|
32079
|
+
const runUnwatched = (result) => {
|
|
32080
|
+
if (result && typeof result.catch === "function") {
|
|
32081
|
+
result.catch(() => {});
|
|
32082
|
+
}
|
|
32083
|
+
};
|
|
32084
|
+
|
|
31424
32085
|
const actionRunEffect = (
|
|
31425
32086
|
action,
|
|
31426
32087
|
deriveActionParamsFromSignals,
|
|
@@ -31470,7 +32131,7 @@ const actionRunEffect = (
|
|
|
31470
32131
|
// falsy params, don't run
|
|
31471
32132
|
return;
|
|
31472
32133
|
}
|
|
31473
|
-
actionTarget.run({ reason: "truthy params first run" });
|
|
32134
|
+
runUnwatched(actionTarget.run({ reason: "truthy params first run" }));
|
|
31474
32135
|
return;
|
|
31475
32136
|
}
|
|
31476
32137
|
|
|
@@ -31487,16 +32148,20 @@ const actionRunEffect = (
|
|
|
31487
32148
|
}
|
|
31488
32149
|
if (!actionTargetPrevious.params) {
|
|
31489
32150
|
// coming from falsy-params state: action may already be cached, avoid unnecessary rerun
|
|
31490
|
-
|
|
32151
|
+
runUnwatched(
|
|
32152
|
+
actionTarget.run({ reason: "params restored from falsy state" }),
|
|
32153
|
+
);
|
|
31491
32154
|
} else {
|
|
31492
|
-
actionTarget.rerun({ reason: "params modified" });
|
|
32155
|
+
runUnwatched(actionTarget.rerun({ reason: "params modified" }));
|
|
31493
32156
|
}
|
|
31494
32157
|
}
|
|
31495
32158
|
},
|
|
31496
32159
|
...options,
|
|
31497
32160
|
});
|
|
31498
32161
|
if (actionParamsSignal.peek()) {
|
|
31499
|
-
|
|
32162
|
+
runUnwatched(
|
|
32163
|
+
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
32164
|
+
);
|
|
31500
32165
|
}
|
|
31501
32166
|
return actionRunnedByThisEffect;
|
|
31502
32167
|
};
|
|
@@ -34620,6 +35285,24 @@ const TYPE_CONVERTERS = {
|
|
|
34620
35285
|
},
|
|
34621
35286
|
};
|
|
34622
35287
|
|
|
35288
|
+
/**
|
|
35289
|
+
* A container has put its page on screen — or as much of it as it can.
|
|
35290
|
+
*
|
|
35291
|
+
* A route matching is a signal changing, and the page it selects reaches the
|
|
35292
|
+
* DOM only once Preact has rendered — an unknown number of passes later, in an
|
|
35293
|
+
* unknown number of microtasks. Anyone who needs the page as it IS rather than
|
|
35294
|
+
* as it has been decided (a travel about to have its picture taken by the
|
|
35295
|
+
* browser, see route_travel.jsx) waits for this instead of counting.
|
|
35296
|
+
*
|
|
35297
|
+
* A page waiting on data is announced too, by the boundary showing its loading
|
|
35298
|
+
* state (see Loading in use_async_data.jsx): what the container could put on
|
|
35299
|
+
* screen is what the browser is about to take a picture of, and a page that
|
|
35300
|
+
* cannot render yet would otherwise be waited on until the transition dies of
|
|
35301
|
+
* it. It lives in a module of its own for that: the async layer says it as much
|
|
35302
|
+
* as the router does, and neither can import the other.
|
|
35303
|
+
*/
|
|
35304
|
+
const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
35305
|
+
|
|
34623
35306
|
const promiseStateWeakMap = new WeakMap();
|
|
34624
35307
|
const usePromiseAsyncData = (
|
|
34625
35308
|
promise,
|
|
@@ -34674,7 +35357,8 @@ const useForceRender = () => {
|
|
|
34674
35357
|
|
|
34675
35358
|
const useAsyncData = (promiseOrAction, {
|
|
34676
35359
|
loading = "delegate",
|
|
34677
|
-
error = "delegate"
|
|
35360
|
+
error = "delegate",
|
|
35361
|
+
onLoad
|
|
34678
35362
|
} = {}) => {
|
|
34679
35363
|
const isAction = Boolean(promiseOrAction && promiseOrAction.isAction);
|
|
34680
35364
|
if (loading === true) {
|
|
@@ -34686,7 +35370,8 @@ const useAsyncData = (promiseOrAction, {
|
|
|
34686
35370
|
if (isAction) {
|
|
34687
35371
|
return useActionAsyncData(promiseOrAction, {
|
|
34688
35372
|
loadingEffect: loading,
|
|
34689
|
-
errorEffect: error
|
|
35373
|
+
errorEffect: error,
|
|
35374
|
+
onLoad
|
|
34690
35375
|
});
|
|
34691
35376
|
}
|
|
34692
35377
|
return usePromiseAsyncData(promiseOrAction, {
|
|
@@ -34703,12 +35388,14 @@ const dismissedActionWeakSet = new WeakSet();
|
|
|
34703
35388
|
const dismissedActionPendingPromiseWeakMap = new WeakMap();
|
|
34704
35389
|
const useActionAsyncData = (action, {
|
|
34705
35390
|
loadingEffect,
|
|
34706
|
-
errorEffect
|
|
35391
|
+
errorEffect,
|
|
35392
|
+
onLoad
|
|
34707
35393
|
}) => {
|
|
34708
35394
|
const loadingRef = useContext(LoadingContext);
|
|
34709
35395
|
if (!loadingRef) {
|
|
34710
35396
|
throw new Error("Missing <Loading>");
|
|
34711
35397
|
}
|
|
35398
|
+
useOnLoad(action, onLoad);
|
|
34712
35399
|
|
|
34713
35400
|
// Use peek() instead of .value to avoid subscribing this component to the signal.
|
|
34714
35401
|
// Reading .value would make Preact re-render the component reactively when the state
|
|
@@ -34829,6 +35516,41 @@ const useActionAsyncData = (action, {
|
|
|
34829
35516
|
throw pendingPromise;
|
|
34830
35517
|
};
|
|
34831
35518
|
|
|
35519
|
+
// What a screen does with the data once, when it becomes known (see onLoad in
|
|
35520
|
+
// the JSDoc above). Kept apart because the two questions it answers are not the
|
|
35521
|
+
// ones the hook around it answers: WHEN — a layout effect, so a form taking its
|
|
35522
|
+
// reference in the same tick sees what was written; and HOW OFTEN — once per set
|
|
35523
|
+
// of params, which is the action's own answer to "is this another thing or the
|
|
35524
|
+
// same one again".
|
|
35525
|
+
const NOTHING_SEEDED = Symbol("nothing_seeded");
|
|
35526
|
+
const useOnLoad = (action, onLoad) => {
|
|
35527
|
+
const onLoadRef = useRef(onLoad);
|
|
35528
|
+
onLoadRef.current = onLoad;
|
|
35529
|
+
const paramsSeededRef = useRef(NOTHING_SEEDED);
|
|
35530
|
+
useLayoutEffect(() => {
|
|
35531
|
+
const callback = onLoadRef.current;
|
|
35532
|
+
if (!callback) {
|
|
35533
|
+
return;
|
|
35534
|
+
}
|
|
35535
|
+
if (action.runningStateSignal.peek() !== COMPLETED) {
|
|
35536
|
+
return;
|
|
35537
|
+
}
|
|
35538
|
+
const data = action.dataSignal.peek();
|
|
35539
|
+
if (data === undefined) {
|
|
35540
|
+
return;
|
|
35541
|
+
}
|
|
35542
|
+
const params = action.paramsSignal.peek();
|
|
35543
|
+
const paramsSeeded = paramsSeededRef.current;
|
|
35544
|
+
if (paramsSeeded !== NOTHING_SEEDED && compareTwoJsValues(params, paramsSeeded)) {
|
|
35545
|
+
return;
|
|
35546
|
+
}
|
|
35547
|
+
paramsSeededRef.current = params;
|
|
35548
|
+
callback(data, {
|
|
35549
|
+
params
|
|
35550
|
+
});
|
|
35551
|
+
});
|
|
35552
|
+
};
|
|
35553
|
+
|
|
34832
35554
|
// ─── Loading ──────────────────────────────────────────────────────────────────
|
|
34833
35555
|
// Wraps Suspense. Provides LoadingContext so useAction can write the suspension
|
|
34834
35556
|
// reason. LoadingFallback reads that reason and subscribes to the action so it
|
|
@@ -34868,6 +35590,14 @@ const LoadingFallback = ({
|
|
|
34868
35590
|
setTick(n => n + 1);
|
|
34869
35591
|
});
|
|
34870
35592
|
}, [action]);
|
|
35593
|
+
// A page that suspends never gets to say it is on screen — its own effects
|
|
35594
|
+
// are held with it — so this says it for it: what the document shows of the
|
|
35595
|
+
// page arriving is this. Anyone waiting for the page to be there before
|
|
35596
|
+
// moving (a travel about to have its picture taken, see route_travel.jsx)
|
|
35597
|
+
// would otherwise wait for a render that cannot happen until the data does.
|
|
35598
|
+
useLayoutEffect(() => {
|
|
35599
|
+
publishRouteRender();
|
|
35600
|
+
});
|
|
34871
35601
|
if (loadingRef.current.reason !== "loading") {
|
|
34872
35602
|
return null;
|
|
34873
35603
|
}
|
|
@@ -36041,700 +36771,142 @@ const UITransition = ({
|
|
|
36041
36771
|
alignY,
|
|
36042
36772
|
...props
|
|
36043
36773
|
}) => {
|
|
36044
|
-
const contentIdRef = useRef(contentId);
|
|
36045
|
-
const updateContentId = () => {
|
|
36046
|
-
const uiTransition = uiTransitionRef.current;
|
|
36047
|
-
if (!uiTransition) {
|
|
36048
|
-
return;
|
|
36049
|
-
}
|
|
36050
|
-
const value = contentIdRef.current;
|
|
36051
|
-
uiTransition.updateContentId(value);
|
|
36052
|
-
};
|
|
36053
|
-
const uiTransitionContentIdContextValue = useMemo(() => {
|
|
36054
|
-
const set = new Set();
|
|
36055
|
-
const onSetChange = () => {
|
|
36056
|
-
const value = Array.from(set).join("|");
|
|
36057
|
-
contentIdRef.current = value;
|
|
36058
|
-
updateContentId();
|
|
36059
|
-
};
|
|
36060
|
-
const update = (part, newPart) => {
|
|
36061
|
-
if (!set.has(part)) {
|
|
36062
|
-
if (set.size === 0) {
|
|
36063
|
-
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id set is empty`);
|
|
36064
|
-
return;
|
|
36065
|
-
}
|
|
36066
|
-
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id not found in set, only got [${Array.from(set).join(", ")}]`);
|
|
36067
|
-
return;
|
|
36068
|
-
}
|
|
36069
|
-
set.delete(part);
|
|
36070
|
-
set.add(newPart);
|
|
36071
|
-
onSetChange();
|
|
36072
|
-
};
|
|
36073
|
-
const add = part => {
|
|
36074
|
-
if (!part) {
|
|
36075
|
-
return;
|
|
36076
|
-
}
|
|
36077
|
-
if (set.has(part)) {
|
|
36078
|
-
return;
|
|
36079
|
-
}
|
|
36080
|
-
set.add(part);
|
|
36081
|
-
onSetChange();
|
|
36082
|
-
};
|
|
36083
|
-
const remove = part => {
|
|
36084
|
-
if (!part) {
|
|
36085
|
-
return;
|
|
36086
|
-
}
|
|
36087
|
-
if (!set.has(part)) {
|
|
36088
|
-
return;
|
|
36089
|
-
}
|
|
36090
|
-
set.delete(part);
|
|
36091
|
-
onSetChange();
|
|
36092
|
-
};
|
|
36093
|
-
return {
|
|
36094
|
-
add,
|
|
36095
|
-
update,
|
|
36096
|
-
remove
|
|
36097
|
-
};
|
|
36098
|
-
}, []);
|
|
36099
|
-
const ref = useRef();
|
|
36100
|
-
const uiTransitionRefDefault = useRef();
|
|
36101
|
-
uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
|
|
36102
|
-
useLayoutEffect(() => {
|
|
36103
|
-
const uiTransition = createUITransitionController(ref.current, {
|
|
36104
|
-
alignX,
|
|
36105
|
-
alignY
|
|
36106
|
-
});
|
|
36107
|
-
uiTransitionRef.current = uiTransition;
|
|
36108
|
-
return () => {
|
|
36109
|
-
uiTransition.cleanup();
|
|
36110
|
-
};
|
|
36111
|
-
}, [disabled, alignX, alignY]);
|
|
36112
|
-
return jsxs("div", {
|
|
36113
|
-
ref: ref,
|
|
36114
|
-
...props,
|
|
36115
|
-
className: "ui_transition",
|
|
36116
|
-
"data-disabled": disabled ? "" : undefined,
|
|
36117
|
-
"data-transition-type": type,
|
|
36118
|
-
"data-transition-duration": duration,
|
|
36119
|
-
"data-debug-detection": debugDetection ? "" : undefined,
|
|
36120
|
-
"data-debug-size": debugSize ? "" : undefined,
|
|
36121
|
-
"data-debug-content": debugContent ? "" : undefined,
|
|
36122
|
-
children: [jsxs("div", {
|
|
36123
|
-
className: "ui_transition_active_group",
|
|
36124
|
-
children: [jsx("div", {
|
|
36125
|
-
className: "ui_transition_target_slot",
|
|
36126
|
-
"data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
|
|
36127
|
-
children: jsx(UITransitionContentIdContext.Provider, {
|
|
36128
|
-
value: uiTransitionContentIdContextValue,
|
|
36129
|
-
children: children
|
|
36130
|
-
})
|
|
36131
|
-
}), jsx("div", {
|
|
36132
|
-
className: "ui_transition_outgoing_slot",
|
|
36133
|
-
inert: true
|
|
36134
|
-
})]
|
|
36135
|
-
}), jsxs("div", {
|
|
36136
|
-
className: "ui_transition_previous_group",
|
|
36137
|
-
inert: true,
|
|
36138
|
-
children: [jsx("div", {
|
|
36139
|
-
className: "ui_transition_previous_target_slot"
|
|
36140
|
-
}), jsx("div", {
|
|
36141
|
-
className: "ui_transition_previous_outgoing_slot"
|
|
36142
|
-
})]
|
|
36143
|
-
})]
|
|
36144
|
-
});
|
|
36145
|
-
};
|
|
36146
|
-
|
|
36147
|
-
/**
|
|
36148
|
-
* The goal of this hook is to allow a component to set a "content key"
|
|
36149
|
-
* Meaning all content within the component is identified by that key
|
|
36150
|
-
*
|
|
36151
|
-
* When the key changes, UITransition will be able to detect that and consider the content
|
|
36152
|
-
* as changed even if the component is still the same
|
|
36153
|
-
*
|
|
36154
|
-
* This is used by <Route> to set the content key to the route path
|
|
36155
|
-
* When the route becomes inactive it will call useUITransitionContentId(undefined)
|
|
36156
|
-
* And if a sibling route becones active it will call useUITransitionContentId with its own path
|
|
36157
|
-
*
|
|
36158
|
-
*/
|
|
36159
|
-
const useUITransitionContentId = value => {
|
|
36160
|
-
const contentId = useContext(UITransitionContentIdContext);
|
|
36161
|
-
const valueRef = useRef();
|
|
36162
|
-
if (contentId !== undefined && valueRef.current !== value) {
|
|
36163
|
-
const previousValue = valueRef.current;
|
|
36164
|
-
valueRef.current = value;
|
|
36165
|
-
if (previousValue === undefined) {
|
|
36166
|
-
contentId.add(value);
|
|
36167
|
-
} else {
|
|
36168
|
-
contentId.update(previousValue, value);
|
|
36169
|
-
}
|
|
36170
|
-
}
|
|
36171
|
-
useLayoutEffect(() => {
|
|
36172
|
-
if (contentId === undefined) {
|
|
36173
|
-
return null;
|
|
36174
|
-
}
|
|
36175
|
-
return () => {
|
|
36176
|
-
contentId.remove(valueRef.current);
|
|
36177
|
-
};
|
|
36178
|
-
}, []);
|
|
36179
|
-
};
|
|
36180
|
-
|
|
36181
|
-
const documentStateSignal = signal(null);
|
|
36182
|
-
const useDocumentState = () => {
|
|
36183
|
-
return documentStateSignal.value;
|
|
36184
|
-
};
|
|
36185
|
-
const updateDocumentState = (value) => {
|
|
36186
|
-
documentStateSignal.value = value;
|
|
36187
|
-
};
|
|
36188
|
-
|
|
36189
|
-
/**
|
|
36190
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
36191
|
-
*
|
|
36192
|
-
* Everything else a router says arrives once the change is made: a route
|
|
36193
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
36194
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
36195
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
36196
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
36197
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
36198
|
-
*
|
|
36199
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
36200
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
36201
|
-
* before the URL, the visited set, or any route has changed.
|
|
36202
|
-
*
|
|
36203
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
36204
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
36205
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
36206
|
-
*/
|
|
36207
|
-
|
|
36208
|
-
|
|
36209
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
36210
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
36211
|
-
|
|
36212
|
-
const setupBrowserIntegrationViaHistory = ({
|
|
36213
|
-
applyActions,
|
|
36214
|
-
applyRouting,
|
|
36215
|
-
isRouting,
|
|
36216
|
-
}) => {
|
|
36217
|
-
const { history } = window;
|
|
36218
|
-
|
|
36219
|
-
let globalAbortController = new AbortController();
|
|
36220
|
-
const triggerGlobalAbort = (reason) => {
|
|
36221
|
-
globalAbortController.abort(reason);
|
|
36222
|
-
globalAbortController = new AbortController();
|
|
36223
|
-
};
|
|
36224
|
-
|
|
36225
|
-
const dispatchActions = (params) => {
|
|
36226
|
-
const { requestedResult } = applyActions({
|
|
36227
|
-
globalAbortSignal: globalAbortController.signal,
|
|
36228
|
-
abortSignal: new AbortController().signal,
|
|
36229
|
-
...params,
|
|
36230
|
-
});
|
|
36231
|
-
return requestedResult;
|
|
36232
|
-
};
|
|
36233
|
-
setActionDispatcher(dispatchActions);
|
|
36234
|
-
|
|
36235
|
-
const getDocumentState = () => {
|
|
36236
|
-
return window.history.state ? { ...window.history.state } : null;
|
|
36237
|
-
};
|
|
36238
|
-
|
|
36239
|
-
const historyStartAtStart = getDocumentState();
|
|
36240
|
-
const visitedUrlSet = historyStartAtStart
|
|
36241
|
-
? new Set(historyStartAtStart.jsenv_visited_urls || [])
|
|
36242
|
-
: new Set();
|
|
36243
|
-
|
|
36244
|
-
// Create a signal that tracks visited URLs for reactive updates
|
|
36245
|
-
// Using a counter instead of the Set directly for better performance
|
|
36246
|
-
// Links will check isVisited() when this signal changes
|
|
36247
|
-
const visitedUrlsSignal = signal(0);
|
|
36248
|
-
|
|
36249
|
-
const isVisited = (url) => {
|
|
36250
|
-
url = new URL(url, window.location.href).href;
|
|
36251
|
-
return visitedUrlSet.has(url);
|
|
36252
|
-
};
|
|
36253
|
-
const markUrlAsVisited = (url) => {
|
|
36254
|
-
if (visitedUrlSet.has(url)) {
|
|
36255
|
-
return;
|
|
36256
|
-
}
|
|
36257
|
-
visitedUrlSet.add(url);
|
|
36258
|
-
visitedUrlsSignal.value++;
|
|
36259
|
-
};
|
|
36260
|
-
|
|
36261
|
-
let abortController = null;
|
|
36262
|
-
const handleRoutingTask = (url, options) => {
|
|
36263
|
-
// Before anything is written: the visited set, the URL and every route are
|
|
36264
|
-
// about to change, and this is the last moment the page still stands as it
|
|
36265
|
-
// was. And after, whichever way the change went out — so that whoever took
|
|
36266
|
-
// something at the first announcement has a definite place to give it back.
|
|
36267
|
-
publishBeforeRouting({ url, ...options });
|
|
36268
|
-
try {
|
|
36269
|
-
return applyRoutingTask(url, options);
|
|
36270
|
-
} finally {
|
|
36271
|
-
publishAfterRouting({ url, ...options });
|
|
36272
|
-
}
|
|
36273
|
-
};
|
|
36274
|
-
|
|
36275
|
-
const applyRoutingTask = (url, options) => {
|
|
36276
|
-
const isSameUrl = url === window.location.href;
|
|
36277
|
-
const {
|
|
36278
|
-
reason,
|
|
36279
|
-
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
36280
|
-
state,
|
|
36281
|
-
} = options;
|
|
36282
|
-
|
|
36283
|
-
if (navigationType === "push" || navigationType === "replace") {
|
|
36284
|
-
markUrlAsVisited(url);
|
|
36285
|
-
// undefined → inherit current state (link click, neutral navigation)
|
|
36286
|
-
// null → explicit reset (no nav-state keys carried over)
|
|
36287
|
-
// {...} → explicit state from enter()/leave(), already built from currentState
|
|
36288
|
-
// When state is given it's responsability of the caller to ensure it inherits document state (or not, you want it 99% of the time)
|
|
36289
|
-
let effectiveState;
|
|
36290
|
-
const sharedState = {
|
|
36291
|
-
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
36292
|
-
};
|
|
36293
|
-
if (state === undefined) {
|
|
36294
|
-
effectiveState = {
|
|
36295
|
-
...(getDocumentState() || {}),
|
|
36296
|
-
...sharedState,
|
|
36297
|
-
};
|
|
36298
|
-
} else if (state === null) {
|
|
36299
|
-
effectiveState = sharedState;
|
|
36300
|
-
} else if (state) {
|
|
36301
|
-
effectiveState = {
|
|
36302
|
-
...state,
|
|
36303
|
-
...sharedState,
|
|
36304
|
-
};
|
|
36305
|
-
}
|
|
36306
|
-
if (navigationType === "push") {
|
|
36307
|
-
window.history.pushState(effectiveState, null, url);
|
|
36308
|
-
} else {
|
|
36309
|
-
window.history.replaceState(effectiveState, null, url);
|
|
36310
|
-
}
|
|
36311
|
-
updateDocumentUrl(url);
|
|
36312
|
-
updateDocumentState(effectiveState);
|
|
36313
|
-
} else {
|
|
36314
|
-
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
36315
|
-
markUrlAsVisited(url);
|
|
36316
|
-
updateDocumentUrl(url);
|
|
36317
|
-
updateDocumentState(state);
|
|
36318
|
-
}
|
|
36319
|
-
|
|
36320
|
-
// Skip route matching for state-only changes: push/replace to the same URL
|
|
36321
|
-
// (e.g. useNavState updating document state without changing the route).
|
|
36322
|
-
// Do NOT apply for "traverse" — window.location.href is already updated by
|
|
36323
|
-
// the browser before the popstate handler runs, so isSameUrl is always true
|
|
36324
|
-
// for back/forward navigation regardless of whether the URL actually changed.
|
|
36325
|
-
if (
|
|
36326
|
-
isSameUrl &&
|
|
36327
|
-
(navigationType === "push" || navigationType === "replace")
|
|
36328
|
-
) {
|
|
36329
|
-
return undefined;
|
|
36330
|
-
}
|
|
36331
|
-
|
|
36332
|
-
if (abortController) {
|
|
36333
|
-
abortController.abort(`navigating to ${url}`);
|
|
36334
|
-
}
|
|
36335
|
-
abortController = new AbortController();
|
|
36336
|
-
const abortSignal = abortController.signal;
|
|
36337
|
-
const { allResult, requestedResult } = applyRouting(url, {
|
|
36338
|
-
globalAbortSignal: globalAbortController.signal,
|
|
36339
|
-
abortSignal,
|
|
36340
|
-
reason,
|
|
36341
|
-
navigationType,
|
|
36342
|
-
isVisited,
|
|
36343
|
-
state,
|
|
36344
|
-
});
|
|
36345
|
-
executeWithCleanup(
|
|
36346
|
-
() => allResult,
|
|
36347
|
-
() => {
|
|
36348
|
-
abortController = undefined;
|
|
36349
|
-
},
|
|
36350
|
-
);
|
|
36351
|
-
return requestedResult;
|
|
36352
|
-
};
|
|
36353
|
-
|
|
36354
|
-
// Browser event handlers
|
|
36355
|
-
window.addEventListener(
|
|
36356
|
-
"click",
|
|
36357
|
-
(e) => {
|
|
36358
|
-
if (e.button !== 0) {
|
|
36359
|
-
// Ignore non-left clicks
|
|
36360
|
-
return;
|
|
36361
|
-
}
|
|
36362
|
-
if (e.metaKey) {
|
|
36363
|
-
// Ignore clicks with meta key (e.g. open in new tab)
|
|
36364
|
-
return;
|
|
36365
|
-
}
|
|
36366
|
-
if (e.defaultPrevented) {
|
|
36367
|
-
return;
|
|
36368
|
-
}
|
|
36369
|
-
const linkElement = e.target.closest("a");
|
|
36370
|
-
if (!linkElement) {
|
|
36774
|
+
const contentIdRef = useRef(contentId);
|
|
36775
|
+
const updateContentId = () => {
|
|
36776
|
+
const uiTransition = uiTransitionRef.current;
|
|
36777
|
+
if (!uiTransition) {
|
|
36778
|
+
return;
|
|
36779
|
+
}
|
|
36780
|
+
const value = contentIdRef.current;
|
|
36781
|
+
uiTransition.updateContentId(value);
|
|
36782
|
+
};
|
|
36783
|
+
const uiTransitionContentIdContextValue = useMemo(() => {
|
|
36784
|
+
const set = new Set();
|
|
36785
|
+
const onSetChange = () => {
|
|
36786
|
+
const value = Array.from(set).join("|");
|
|
36787
|
+
contentIdRef.current = value;
|
|
36788
|
+
updateContentId();
|
|
36789
|
+
};
|
|
36790
|
+
const update = (part, newPart) => {
|
|
36791
|
+
if (!set.has(part)) {
|
|
36792
|
+
if (set.size === 0) {
|
|
36793
|
+
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id set is empty`);
|
|
36794
|
+
return;
|
|
36795
|
+
}
|
|
36796
|
+
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id not found in set, only got [${Array.from(set).join(", ")}]`);
|
|
36371
36797
|
return;
|
|
36372
36798
|
}
|
|
36373
|
-
|
|
36799
|
+
set.delete(part);
|
|
36800
|
+
set.add(newPart);
|
|
36801
|
+
onSetChange();
|
|
36802
|
+
};
|
|
36803
|
+
const add = part => {
|
|
36804
|
+
if (!part) {
|
|
36374
36805
|
return;
|
|
36375
36806
|
}
|
|
36376
|
-
|
|
36377
|
-
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
36378
|
-
getHrefTargetInfo(href);
|
|
36379
|
-
if (isEmpty || !isSameOrigin) {
|
|
36380
|
-
// Let link to other origins be handled by the browser
|
|
36807
|
+
if (set.has(part)) {
|
|
36381
36808
|
return;
|
|
36382
36809
|
}
|
|
36383
|
-
|
|
36384
|
-
|
|
36385
|
-
|
|
36386
|
-
|
|
36387
|
-
|
|
36388
|
-
// Except this one, which the browser answers with a scroll and
|
|
36389
|
-
// nothing else: same pathname, same hash, so no event and no url
|
|
36390
|
-
// change reaches whoever is waiting on the designated element.
|
|
36391
|
-
rearmUrlTarget();
|
|
36392
|
-
}
|
|
36810
|
+
set.add(part);
|
|
36811
|
+
onSetChange();
|
|
36812
|
+
};
|
|
36813
|
+
const remove = part => {
|
|
36814
|
+
if (!part) {
|
|
36393
36815
|
return;
|
|
36394
36816
|
}
|
|
36395
|
-
|
|
36396
|
-
// page is a plain document and a link in it is a plain link. Taking it
|
|
36397
|
-
// over anyway would push the url and then have nothing to show for it —
|
|
36398
|
-
// the address bar moves and the page does not (see applyRouting's own
|
|
36399
|
-
// "not called yet" branch, which is where that used to end up).
|
|
36400
|
-
if (!isRouting()) {
|
|
36817
|
+
if (!set.has(part)) {
|
|
36401
36818
|
return;
|
|
36402
36819
|
}
|
|
36403
|
-
|
|
36404
|
-
|
|
36405
|
-
|
|
36406
|
-
navigationType: "push",
|
|
36407
|
-
});
|
|
36408
|
-
},
|
|
36409
|
-
{ capture: true },
|
|
36410
|
-
);
|
|
36411
|
-
|
|
36412
|
-
window.addEventListener(
|
|
36413
|
-
"submit",
|
|
36414
|
-
() => {
|
|
36415
|
-
// Handle form submissions?
|
|
36416
|
-
// Not needed yet
|
|
36417
|
-
},
|
|
36418
|
-
{ capture: true },
|
|
36419
|
-
);
|
|
36420
|
-
|
|
36421
|
-
window.addEventListener("popstate", (popstateEvent) => {
|
|
36422
|
-
const url = window.location.href;
|
|
36423
|
-
const state = popstateEvent.state;
|
|
36424
|
-
handleRoutingTask(url, {
|
|
36425
|
-
reason: `"popstate" event for ${url}`,
|
|
36426
|
-
navigationType: "traverse",
|
|
36427
|
-
state,
|
|
36428
|
-
});
|
|
36429
|
-
});
|
|
36430
|
-
|
|
36431
|
-
// A fragment navigation is left to the browser (see the click handler above):
|
|
36432
|
-
// it owns the indicated part of the document, and taking it over would cost
|
|
36433
|
-
// `:target` and the focus handling that come with it. The document url still
|
|
36434
|
-
// has to follow it — nothing else here would notice that it moved.
|
|
36435
|
-
window.addEventListener("hashchange", () => {
|
|
36436
|
-
updateDocumentUrl(window.location.href);
|
|
36437
|
-
});
|
|
36438
|
-
|
|
36439
|
-
const navTo = async (url, { replace, state } = {}) => {
|
|
36440
|
-
handleRoutingTask(url, {
|
|
36441
|
-
reason: `navTo called with "${url}"`,
|
|
36442
|
-
navigationType: replace ? "replace" : "push",
|
|
36443
|
-
state,
|
|
36444
|
-
});
|
|
36445
|
-
};
|
|
36446
|
-
|
|
36447
|
-
const stop = (reason = "stop called") => {
|
|
36448
|
-
triggerGlobalAbort(reason);
|
|
36449
|
-
};
|
|
36450
|
-
|
|
36451
|
-
const reload = () => {
|
|
36452
|
-
const url = window.location.href;
|
|
36453
|
-
const state = history.state;
|
|
36454
|
-
handleRoutingTask(url, {
|
|
36455
|
-
reason: "reload called",
|
|
36456
|
-
navigationType: "reload",
|
|
36457
|
-
state,
|
|
36458
|
-
});
|
|
36459
|
-
};
|
|
36460
|
-
|
|
36461
|
-
const navBack = () => {
|
|
36462
|
-
window.history.back();
|
|
36463
|
-
};
|
|
36464
|
-
|
|
36465
|
-
const navForward = () => {
|
|
36466
|
-
window.history.forward();
|
|
36467
|
-
};
|
|
36468
|
-
|
|
36469
|
-
const init = () => {
|
|
36470
|
-
const url = window.location.href;
|
|
36471
|
-
const state = history.state;
|
|
36472
|
-
handleRoutingTask(url, {
|
|
36473
|
-
reason: "routing initialization",
|
|
36474
|
-
navigationType: "load",
|
|
36475
|
-
state,
|
|
36476
|
-
});
|
|
36477
|
-
};
|
|
36478
|
-
|
|
36479
|
-
return {
|
|
36480
|
-
integration: "browser_history_api",
|
|
36481
|
-
init,
|
|
36482
|
-
navTo,
|
|
36483
|
-
stop,
|
|
36484
|
-
reload,
|
|
36485
|
-
navBack,
|
|
36486
|
-
navForward,
|
|
36487
|
-
getDocumentState,
|
|
36488
|
-
isVisited,
|
|
36489
|
-
visitedUrlsSignal,
|
|
36490
|
-
};
|
|
36491
|
-
};
|
|
36492
|
-
|
|
36493
|
-
let updateRoutes;
|
|
36494
|
-
|
|
36495
|
-
const applyActions = (params) => {
|
|
36496
|
-
const updateActionsResult = updateActions(params);
|
|
36497
|
-
const { allResult, runningActionSet } = updateActionsResult;
|
|
36498
|
-
const pendingTaskNameArray = [];
|
|
36499
|
-
for (const runningAction of runningActionSet) {
|
|
36500
|
-
pendingTaskNameArray.push(runningAction.name);
|
|
36501
|
-
}
|
|
36502
|
-
workingWhile(() => allResult, pendingTaskNameArray);
|
|
36503
|
-
return updateActionsResult;
|
|
36504
|
-
};
|
|
36505
|
-
const applyRouting = (
|
|
36506
|
-
url,
|
|
36507
|
-
{
|
|
36508
|
-
globalAbortSignal,
|
|
36509
|
-
abortSignal,
|
|
36510
|
-
// state
|
|
36511
|
-
navigationType,
|
|
36512
|
-
isVisited,
|
|
36513
|
-
reason,
|
|
36514
|
-
},
|
|
36515
|
-
) => {
|
|
36516
|
-
if (!updateRoutes) {
|
|
36517
|
-
// .init() not called yet
|
|
36518
|
-
// likely because code does not uses routing at all
|
|
36519
|
-
return {};
|
|
36520
|
-
}
|
|
36521
|
-
const {
|
|
36522
|
-
loadSet,
|
|
36523
|
-
reloadSet,
|
|
36524
|
-
abortSignalMap,
|
|
36525
|
-
routeLoadRequestedMap,
|
|
36526
|
-
activeRouteSet,
|
|
36527
|
-
} = updateRoutes(url, {
|
|
36528
|
-
navigationType,
|
|
36529
|
-
isVisited,
|
|
36530
|
-
// state,
|
|
36531
|
-
});
|
|
36532
|
-
if (
|
|
36533
|
-
(!loadSet || loadSet.size === 0) &&
|
|
36534
|
-
(!reloadSet || reloadSet.size === 0)
|
|
36535
|
-
) {
|
|
36820
|
+
set.delete(part);
|
|
36821
|
+
onSetChange();
|
|
36822
|
+
};
|
|
36536
36823
|
return {
|
|
36537
|
-
|
|
36538
|
-
|
|
36539
|
-
|
|
36824
|
+
add,
|
|
36825
|
+
update,
|
|
36826
|
+
remove
|
|
36540
36827
|
};
|
|
36541
|
-
}
|
|
36542
|
-
const
|
|
36543
|
-
|
|
36544
|
-
|
|
36545
|
-
|
|
36546
|
-
|
|
36547
|
-
|
|
36548
|
-
|
|
36549
|
-
isReplace: navigationType === "replace",
|
|
36550
|
-
});
|
|
36551
|
-
const { allResult, runningActionSet } = updateActionsResult;
|
|
36552
|
-
const pendingTaskNameArray = [];
|
|
36553
|
-
for (const [route, routeAction] of routeLoadRequestedMap) {
|
|
36554
|
-
if (runningActionSet.has(routeAction)) {
|
|
36555
|
-
pendingTaskNameArray.push(`${route.relativeUrl} -> ${routeAction.name}`);
|
|
36556
|
-
}
|
|
36557
|
-
}
|
|
36558
|
-
routingWhile(() => allResult, pendingTaskNameArray);
|
|
36559
|
-
return { ...updateActionsResult, activeRouteSet };
|
|
36560
|
-
};
|
|
36561
|
-
|
|
36562
|
-
const browserIntegration = setupBrowserIntegrationViaHistory({
|
|
36563
|
-
applyActions,
|
|
36564
|
-
applyRouting,
|
|
36565
|
-
// Routes are declared by the consumer and registered through
|
|
36566
|
-
// setOnAllRouteReady below, so "does this document route at all?" is only
|
|
36567
|
-
// answerable once that has run — hence a function, read at click time rather
|
|
36568
|
-
// than a value read at setup time.
|
|
36569
|
-
isRouting: () => Boolean(updateRoutes),
|
|
36570
|
-
});
|
|
36571
|
-
|
|
36572
|
-
setOnAllRouteReady((v) => {
|
|
36573
|
-
updateRoutes = v;
|
|
36574
|
-
browserIntegration.init();
|
|
36575
|
-
});
|
|
36576
|
-
setRouteIntegration(browserIntegration);
|
|
36577
|
-
|
|
36578
|
-
const navIntegratedVia = browserIntegration.integration;
|
|
36579
|
-
const navTo = (target, options) => {
|
|
36580
|
-
const url = new URL(target, window.location.href).href;
|
|
36581
|
-
const currentUrl = documentUrlSignal.peek();
|
|
36582
|
-
if (url === currentUrl) {
|
|
36583
|
-
if (options?.state === undefined) {
|
|
36584
|
-
return null;
|
|
36585
|
-
}
|
|
36586
|
-
// State-only update on same URL: skip if state is identical to current.
|
|
36587
|
-
const currentState = browserIntegration.getDocumentState();
|
|
36588
|
-
if (compareTwoJsValues(options.state, currentState)) {
|
|
36589
|
-
return null;
|
|
36590
|
-
}
|
|
36591
|
-
}
|
|
36592
|
-
return browserIntegration.navTo(url, options);
|
|
36593
|
-
};
|
|
36594
|
-
const stopLoad = (reason = "stopLoad() called") => {
|
|
36595
|
-
const windowIsLoading = windowIsLoadingSignal.value;
|
|
36596
|
-
if (windowIsLoading) {
|
|
36597
|
-
window.stop();
|
|
36598
|
-
}
|
|
36599
|
-
const documentIsBusy = documentIsBusySignal.value;
|
|
36600
|
-
if (documentIsBusy) {
|
|
36601
|
-
browserIntegration.stop(reason);
|
|
36602
|
-
}
|
|
36603
|
-
};
|
|
36604
|
-
const reload = browserIntegration.reload;
|
|
36605
|
-
const navBack = browserIntegration.navBack;
|
|
36606
|
-
const navForward = browserIntegration.navForward;
|
|
36607
|
-
const isVisited = browserIntegration.isVisited;
|
|
36608
|
-
const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
|
|
36609
|
-
browserIntegration.handleActionTask;
|
|
36610
|
-
|
|
36611
|
-
// Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where
|
|
36612
|
-
// the mask is derived from render order within the nearest root/async
|
|
36613
|
-
// boundary — stable across re-renders of the *same* mount, but not across a
|
|
36614
|
-
// reload (render order can differ) or even across two mounts on the same
|
|
36615
|
-
// page (two components hitting useId() in the same relative order get the
|
|
36616
|
-
// same string). Storing one of these under type: "push" bakes it into a
|
|
36617
|
-
// history entry: reload the page and the entry's key may now belong to a
|
|
36618
|
-
// completely different component (or none), silently auto-opening whatever
|
|
36619
|
-
// happens to render at that same position instead.
|
|
36620
|
-
const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
|
|
36621
|
-
const isLikelyPreactGeneratedId = (id) => PREACT_GENERATED_ID_REGEX.test(id);
|
|
36622
|
-
|
|
36623
|
-
const NO_OP = () => {};
|
|
36624
|
-
const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
|
|
36625
|
-
const useNavStateBasic = (
|
|
36626
|
-
id,
|
|
36627
|
-
{ debug, type = "replace", onLeave, defaultValue } = {},
|
|
36628
|
-
) => {
|
|
36629
|
-
// Hooks must be called unconditionally — before the !id early return.
|
|
36630
|
-
const state = documentStateSignal.value;
|
|
36631
|
-
// Key presence is the flag — the value may be anything, including undefined.
|
|
36632
|
-
const keyInState = Boolean(id && state && Object.hasOwn(state, id));
|
|
36633
|
-
const onLeaveRef = useRef(onLeave);
|
|
36634
|
-
onLeaveRef.current = onLeave;
|
|
36635
|
-
const prevKeyInStateRef = useRef(keyInState);
|
|
36636
|
-
// enteredRef tracks whether enter() was called without a matching leave() yet.
|
|
36637
|
-
// It lets the effect distinguish an external disappearance (back button → fire onLeave)
|
|
36638
|
-
// from a programmatic one (leave() already set it to false before the state updates).
|
|
36639
|
-
const enteredRef = useRef(false);
|
|
36640
|
-
useEffect(() => {
|
|
36641
|
-
const prevKeyInState = prevKeyInStateRef.current;
|
|
36642
|
-
prevKeyInStateRef.current = keyInState;
|
|
36643
|
-
if (prevKeyInState && !keyInState && enteredRef.current) {
|
|
36644
|
-
enteredRef.current = false;
|
|
36645
|
-
onLeaveRef.current?.();
|
|
36646
|
-
}
|
|
36647
|
-
}, [keyInState]);
|
|
36648
|
-
|
|
36649
|
-
if (!id) {
|
|
36650
|
-
return NO_ID_GIVEN;
|
|
36651
|
-
}
|
|
36652
|
-
|
|
36653
|
-
let effectiveType = type;
|
|
36654
|
-
if (type === "push" && isLikelyPreactGeneratedId(id)) {
|
|
36655
|
-
effectiveType = "replace";
|
|
36656
|
-
}
|
|
36657
|
-
|
|
36658
|
-
const currentValue = keyInState ? state[id] : defaultValue;
|
|
36659
|
-
|
|
36660
|
-
if (debug) {
|
|
36661
|
-
console.debug(`useNavState(${id}) current value is ${currentValue}`);
|
|
36662
|
-
}
|
|
36663
|
-
|
|
36664
|
-
// enter(value): navigate TO this state (push or replace depending on type).
|
|
36665
|
-
// Calling enter() without a value stores "on" — the mere presence of the key
|
|
36666
|
-
// in the document state is enough to match; the value just allows associating
|
|
36667
|
-
// extra data with the entry when needed.
|
|
36668
|
-
const enter = (value = "on") => {
|
|
36669
|
-
enteredRef.current = true;
|
|
36670
|
-
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
36671
|
-
if (Object.hasOwn(currentStateCopy, id) && currentStateCopy[id] === value) {
|
|
36672
|
-
return;
|
|
36673
|
-
}
|
|
36674
|
-
currentStateCopy[id] = value;
|
|
36675
|
-
navTo(window.location.href, {
|
|
36676
|
-
replace: effectiveType !== "push",
|
|
36677
|
-
state: currentStateCopy,
|
|
36828
|
+
}, []);
|
|
36829
|
+
const ref = useRef();
|
|
36830
|
+
const uiTransitionRefDefault = useRef();
|
|
36831
|
+
uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
|
|
36832
|
+
useLayoutEffect(() => {
|
|
36833
|
+
const uiTransition = createUITransitionController(ref.current, {
|
|
36834
|
+
alignX,
|
|
36835
|
+
alignY
|
|
36678
36836
|
});
|
|
36679
|
-
|
|
36680
|
-
|
|
36681
|
-
|
|
36682
|
-
|
|
36683
|
-
|
|
36684
|
-
|
|
36685
|
-
|
|
36686
|
-
|
|
36687
|
-
|
|
36688
|
-
|
|
36689
|
-
|
|
36690
|
-
|
|
36691
|
-
|
|
36692
|
-
|
|
36693
|
-
|
|
36694
|
-
|
|
36695
|
-
|
|
36696
|
-
|
|
36697
|
-
|
|
36698
|
-
|
|
36699
|
-
|
|
36700
|
-
|
|
36701
|
-
|
|
36702
|
-
|
|
36703
|
-
|
|
36837
|
+
uiTransitionRef.current = uiTransition;
|
|
36838
|
+
return () => {
|
|
36839
|
+
uiTransition.cleanup();
|
|
36840
|
+
};
|
|
36841
|
+
}, [disabled, alignX, alignY]);
|
|
36842
|
+
return jsxs("div", {
|
|
36843
|
+
ref: ref,
|
|
36844
|
+
...props,
|
|
36845
|
+
className: "ui_transition",
|
|
36846
|
+
"data-disabled": disabled ? "" : undefined,
|
|
36847
|
+
"data-transition-type": type,
|
|
36848
|
+
"data-transition-duration": duration,
|
|
36849
|
+
"data-debug-detection": debugDetection ? "" : undefined,
|
|
36850
|
+
"data-debug-size": debugSize ? "" : undefined,
|
|
36851
|
+
"data-debug-content": debugContent ? "" : undefined,
|
|
36852
|
+
children: [jsxs("div", {
|
|
36853
|
+
className: "ui_transition_active_group",
|
|
36854
|
+
children: [jsx("div", {
|
|
36855
|
+
className: "ui_transition_target_slot",
|
|
36856
|
+
"data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
|
|
36857
|
+
children: jsx(UITransitionContentIdContext.Provider, {
|
|
36858
|
+
value: uiTransitionContentIdContextValue,
|
|
36859
|
+
children: children
|
|
36860
|
+
})
|
|
36861
|
+
}), jsx("div", {
|
|
36862
|
+
className: "ui_transition_outgoing_slot",
|
|
36863
|
+
inert: true
|
|
36864
|
+
})]
|
|
36865
|
+
}), jsxs("div", {
|
|
36866
|
+
className: "ui_transition_previous_group",
|
|
36867
|
+
inert: true,
|
|
36868
|
+
children: [jsx("div", {
|
|
36869
|
+
className: "ui_transition_previous_target_slot"
|
|
36870
|
+
}), jsx("div", {
|
|
36871
|
+
className: "ui_transition_previous_outgoing_slot"
|
|
36872
|
+
})]
|
|
36873
|
+
})]
|
|
36874
|
+
});
|
|
36704
36875
|
};
|
|
36705
36876
|
|
|
36706
36877
|
/**
|
|
36707
|
-
*
|
|
36708
|
-
*
|
|
36878
|
+
* The goal of this hook is to allow a component to set a "content key"
|
|
36879
|
+
* Meaning all content within the component is identified by that key
|
|
36709
36880
|
*
|
|
36710
|
-
*
|
|
36711
|
-
*
|
|
36881
|
+
* When the key changes, UITransition will be able to detect that and consider the content
|
|
36882
|
+
* as changed even if the component is still the same
|
|
36712
36883
|
*
|
|
36713
|
-
*
|
|
36714
|
-
*
|
|
36715
|
-
*
|
|
36716
|
-
* - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
|
|
36717
|
-
* - "replace": updates the current history entry — no extra history entry is created.
|
|
36718
|
-
* Silently downgraded to "replace" (with a dev-only console.warn) when `id`
|
|
36719
|
-
* looks auto-generated (e.g. preact's own useId()) — an unstable id baked
|
|
36720
|
-
* into a pushed history entry won't survive a reload correctly, and could
|
|
36721
|
-
* even collide with a different component's own auto-generated id. Pass a
|
|
36722
|
-
* stable, explicit id to actually get "push" behavior.
|
|
36723
|
-
* @param {() => void} [options.onLeave]
|
|
36724
|
-
* Called when the state key disappears **externally** — e.g. the user presses the browser
|
|
36725
|
-
* back button. Not called when leave() is invoked programmatically.
|
|
36726
|
-
* @param {*} [options.defaultValue]
|
|
36727
|
-
* Value returned when `id` is absent from document state. Defaults to `undefined`.
|
|
36884
|
+
* This is used by <Route> to set the content key to the route path
|
|
36885
|
+
* When the route becomes inactive it will call useUITransitionContentId(undefined)
|
|
36886
|
+
* And if a sibling route becones active it will call useUITransitionContentId with its own path
|
|
36728
36887
|
*
|
|
36729
|
-
* @returns {[value, enter, leave]}
|
|
36730
|
-
* - `value`: current value from document state, or `defaultValue` when the key is absent.
|
|
36731
|
-
* - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
|
|
36732
|
-
* Calling without an argument stores `"on"` — the presence of the key is enough to match;
|
|
36733
|
-
* the value allows associating extra data when needed.
|
|
36734
|
-
* - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
|
|
36735
|
-
* or goes back in history when `type` is "push").
|
|
36736
36888
|
*/
|
|
36737
|
-
const
|
|
36889
|
+
const useUITransitionContentId = value => {
|
|
36890
|
+
const contentId = useContext(UITransitionContentIdContext);
|
|
36891
|
+
const valueRef = useRef();
|
|
36892
|
+
if (contentId !== undefined && valueRef.current !== value) {
|
|
36893
|
+
const previousValue = valueRef.current;
|
|
36894
|
+
valueRef.current = value;
|
|
36895
|
+
if (previousValue === undefined) {
|
|
36896
|
+
contentId.add(value);
|
|
36897
|
+
} else {
|
|
36898
|
+
contentId.update(previousValue, value);
|
|
36899
|
+
}
|
|
36900
|
+
}
|
|
36901
|
+
useLayoutEffect(() => {
|
|
36902
|
+
if (contentId === undefined) {
|
|
36903
|
+
return null;
|
|
36904
|
+
}
|
|
36905
|
+
return () => {
|
|
36906
|
+
contentId.remove(valueRef.current);
|
|
36907
|
+
};
|
|
36908
|
+
}, []);
|
|
36909
|
+
};
|
|
36738
36910
|
|
|
36739
36911
|
const NEVER_SET = {};
|
|
36740
36912
|
const useUrlSearchParam = (paramName, defaultValue) => {
|
|
@@ -36858,7 +37030,6 @@ const Head = ({
|
|
|
36858
37030
|
* ```
|
|
36859
37031
|
*/
|
|
36860
37032
|
|
|
36861
|
-
const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
36862
37033
|
|
|
36863
37034
|
/**
|
|
36864
37035
|
* Keep every container showing the page it is showing, whatever the routes say.
|
|
@@ -37146,6 +37317,10 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
37146
37317
|
const CAN_KEEP_PICTURE = Boolean(document.startViewTransition && !document.startViewTransition.isPolyfill);
|
|
37147
37318
|
const startViewTransition = ensureDocumentStartViewTransition();
|
|
37148
37319
|
const TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
37320
|
+
// Which way the pages move, said on the document: the pictures of a transition
|
|
37321
|
+
// hang off the root, not off the box that travels, so the box's own `axis` has
|
|
37322
|
+
// to be lent to the document for the length of the travel.
|
|
37323
|
+
const TRAVEL_AXIS_ATTRIBUTE = "data-navi-route-travel-axis";
|
|
37149
37324
|
// While a finger holds the travel: the pictures stand still and go exactly
|
|
37150
37325
|
// where it says (see the CSS, and scrubTravel).
|
|
37151
37326
|
const HOLD_ATTRIBUTE = "data-navi-route-travel-held";
|
|
@@ -37339,6 +37514,28 @@ const css$R = /* css */`
|
|
|
37339
37514
|
}
|
|
37340
37515
|
}
|
|
37341
37516
|
|
|
37517
|
+
/* The same four movements, along the axis the pages are laid out on: the
|
|
37518
|
+
start of a column is its top, so going forward there is the page rising and
|
|
37519
|
+
the next one coming up from below. */
|
|
37520
|
+
:root[${TRAVEL_AXIS_ATTRIBUTE}="y"] {
|
|
37521
|
+
&[${TRAVEL_ATTRIBUTE}="forward"] {
|
|
37522
|
+
&::view-transition-old(navi-route-travel) {
|
|
37523
|
+
animation-name: navi-route-travel-leave-towards-top;
|
|
37524
|
+
}
|
|
37525
|
+
&::view-transition-new(navi-route-travel) {
|
|
37526
|
+
animation-name: navi-route-travel-enter-from-bottom;
|
|
37527
|
+
}
|
|
37528
|
+
}
|
|
37529
|
+
&[${TRAVEL_ATTRIBUTE}="back"] {
|
|
37530
|
+
&::view-transition-old(navi-route-travel) {
|
|
37531
|
+
animation-name: navi-route-travel-leave-towards-bottom;
|
|
37532
|
+
}
|
|
37533
|
+
&::view-transition-new(navi-route-travel) {
|
|
37534
|
+
animation-name: navi-route-travel-enter-from-top;
|
|
37535
|
+
}
|
|
37536
|
+
}
|
|
37537
|
+
}
|
|
37538
|
+
|
|
37342
37539
|
@keyframes navi-route-travel-leave-towards-start {
|
|
37343
37540
|
from {
|
|
37344
37541
|
translate: 0 0;
|
|
@@ -37371,6 +37568,38 @@ const css$R = /* css */`
|
|
|
37371
37568
|
translate: 0 0;
|
|
37372
37569
|
}
|
|
37373
37570
|
}
|
|
37571
|
+
@keyframes navi-route-travel-leave-towards-top {
|
|
37572
|
+
from {
|
|
37573
|
+
translate: 0 0;
|
|
37574
|
+
}
|
|
37575
|
+
to {
|
|
37576
|
+
translate: 0 -100%;
|
|
37577
|
+
}
|
|
37578
|
+
}
|
|
37579
|
+
@keyframes navi-route-travel-enter-from-bottom {
|
|
37580
|
+
from {
|
|
37581
|
+
translate: 0 100%;
|
|
37582
|
+
}
|
|
37583
|
+
to {
|
|
37584
|
+
translate: 0 0;
|
|
37585
|
+
}
|
|
37586
|
+
}
|
|
37587
|
+
@keyframes navi-route-travel-leave-towards-bottom {
|
|
37588
|
+
from {
|
|
37589
|
+
translate: 0 0;
|
|
37590
|
+
}
|
|
37591
|
+
to {
|
|
37592
|
+
translate: 0 100%;
|
|
37593
|
+
}
|
|
37594
|
+
}
|
|
37595
|
+
@keyframes navi-route-travel-enter-from-top {
|
|
37596
|
+
from {
|
|
37597
|
+
translate: 0 -100%;
|
|
37598
|
+
}
|
|
37599
|
+
to {
|
|
37600
|
+
translate: 0 0;
|
|
37601
|
+
}
|
|
37602
|
+
}
|
|
37374
37603
|
`;
|
|
37375
37604
|
|
|
37376
37605
|
/**
|
|
@@ -37507,6 +37736,7 @@ const RouteTravel = ({
|
|
|
37507
37736
|
// own for as long as it is the one travelling.
|
|
37508
37737
|
nameForTravel(elementRef.current);
|
|
37509
37738
|
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
37739
|
+
document.documentElement.setAttribute(TRAVEL_AXIS_ATTRIBUTE, axis);
|
|
37510
37740
|
if (scrub) {
|
|
37511
37741
|
holdPictures(travel);
|
|
37512
37742
|
document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
|
|
@@ -37520,14 +37750,21 @@ const RouteTravel = ({
|
|
|
37520
37750
|
const releaseRendering = renderingHeldForRouting || holdRendering();
|
|
37521
37751
|
renderingHeldForRouting = null;
|
|
37522
37752
|
// The picture the browser is about to take must be of the page that was
|
|
37523
|
-
// asked for, and a route matching is not yet a page rendered.
|
|
37753
|
+
// asked for, and a route matching is not yet a page rendered. Watched from
|
|
37754
|
+
// here rather than from inside the callback below: the browser calls that
|
|
37755
|
+
// callback a frame later, and a navigation that has already been decided
|
|
37756
|
+
// (what follows a send, a command) renders its page in between. A wait
|
|
37757
|
+
// armed then waits for something that has already happened — until the
|
|
37758
|
+
// browser gives up on the transition, leaving the page it was leaving on
|
|
37759
|
+
// screen and an error nobody asked for.
|
|
37760
|
+
const renderWait = armRouteRenderWait();
|
|
37524
37761
|
const viewTransition = startViewTransition(async () => {
|
|
37525
37762
|
await whilePageRenders(page, async () => {
|
|
37526
37763
|
releaseRendering();
|
|
37527
37764
|
if (change) {
|
|
37528
37765
|
await change();
|
|
37529
37766
|
}
|
|
37530
|
-
});
|
|
37767
|
+
}, renderWait);
|
|
37531
37768
|
// The page arriving is in the DOM and the transition has not started
|
|
37532
37769
|
// playing: the one moment both boxes can be known.
|
|
37533
37770
|
holdTravelHeight(elementRef.current, heightBefore);
|
|
@@ -37552,6 +37789,7 @@ const RouteTravel = ({
|
|
|
37552
37789
|
viewTransition.finished.catch(() => {
|
|
37553
37790
|
// A transition that fails before it ever calls back leaves the page held:
|
|
37554
37791
|
// whoever asked for the hold gives it back, here as everywhere else.
|
|
37792
|
+
renderWait.stop();
|
|
37555
37793
|
releaseRendering();
|
|
37556
37794
|
endTravel(travel);
|
|
37557
37795
|
});
|
|
@@ -37855,6 +38093,7 @@ const RouteTravel = ({
|
|
|
37855
38093
|
// rather than pick.
|
|
37856
38094
|
unnameAfterTravel(elementRef.current);
|
|
37857
38095
|
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
38096
|
+
document.documentElement.removeAttribute(TRAVEL_AXIS_ATTRIBUTE);
|
|
37858
38097
|
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
37859
38098
|
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
37860
38099
|
releaseTravelHeight();
|
|
@@ -38417,20 +38656,30 @@ const scrubTravel = (travel, ratio) => {
|
|
|
38417
38656
|
// inside the callback of a view transition: the browser has stopped rendering
|
|
38418
38657
|
// and is waiting on this very promise to take its picture, so a wait that never
|
|
38419
38658
|
// ends is a page frozen under a transition that never became ready.
|
|
38420
|
-
|
|
38659
|
+
// Listening starts before the change, or a render landing while the change is
|
|
38660
|
+
// being awaited is a render nobody heard. Armed apart from the wait itself
|
|
38661
|
+
// because the two do not always happen at the same moment: a view transition
|
|
38662
|
+
// calls its update callback a frame after it is started, and the render can
|
|
38663
|
+
// land in that gap — see beginTravel, which arms this the moment the travel is
|
|
38664
|
+
// decided and hands it over.
|
|
38665
|
+
const armRouteRenderWait = () => {
|
|
38421
38666
|
let stopListening;
|
|
38422
38667
|
const rendered = new Promise(resolve => {
|
|
38423
|
-
// Listened for before the change, or a render landing while the change is
|
|
38424
|
-
// being awaited is a render nobody heard.
|
|
38425
38668
|
stopListening = observeRouteRender(resolve);
|
|
38426
38669
|
});
|
|
38670
|
+
return {
|
|
38671
|
+
rendered,
|
|
38672
|
+
stop: () => stopListening()
|
|
38673
|
+
};
|
|
38674
|
+
};
|
|
38675
|
+
const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
|
|
38427
38676
|
try {
|
|
38428
38677
|
await change();
|
|
38429
38678
|
if (pageIsCurrent(page)) {
|
|
38430
|
-
await rendered;
|
|
38679
|
+
await wait.rendered;
|
|
38431
38680
|
}
|
|
38432
38681
|
} finally {
|
|
38433
|
-
|
|
38682
|
+
wait.stop();
|
|
38434
38683
|
}
|
|
38435
38684
|
};
|
|
38436
38685
|
|
|
@@ -38478,12 +38727,20 @@ const pageIsCurrent = ({
|
|
|
38478
38727
|
}
|
|
38479
38728
|
return params ? route.matchesParams(params) : true;
|
|
38480
38729
|
};
|
|
38481
|
-
//
|
|
38482
|
-
//
|
|
38730
|
+
// The FIRST page that answers, as with the branches of a <Route>: several
|
|
38731
|
+
// routes match at once — a literal one and the parameterized one it is a case of
|
|
38732
|
+
// ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
|
|
38733
|
+
// and the row has to be on the page the router is showing, which is the first
|
|
38734
|
+
// one written that matches.
|
|
38735
|
+
//
|
|
38736
|
+
// Every page is read all the same, never only up to the one that answers yes: a
|
|
38737
|
+
// page that is not the current one today is the one that must wake the reader
|
|
38738
|
+
// tomorrow.
|
|
38483
38739
|
const currentPageIndex = pages => {
|
|
38484
38740
|
let currentIndex = -1;
|
|
38485
38741
|
for (let i = 0; i < pages.length; i++) {
|
|
38486
|
-
|
|
38742
|
+
const isCurrent = pageIsCurrent(pages[i]);
|
|
38743
|
+
if (isCurrent && currentIndex === -1) {
|
|
38487
38744
|
currentIndex = i;
|
|
38488
38745
|
}
|
|
38489
38746
|
}
|
|
@@ -47865,21 +48122,51 @@ const withoutEmptyFields = uiState => {
|
|
|
47865
48122
|
// register themselves in their own effects, which run first — this is the
|
|
47866
48123
|
// earliest moment the form knows what it holds. Everything after this baseline
|
|
47867
48124
|
// is a real send moving it forward (see useFormGroup's own onnavi_action_end).
|
|
48125
|
+
//
|
|
48126
|
+
// Taken a second time at the end of the tick, because "the earliest moment" is
|
|
48127
|
+
// not always late enough: a field that re-renders on its own schedule rather
|
|
48128
|
+
// than with the form — a row whose value is computed from signals, sitting
|
|
48129
|
+
// behind a memo — brings its value in a render of its own, which lands after
|
|
48130
|
+
// these effects. A form measured before it would open already changed, and
|
|
48131
|
+
// would never take the reference again. Both takes are the same arrival, so the
|
|
48132
|
+
// second one costs a render only when it moves something.
|
|
47868
48133
|
const useHeldUIStateAsSent = (uiStateController, pristineKey) => {
|
|
47869
48134
|
// The render that brought a new pristineKey read `changed` against the
|
|
47870
48135
|
// previous baseline, and nothing else is going to move: the button would stay
|
|
47871
48136
|
// lit on a form that holds exactly what it was just given. So ask for the one
|
|
47872
|
-
// render that reads the new baseline — the first
|
|
47873
|
-
// every field it is waiting for re-renders the form as it registers.
|
|
48137
|
+
// render that reads the new baseline — the first take on mount has nobody to
|
|
48138
|
+
// tell, every field it is waiting for re-renders the form as it registers.
|
|
47874
48139
|
const [, rereadBaseline] = useState(0);
|
|
47875
48140
|
const isFirstRef = useRef(true);
|
|
47876
48141
|
useLayoutEffect(() => {
|
|
47877
|
-
|
|
47878
|
-
|
|
47879
|
-
|
|
47880
|
-
|
|
47881
|
-
|
|
47882
|
-
|
|
48142
|
+
const takeBaseline = () => {
|
|
48143
|
+
const baselineBefore = uiStateController.sentUIState;
|
|
48144
|
+
const baseline = readHeldUIState(uiStateController);
|
|
48145
|
+
uiStateController.sentUIState = baseline;
|
|
48146
|
+
return !compareTwoJsValues(baselineBefore, baseline);
|
|
48147
|
+
};
|
|
48148
|
+
const moved = takeBaseline();
|
|
48149
|
+
const isFirst = isFirstRef.current;
|
|
48150
|
+
isFirstRef.current = false;
|
|
48151
|
+
if (moved && !isFirst) {
|
|
48152
|
+
rereadBaseline(count => count + 1);
|
|
48153
|
+
}
|
|
48154
|
+
// A microtask, not a timeout: everything that belongs to this arrival —
|
|
48155
|
+
// the renders preact still has queued, the state they push into the form —
|
|
48156
|
+
// happens before the tick ends, and nothing a person does can land in
|
|
48157
|
+
// between.
|
|
48158
|
+
let abandoned = false;
|
|
48159
|
+
queueMicrotask(() => {
|
|
48160
|
+
if (abandoned) {
|
|
48161
|
+
return;
|
|
48162
|
+
}
|
|
48163
|
+
if (takeBaseline()) {
|
|
48164
|
+
rereadBaseline(count => count + 1);
|
|
48165
|
+
}
|
|
48166
|
+
});
|
|
48167
|
+
return () => {
|
|
48168
|
+
abandoned = true;
|
|
48169
|
+
};
|
|
47883
48170
|
}, [uiStateController, pristineKey]);
|
|
47884
48171
|
};
|
|
47885
48172
|
const useUnregisteredControlWarning = ref => {
|
|
@@ -52273,7 +52560,7 @@ const PickerContentInsidePopup = props => {
|
|
|
52273
52560
|
// above: those exist because "expand" already means something on the picker
|
|
52274
52561
|
// itself, and this one does not. Popover ignores it, same as Dialog ignores
|
|
52275
52562
|
// marginWithAnchor.
|
|
52276
|
-
|
|
52563
|
+
dockedOnSmallTouchScreen,
|
|
52277
52564
|
animation,
|
|
52278
52565
|
...rest
|
|
52279
52566
|
} = props;
|
|
@@ -52321,7 +52608,7 @@ const PickerContentInsidePopup = props => {
|
|
|
52321
52608
|
expand: isPopover ? undefined : dialogExpand,
|
|
52322
52609
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
52323
52610
|
expandY: isPopover ? undefined : dialogExpandY,
|
|
52324
|
-
|
|
52611
|
+
dockedOnSmallTouchScreen: isPopover ? undefined : dockedOnSmallTouchScreen,
|
|
52325
52612
|
children: jsx(PopupModeContext.Provider, {
|
|
52326
52613
|
value: mode,
|
|
52327
52614
|
children: children
|
|
@@ -53999,7 +54286,7 @@ const css$v = /* css */`
|
|
|
53999
54286
|
|
|
54000
54287
|
/* Same reasoning, for the corners: a dialog squares off whatever corner
|
|
54001
54288
|
lands on its container's own (see the data-flush-* rules in dialog.jsx —
|
|
54002
|
-
a bottom sheet from
|
|
54289
|
+
a bottom sheet from dockedOnSmallTouchScreen squares its two bottom ones). A list
|
|
54003
54290
|
drawn right against that corner has to square the same one, otherwise its
|
|
54004
54291
|
own radius carves a notch out of the popup's square corner. Direct child
|
|
54005
54292
|
only: any deeper and the list is presumably inset from the popup's edge,
|
|
@@ -70991,5 +71278,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
70991
71278
|
})
|
|
70992
71279
|
});
|
|
70993
71280
|
|
|
70994
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership,
|
|
71281
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
70995
71282
|
//# sourceMappingURL=jsenv_navi.js.map
|