@jsenv/navi 0.29.44 → 0.29.45
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 +806 -718
- package/dist/jsenv_navi.js.map +21 -16
- package/dist/jsenv_navi_side_effects.js +86 -1
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/css_architecture.md +66 -0
- package/docs/form_changed.md +50 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
5
|
+
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
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
7
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
@@ -17237,15 +17237,26 @@ const isSizeSpacingKey = (key) => {
|
|
|
17237
17237
|
// "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
|
|
17238
17238
|
// shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
|
|
17239
17239
|
// they are what a popup meant to stay clear of the keyboard should use.
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
const
|
|
17240
|
+
// "appw"/"apph" are the same thing narrowed to the app's own screen: identical
|
|
17241
|
+
// to vvw/vvh until the app declares --navi-app-max-width, and a share of that
|
|
17242
|
+
// width afterwards. A gap meant to read as "a small margin" must use these —
|
|
17243
|
+
// 3vvw on a 1500px window is a 45px gap around a 600px app.
|
|
17244
|
+
// Functions rather than the signals themselves: appw/apph are not a signal to
|
|
17245
|
+
// read but a value to compute (a signal, then a CSS var read back). Reading the
|
|
17246
|
+
// signal inside still registers the same dependency for a caller doing this
|
|
17247
|
+
// during a render.
|
|
17248
|
+
const VIEWPORT_UNIT_VALUES = {
|
|
17249
|
+
appw: getAppWidth,
|
|
17250
|
+
apph: getAppHeight,
|
|
17251
|
+
vvw: () => visualViewportWidthSignal.value,
|
|
17252
|
+
vvh: () => visualViewportHeightSignal.value,
|
|
17253
|
+
vw: () => windowWidthSignal.value,
|
|
17254
|
+
vh: () => windowHeightSignal.value,
|
|
17255
|
+
dvw: () => windowWidthSignal.value,
|
|
17256
|
+
dvh: () => windowHeightSignal.value,
|
|
17257
|
+
};
|
|
17258
|
+
const VIEWPORT_LENGTH_REGEX =
|
|
17259
|
+
/^(-?\d+(?:\.\d+)?)(appw|apph|vvw|vvh|dvw|dvh|vw|vh)$/;
|
|
17249
17260
|
const resolveViewportLength = (size) => {
|
|
17250
17261
|
if (typeof size !== "string") {
|
|
17251
17262
|
return null;
|
|
@@ -17255,7 +17266,7 @@ const resolveViewportLength = (size) => {
|
|
|
17255
17266
|
return null;
|
|
17256
17267
|
}
|
|
17257
17268
|
const [, amount, unit] = match;
|
|
17258
|
-
return (parseFloat(amount) / 100) *
|
|
17269
|
+
return (parseFloat(amount) / 100) * VIEWPORT_UNIT_VALUES[unit]();
|
|
17259
17270
|
};
|
|
17260
17271
|
|
|
17261
17272
|
// "3cqw"/"2cqh" — a share of the container the given element lives in, the way
|
|
@@ -21546,6 +21557,564 @@ document.body.addEventListener(
|
|
|
21546
21557
|
{ capture: true },
|
|
21547
21558
|
);
|
|
21548
21559
|
|
|
21560
|
+
const documentStateSignal = signal(null);
|
|
21561
|
+
const useDocumentState = () => {
|
|
21562
|
+
return documentStateSignal.value;
|
|
21563
|
+
};
|
|
21564
|
+
const updateDocumentState = (value) => {
|
|
21565
|
+
documentStateSignal.value = value;
|
|
21566
|
+
};
|
|
21567
|
+
|
|
21568
|
+
/**
|
|
21569
|
+
* A navigation is ABOUT to be applied — said before its very first write.
|
|
21570
|
+
*
|
|
21571
|
+
* Everything else a router says arrives once the change is made: a route
|
|
21572
|
+
* announces that it matches, an action that it is running. That is too late for
|
|
21573
|
+
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
21574
|
+
* transitions are exactly that kind of reader — the picture they keep of the
|
|
21575
|
+
* page being left is taken at the next frame, and a render answering a signal
|
|
21576
|
+
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
21577
|
+
*
|
|
21578
|
+
* So this is the one moment where nothing has moved yet. It is published
|
|
21579
|
+
* synchronously, from the top of the navigation, and whoever listens runs
|
|
21580
|
+
* before the URL, the visited set, or any route has changed.
|
|
21581
|
+
*
|
|
21582
|
+
* The other end is published too, and for the same kind of reader: whoever
|
|
21583
|
+
* held something across the change and has nobody to hand it to gets a moment
|
|
21584
|
+
* to let go of it that does not depend on guessing how long the change takes.
|
|
21585
|
+
*/
|
|
21586
|
+
|
|
21587
|
+
|
|
21588
|
+
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
21589
|
+
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
21590
|
+
|
|
21591
|
+
const setupBrowserIntegrationViaHistory = ({
|
|
21592
|
+
applyActions,
|
|
21593
|
+
applyRouting,
|
|
21594
|
+
isRouting,
|
|
21595
|
+
}) => {
|
|
21596
|
+
const { history } = window;
|
|
21597
|
+
|
|
21598
|
+
let globalAbortController = new AbortController();
|
|
21599
|
+
const triggerGlobalAbort = (reason) => {
|
|
21600
|
+
globalAbortController.abort(reason);
|
|
21601
|
+
globalAbortController = new AbortController();
|
|
21602
|
+
};
|
|
21603
|
+
|
|
21604
|
+
const dispatchActions = (params) => {
|
|
21605
|
+
const { requestedResult } = applyActions({
|
|
21606
|
+
globalAbortSignal: globalAbortController.signal,
|
|
21607
|
+
abortSignal: new AbortController().signal,
|
|
21608
|
+
...params,
|
|
21609
|
+
});
|
|
21610
|
+
return requestedResult;
|
|
21611
|
+
};
|
|
21612
|
+
setActionDispatcher(dispatchActions);
|
|
21613
|
+
|
|
21614
|
+
const getDocumentState = () => {
|
|
21615
|
+
return window.history.state ? { ...window.history.state } : null;
|
|
21616
|
+
};
|
|
21617
|
+
|
|
21618
|
+
const historyStartAtStart = getDocumentState();
|
|
21619
|
+
const visitedUrlSet = historyStartAtStart
|
|
21620
|
+
? new Set(historyStartAtStart.jsenv_visited_urls || [])
|
|
21621
|
+
: new Set();
|
|
21622
|
+
|
|
21623
|
+
// Create a signal that tracks visited URLs for reactive updates
|
|
21624
|
+
// Using a counter instead of the Set directly for better performance
|
|
21625
|
+
// Links will check isVisited() when this signal changes
|
|
21626
|
+
const visitedUrlsSignal = signal(0);
|
|
21627
|
+
|
|
21628
|
+
const isVisited = (url) => {
|
|
21629
|
+
url = new URL(url, window.location.href).href;
|
|
21630
|
+
return visitedUrlSet.has(url);
|
|
21631
|
+
};
|
|
21632
|
+
const markUrlAsVisited = (url) => {
|
|
21633
|
+
if (visitedUrlSet.has(url)) {
|
|
21634
|
+
return;
|
|
21635
|
+
}
|
|
21636
|
+
visitedUrlSet.add(url);
|
|
21637
|
+
visitedUrlsSignal.value++;
|
|
21638
|
+
};
|
|
21639
|
+
|
|
21640
|
+
let abortController = null;
|
|
21641
|
+
const handleRoutingTask = (url, options) => {
|
|
21642
|
+
// Before anything is written: the visited set, the URL and every route are
|
|
21643
|
+
// about to change, and this is the last moment the page still stands as it
|
|
21644
|
+
// was. And after, whichever way the change went out — so that whoever took
|
|
21645
|
+
// something at the first announcement has a definite place to give it back.
|
|
21646
|
+
publishBeforeRouting({ url, ...options });
|
|
21647
|
+
try {
|
|
21648
|
+
return applyRoutingTask(url, options);
|
|
21649
|
+
} finally {
|
|
21650
|
+
publishAfterRouting({ url, ...options });
|
|
21651
|
+
}
|
|
21652
|
+
};
|
|
21653
|
+
|
|
21654
|
+
const applyRoutingTask = (url, options) => {
|
|
21655
|
+
const isSameUrl = url === window.location.href;
|
|
21656
|
+
const {
|
|
21657
|
+
reason,
|
|
21658
|
+
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
21659
|
+
state,
|
|
21660
|
+
} = options;
|
|
21661
|
+
|
|
21662
|
+
if (navigationType === "push" || navigationType === "replace") {
|
|
21663
|
+
markUrlAsVisited(url);
|
|
21664
|
+
// undefined → inherit current state (link click, neutral navigation)
|
|
21665
|
+
// null → explicit reset (no nav-state keys carried over)
|
|
21666
|
+
// {...} → explicit state from enter()/leave(), already built from currentState
|
|
21667
|
+
// 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)
|
|
21668
|
+
let effectiveState;
|
|
21669
|
+
const sharedState = {
|
|
21670
|
+
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
21671
|
+
};
|
|
21672
|
+
if (state === undefined) {
|
|
21673
|
+
effectiveState = {
|
|
21674
|
+
...(getDocumentState() || {}),
|
|
21675
|
+
...sharedState,
|
|
21676
|
+
};
|
|
21677
|
+
} else if (state === null) {
|
|
21678
|
+
effectiveState = sharedState;
|
|
21679
|
+
} else if (state) {
|
|
21680
|
+
effectiveState = {
|
|
21681
|
+
...state,
|
|
21682
|
+
...sharedState,
|
|
21683
|
+
};
|
|
21684
|
+
}
|
|
21685
|
+
if (navigationType === "push") {
|
|
21686
|
+
window.history.pushState(effectiveState, null, url);
|
|
21687
|
+
} else {
|
|
21688
|
+
window.history.replaceState(effectiveState, null, url);
|
|
21689
|
+
}
|
|
21690
|
+
updateDocumentUrl(url);
|
|
21691
|
+
updateDocumentState(effectiveState);
|
|
21692
|
+
} else {
|
|
21693
|
+
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
21694
|
+
markUrlAsVisited(url);
|
|
21695
|
+
updateDocumentUrl(url);
|
|
21696
|
+
updateDocumentState(state);
|
|
21697
|
+
}
|
|
21698
|
+
|
|
21699
|
+
// Skip route matching for state-only changes: push/replace to the same URL
|
|
21700
|
+
// (e.g. useNavState updating document state without changing the route).
|
|
21701
|
+
// Do NOT apply for "traverse" — window.location.href is already updated by
|
|
21702
|
+
// the browser before the popstate handler runs, so isSameUrl is always true
|
|
21703
|
+
// for back/forward navigation regardless of whether the URL actually changed.
|
|
21704
|
+
if (
|
|
21705
|
+
isSameUrl &&
|
|
21706
|
+
(navigationType === "push" || navigationType === "replace")
|
|
21707
|
+
) {
|
|
21708
|
+
return undefined;
|
|
21709
|
+
}
|
|
21710
|
+
|
|
21711
|
+
if (abortController) {
|
|
21712
|
+
abortController.abort(`navigating to ${url}`);
|
|
21713
|
+
}
|
|
21714
|
+
abortController = new AbortController();
|
|
21715
|
+
const abortSignal = abortController.signal;
|
|
21716
|
+
const { allResult, requestedResult } = applyRouting(url, {
|
|
21717
|
+
globalAbortSignal: globalAbortController.signal,
|
|
21718
|
+
abortSignal,
|
|
21719
|
+
reason,
|
|
21720
|
+
navigationType,
|
|
21721
|
+
isVisited,
|
|
21722
|
+
state,
|
|
21723
|
+
});
|
|
21724
|
+
executeWithCleanup(
|
|
21725
|
+
() => allResult,
|
|
21726
|
+
() => {
|
|
21727
|
+
abortController = undefined;
|
|
21728
|
+
},
|
|
21729
|
+
);
|
|
21730
|
+
return requestedResult;
|
|
21731
|
+
};
|
|
21732
|
+
|
|
21733
|
+
// Browser event handlers
|
|
21734
|
+
window.addEventListener(
|
|
21735
|
+
"click",
|
|
21736
|
+
(e) => {
|
|
21737
|
+
if (e.button !== 0) {
|
|
21738
|
+
// Ignore non-left clicks
|
|
21739
|
+
return;
|
|
21740
|
+
}
|
|
21741
|
+
if (e.metaKey) {
|
|
21742
|
+
// Ignore clicks with meta key (e.g. open in new tab)
|
|
21743
|
+
return;
|
|
21744
|
+
}
|
|
21745
|
+
if (e.defaultPrevented) {
|
|
21746
|
+
return;
|
|
21747
|
+
}
|
|
21748
|
+
const linkElement = e.target.closest("a");
|
|
21749
|
+
if (!linkElement) {
|
|
21750
|
+
return;
|
|
21751
|
+
}
|
|
21752
|
+
if (linkElement.hasAttribute("data-readonly")) {
|
|
21753
|
+
return;
|
|
21754
|
+
}
|
|
21755
|
+
const href = linkElement.href;
|
|
21756
|
+
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
21757
|
+
getHrefTargetInfo(href);
|
|
21758
|
+
if (isEmpty || !isSameOrigin) {
|
|
21759
|
+
// Let link to other origins be handled by the browser
|
|
21760
|
+
return;
|
|
21761
|
+
}
|
|
21762
|
+
if (isAnchor) {
|
|
21763
|
+
// Fragment navigation belongs to the browser: it owns the indicated
|
|
21764
|
+
// part of the document, and taking it over would cost `:target` and the
|
|
21765
|
+
// focus handling that come with it.
|
|
21766
|
+
if (isCurrent) {
|
|
21767
|
+
// Except this one, which the browser answers with a scroll and
|
|
21768
|
+
// nothing else: same pathname, same hash, so no event and no url
|
|
21769
|
+
// change reaches whoever is waiting on the designated element.
|
|
21770
|
+
rearmUrlTarget();
|
|
21771
|
+
}
|
|
21772
|
+
return;
|
|
21773
|
+
}
|
|
21774
|
+
// Nothing here declared a route, so there is nothing to route to: the
|
|
21775
|
+
// page is a plain document and a link in it is a plain link. Taking it
|
|
21776
|
+
// over anyway would push the url and then have nothing to show for it —
|
|
21777
|
+
// the address bar moves and the page does not (see applyRouting's own
|
|
21778
|
+
// "not called yet" branch, which is where that used to end up).
|
|
21779
|
+
if (!isRouting()) {
|
|
21780
|
+
return;
|
|
21781
|
+
}
|
|
21782
|
+
e.preventDefault();
|
|
21783
|
+
handleRoutingTask(href, {
|
|
21784
|
+
reason: `"click" on a[href="${href}"]`,
|
|
21785
|
+
navigationType: "push",
|
|
21786
|
+
});
|
|
21787
|
+
},
|
|
21788
|
+
{ capture: true },
|
|
21789
|
+
);
|
|
21790
|
+
|
|
21791
|
+
window.addEventListener(
|
|
21792
|
+
"submit",
|
|
21793
|
+
() => {
|
|
21794
|
+
// Handle form submissions?
|
|
21795
|
+
// Not needed yet
|
|
21796
|
+
},
|
|
21797
|
+
{ capture: true },
|
|
21798
|
+
);
|
|
21799
|
+
|
|
21800
|
+
window.addEventListener("popstate", (popstateEvent) => {
|
|
21801
|
+
const url = window.location.href;
|
|
21802
|
+
const state = popstateEvent.state;
|
|
21803
|
+
handleRoutingTask(url, {
|
|
21804
|
+
reason: `"popstate" event for ${url}`,
|
|
21805
|
+
navigationType: "traverse",
|
|
21806
|
+
state,
|
|
21807
|
+
});
|
|
21808
|
+
});
|
|
21809
|
+
|
|
21810
|
+
// A fragment navigation is left to the browser (see the click handler above):
|
|
21811
|
+
// it owns the indicated part of the document, and taking it over would cost
|
|
21812
|
+
// `:target` and the focus handling that come with it. The document url still
|
|
21813
|
+
// has to follow it — nothing else here would notice that it moved.
|
|
21814
|
+
window.addEventListener("hashchange", () => {
|
|
21815
|
+
updateDocumentUrl(window.location.href);
|
|
21816
|
+
});
|
|
21817
|
+
|
|
21818
|
+
const navTo = async (url, { replace, state } = {}) => {
|
|
21819
|
+
handleRoutingTask(url, {
|
|
21820
|
+
reason: `navTo called with "${url}"`,
|
|
21821
|
+
navigationType: replace ? "replace" : "push",
|
|
21822
|
+
state,
|
|
21823
|
+
});
|
|
21824
|
+
};
|
|
21825
|
+
|
|
21826
|
+
const stop = (reason = "stop called") => {
|
|
21827
|
+
triggerGlobalAbort(reason);
|
|
21828
|
+
};
|
|
21829
|
+
|
|
21830
|
+
const reload = () => {
|
|
21831
|
+
const url = window.location.href;
|
|
21832
|
+
const state = history.state;
|
|
21833
|
+
handleRoutingTask(url, {
|
|
21834
|
+
reason: "reload called",
|
|
21835
|
+
navigationType: "reload",
|
|
21836
|
+
state,
|
|
21837
|
+
});
|
|
21838
|
+
};
|
|
21839
|
+
|
|
21840
|
+
const navBack = () => {
|
|
21841
|
+
window.history.back();
|
|
21842
|
+
};
|
|
21843
|
+
|
|
21844
|
+
const navForward = () => {
|
|
21845
|
+
window.history.forward();
|
|
21846
|
+
};
|
|
21847
|
+
|
|
21848
|
+
const init = () => {
|
|
21849
|
+
const url = window.location.href;
|
|
21850
|
+
const state = history.state;
|
|
21851
|
+
handleRoutingTask(url, {
|
|
21852
|
+
reason: "routing initialization",
|
|
21853
|
+
navigationType: "load",
|
|
21854
|
+
state,
|
|
21855
|
+
});
|
|
21856
|
+
};
|
|
21857
|
+
|
|
21858
|
+
return {
|
|
21859
|
+
integration: "browser_history_api",
|
|
21860
|
+
init,
|
|
21861
|
+
navTo,
|
|
21862
|
+
stop,
|
|
21863
|
+
reload,
|
|
21864
|
+
navBack,
|
|
21865
|
+
navForward,
|
|
21866
|
+
getDocumentState,
|
|
21867
|
+
isVisited,
|
|
21868
|
+
visitedUrlsSignal,
|
|
21869
|
+
};
|
|
21870
|
+
};
|
|
21871
|
+
|
|
21872
|
+
let updateRoutes;
|
|
21873
|
+
|
|
21874
|
+
const applyActions = (params) => {
|
|
21875
|
+
const updateActionsResult = updateActions(params);
|
|
21876
|
+
const { allResult, runningActionSet } = updateActionsResult;
|
|
21877
|
+
const pendingTaskNameArray = [];
|
|
21878
|
+
for (const runningAction of runningActionSet) {
|
|
21879
|
+
pendingTaskNameArray.push(runningAction.name);
|
|
21880
|
+
}
|
|
21881
|
+
workingWhile(() => allResult, pendingTaskNameArray);
|
|
21882
|
+
return updateActionsResult;
|
|
21883
|
+
};
|
|
21884
|
+
const applyRouting = (
|
|
21885
|
+
url,
|
|
21886
|
+
{
|
|
21887
|
+
globalAbortSignal,
|
|
21888
|
+
abortSignal,
|
|
21889
|
+
// state
|
|
21890
|
+
navigationType,
|
|
21891
|
+
isVisited,
|
|
21892
|
+
reason,
|
|
21893
|
+
},
|
|
21894
|
+
) => {
|
|
21895
|
+
if (!updateRoutes) {
|
|
21896
|
+
// .init() not called yet
|
|
21897
|
+
// likely because code does not uses routing at all
|
|
21898
|
+
return {};
|
|
21899
|
+
}
|
|
21900
|
+
const {
|
|
21901
|
+
loadSet,
|
|
21902
|
+
reloadSet,
|
|
21903
|
+
abortSignalMap,
|
|
21904
|
+
routeLoadRequestedMap,
|
|
21905
|
+
activeRouteSet,
|
|
21906
|
+
} = updateRoutes(url, {
|
|
21907
|
+
navigationType,
|
|
21908
|
+
isVisited,
|
|
21909
|
+
// state,
|
|
21910
|
+
});
|
|
21911
|
+
if (
|
|
21912
|
+
(!loadSet || loadSet.size === 0) &&
|
|
21913
|
+
(!reloadSet || reloadSet.size === 0)
|
|
21914
|
+
) {
|
|
21915
|
+
return {
|
|
21916
|
+
allResult: undefined,
|
|
21917
|
+
requestedResult: undefined,
|
|
21918
|
+
activeRouteSet: new Set(),
|
|
21919
|
+
};
|
|
21920
|
+
}
|
|
21921
|
+
const updateActionsResult = updateActions({
|
|
21922
|
+
globalAbortSignal,
|
|
21923
|
+
abortSignal,
|
|
21924
|
+
runSet: loadSet,
|
|
21925
|
+
rerunSet: reloadSet,
|
|
21926
|
+
abortSignalMap,
|
|
21927
|
+
reason,
|
|
21928
|
+
isReplace: navigationType === "replace",
|
|
21929
|
+
});
|
|
21930
|
+
const { allResult, runningActionSet } = updateActionsResult;
|
|
21931
|
+
const pendingTaskNameArray = [];
|
|
21932
|
+
for (const [route, routeAction] of routeLoadRequestedMap) {
|
|
21933
|
+
if (runningActionSet.has(routeAction)) {
|
|
21934
|
+
pendingTaskNameArray.push(`${route.relativeUrl} -> ${routeAction.name}`);
|
|
21935
|
+
}
|
|
21936
|
+
}
|
|
21937
|
+
routingWhile(() => allResult, pendingTaskNameArray);
|
|
21938
|
+
return { ...updateActionsResult, activeRouteSet };
|
|
21939
|
+
};
|
|
21940
|
+
|
|
21941
|
+
const browserIntegration = setupBrowserIntegrationViaHistory({
|
|
21942
|
+
applyActions,
|
|
21943
|
+
applyRouting,
|
|
21944
|
+
// Routes are declared by the consumer and registered through
|
|
21945
|
+
// setOnAllRouteReady below, so "does this document route at all?" is only
|
|
21946
|
+
// answerable once that has run — hence a function, read at click time rather
|
|
21947
|
+
// than a value read at setup time.
|
|
21948
|
+
isRouting: () => Boolean(updateRoutes),
|
|
21949
|
+
});
|
|
21950
|
+
|
|
21951
|
+
setOnAllRouteReady((v) => {
|
|
21952
|
+
updateRoutes = v;
|
|
21953
|
+
browserIntegration.init();
|
|
21954
|
+
});
|
|
21955
|
+
setRouteIntegration(browserIntegration);
|
|
21956
|
+
|
|
21957
|
+
const navIntegratedVia = browserIntegration.integration;
|
|
21958
|
+
const navTo = (target, options) => {
|
|
21959
|
+
const url = new URL(target, window.location.href).href;
|
|
21960
|
+
const currentUrl = documentUrlSignal.peek();
|
|
21961
|
+
if (url === currentUrl) {
|
|
21962
|
+
if (options?.state === undefined) {
|
|
21963
|
+
return null;
|
|
21964
|
+
}
|
|
21965
|
+
// State-only update on same URL: skip if state is identical to current.
|
|
21966
|
+
const currentState = browserIntegration.getDocumentState();
|
|
21967
|
+
if (compareTwoJsValues(options.state, currentState)) {
|
|
21968
|
+
return null;
|
|
21969
|
+
}
|
|
21970
|
+
}
|
|
21971
|
+
return browserIntegration.navTo(url, options);
|
|
21972
|
+
};
|
|
21973
|
+
const stopLoad = (reason = "stopLoad() called") => {
|
|
21974
|
+
const windowIsLoading = windowIsLoadingSignal.value;
|
|
21975
|
+
if (windowIsLoading) {
|
|
21976
|
+
window.stop();
|
|
21977
|
+
}
|
|
21978
|
+
const documentIsBusy = documentIsBusySignal.value;
|
|
21979
|
+
if (documentIsBusy) {
|
|
21980
|
+
browserIntegration.stop(reason);
|
|
21981
|
+
}
|
|
21982
|
+
};
|
|
21983
|
+
const reload = browserIntegration.reload;
|
|
21984
|
+
const navBack = browserIntegration.navBack;
|
|
21985
|
+
const navForward = browserIntegration.navForward;
|
|
21986
|
+
const isVisited = browserIntegration.isVisited;
|
|
21987
|
+
const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
|
|
21988
|
+
browserIntegration.handleActionTask;
|
|
21989
|
+
|
|
21990
|
+
// Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where
|
|
21991
|
+
// the mask is derived from render order within the nearest root/async
|
|
21992
|
+
// boundary — stable across re-renders of the *same* mount, but not across a
|
|
21993
|
+
// reload (render order can differ) or even across two mounts on the same
|
|
21994
|
+
// page (two components hitting useId() in the same relative order get the
|
|
21995
|
+
// same string). Storing one of these under type: "push" bakes it into a
|
|
21996
|
+
// history entry: reload the page and the entry's key may now belong to a
|
|
21997
|
+
// completely different component (or none), silently auto-opening whatever
|
|
21998
|
+
// happens to render at that same position instead.
|
|
21999
|
+
const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
|
|
22000
|
+
const isLikelyPreactGeneratedId = (id) => PREACT_GENERATED_ID_REGEX.test(id);
|
|
22001
|
+
|
|
22002
|
+
const NO_OP = () => {};
|
|
22003
|
+
const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
|
|
22004
|
+
const useNavStateBasic = (
|
|
22005
|
+
id,
|
|
22006
|
+
{ debug, type = "replace", onLeave, defaultValue } = {},
|
|
22007
|
+
) => {
|
|
22008
|
+
// Hooks must be called unconditionally — before the !id early return.
|
|
22009
|
+
const state = documentStateSignal.value;
|
|
22010
|
+
// Key presence is the flag — the value may be anything, including undefined.
|
|
22011
|
+
const keyInState = Boolean(id && state && Object.hasOwn(state, id));
|
|
22012
|
+
const onLeaveRef = useRef(onLeave);
|
|
22013
|
+
onLeaveRef.current = onLeave;
|
|
22014
|
+
const prevKeyInStateRef = useRef(keyInState);
|
|
22015
|
+
// enteredRef tracks whether enter() was called without a matching leave() yet.
|
|
22016
|
+
// It lets the effect distinguish an external disappearance (back button → fire onLeave)
|
|
22017
|
+
// from a programmatic one (leave() already set it to false before the state updates).
|
|
22018
|
+
const enteredRef = useRef(false);
|
|
22019
|
+
useEffect(() => {
|
|
22020
|
+
const prevKeyInState = prevKeyInStateRef.current;
|
|
22021
|
+
prevKeyInStateRef.current = keyInState;
|
|
22022
|
+
if (prevKeyInState && !keyInState && enteredRef.current) {
|
|
22023
|
+
enteredRef.current = false;
|
|
22024
|
+
onLeaveRef.current?.();
|
|
22025
|
+
}
|
|
22026
|
+
}, [keyInState]);
|
|
22027
|
+
|
|
22028
|
+
if (!id) {
|
|
22029
|
+
return NO_ID_GIVEN;
|
|
22030
|
+
}
|
|
22031
|
+
|
|
22032
|
+
let effectiveType = type;
|
|
22033
|
+
if (type === "push" && isLikelyPreactGeneratedId(id)) {
|
|
22034
|
+
effectiveType = "replace";
|
|
22035
|
+
}
|
|
22036
|
+
|
|
22037
|
+
const currentValue = keyInState ? state[id] : defaultValue;
|
|
22038
|
+
|
|
22039
|
+
if (debug) {
|
|
22040
|
+
console.debug(`useNavState(${id}) current value is ${currentValue}`);
|
|
22041
|
+
}
|
|
22042
|
+
|
|
22043
|
+
// enter(value): navigate TO this state (push or replace depending on type).
|
|
22044
|
+
// Calling enter() without a value stores "on" — the mere presence of the key
|
|
22045
|
+
// in the document state is enough to match; the value just allows associating
|
|
22046
|
+
// extra data with the entry when needed.
|
|
22047
|
+
const enter = (value = "on") => {
|
|
22048
|
+
enteredRef.current = true;
|
|
22049
|
+
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
22050
|
+
if (Object.hasOwn(currentStateCopy, id) && currentStateCopy[id] === value) {
|
|
22051
|
+
return;
|
|
22052
|
+
}
|
|
22053
|
+
currentStateCopy[id] = value;
|
|
22054
|
+
navTo(window.location.href, {
|
|
22055
|
+
replace: effectiveType !== "push",
|
|
22056
|
+
state: currentStateCopy,
|
|
22057
|
+
});
|
|
22058
|
+
};
|
|
22059
|
+
|
|
22060
|
+
// leave(): navigate AWAY FROM this state (navBack in push mode, replace in replace mode).
|
|
22061
|
+
// isBack: when true (cancel close in push mode), call history.back() to restore the
|
|
22062
|
+
// pre-open state — discards any in-progress edits.
|
|
22063
|
+
// When false (confirmed close), replace the pushed entry instead: preserves the
|
|
22064
|
+
// current URL state (e.g. a new picker value) while removing the popup key.
|
|
22065
|
+
const leave = ({ isBack } = {}) => {
|
|
22066
|
+
enteredRef.current = false;
|
|
22067
|
+
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
22068
|
+
if (!Object.hasOwn(currentStateCopy, id)) {
|
|
22069
|
+
return;
|
|
22070
|
+
}
|
|
22071
|
+
if (effectiveType === "push" && isBack) {
|
|
22072
|
+
browserIntegration.navBack();
|
|
22073
|
+
} else {
|
|
22074
|
+
delete currentStateCopy[id];
|
|
22075
|
+
navTo(window.location.href, {
|
|
22076
|
+
replace: true,
|
|
22077
|
+
state: currentStateCopy,
|
|
22078
|
+
});
|
|
22079
|
+
}
|
|
22080
|
+
};
|
|
22081
|
+
|
|
22082
|
+
return [currentValue, enter, leave];
|
|
22083
|
+
};
|
|
22084
|
+
|
|
22085
|
+
/**
|
|
22086
|
+
* Stores a named value in the browser's document state and returns it reactively.
|
|
22087
|
+
* The component re-renders whenever the value changes (navigation, back/forward button).
|
|
22088
|
+
*
|
|
22089
|
+
* @param {string} id
|
|
22090
|
+
* Unique key used to store the value in document state. Must be stable across renders.
|
|
22091
|
+
*
|
|
22092
|
+
* @param {object} [options]
|
|
22093
|
+
* @param {"push"|"replace"} [options.type="replace"]
|
|
22094
|
+
* Controls how enter() adds the state to browser history.
|
|
22095
|
+
* - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
|
|
22096
|
+
* - "replace": updates the current history entry — no extra history entry is created.
|
|
22097
|
+
* Silently downgraded to "replace" (with a dev-only console.warn) when `id`
|
|
22098
|
+
* looks auto-generated (e.g. preact's own useId()) — an unstable id baked
|
|
22099
|
+
* into a pushed history entry won't survive a reload correctly, and could
|
|
22100
|
+
* even collide with a different component's own auto-generated id. Pass a
|
|
22101
|
+
* stable, explicit id to actually get "push" behavior.
|
|
22102
|
+
* @param {() => void} [options.onLeave]
|
|
22103
|
+
* Called when the state key disappears **externally** — e.g. the user presses the browser
|
|
22104
|
+
* back button. Not called when leave() is invoked programmatically.
|
|
22105
|
+
* @param {*} [options.defaultValue]
|
|
22106
|
+
* Value returned when `id` is absent from document state. Defaults to `undefined`.
|
|
22107
|
+
*
|
|
22108
|
+
* @returns {[value, enter, leave]}
|
|
22109
|
+
* - `value`: current value from document state, or `defaultValue` when the key is absent.
|
|
22110
|
+
* - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
|
|
22111
|
+
* Calling without an argument stores `"on"` — the presence of the key is enough to match;
|
|
22112
|
+
* the value allows associating extra data when needed.
|
|
22113
|
+
* - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
|
|
22114
|
+
* or goes back in history when `type` is "push").
|
|
22115
|
+
*/
|
|
22116
|
+
const useNavState = useNavStateBasic;
|
|
22117
|
+
|
|
21549
22118
|
/**
|
|
21550
22119
|
* @param {Element} element The element asking — the command's source, and the
|
|
21551
22120
|
* anchor a popup opens on unless `anchor` says otherwise.
|
|
@@ -21953,15 +22522,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21953
22522
|
requester = firstButtonSubmitting;
|
|
21954
22523
|
}
|
|
21955
22524
|
}
|
|
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
22525
|
// Nothing is committed when a constraint fails, so nothing is decided
|
|
21961
22526
|
// and the popup must stay open — with the form still in front of the
|
|
21962
22527
|
// user, showing what it is waiting for.
|
|
21963
22528
|
let invalid = false;
|
|
22529
|
+
// What follows the send is read at the moment it runs, never before it:
|
|
22530
|
+
// it depends on the requester (Enter in a field sends through the first
|
|
22531
|
+
// submit button, and what follows is that button's answer), and on
|
|
22532
|
+
// anything the send itself decided — an action that learned where to go
|
|
22533
|
+
// from the response writes it on the form while it runs
|
|
22534
|
+
// (data-after-send), and this is what picks it up.
|
|
21964
22535
|
const runAfterSend = () => {
|
|
22536
|
+
const afterSend = resolveAfterSend(target, requester);
|
|
22537
|
+
if (!afterSend) {
|
|
22538
|
+
return;
|
|
22539
|
+
}
|
|
21965
22540
|
triggerNaviCommand(source, afterSend, event, { optional: true });
|
|
21966
22541
|
};
|
|
21967
22542
|
const {
|
|
@@ -21996,7 +22571,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21996
22571
|
requester,
|
|
21997
22572
|
}),
|
|
21998
22573
|
);
|
|
21999
|
-
if (sent === false || invalid
|
|
22574
|
+
if (sent === false || invalid) {
|
|
22000
22575
|
return sent;
|
|
22001
22576
|
}
|
|
22002
22577
|
if (isRunning) {
|
|
@@ -22188,6 +22763,30 @@ registerNaviCommand("--navi-back", (source, event) => {
|
|
|
22188
22763
|
};
|
|
22189
22764
|
});
|
|
22190
22765
|
|
|
22766
|
+
// Where a press takes the user. The destination is the command's argument
|
|
22767
|
+
// because it says WHAT the command does — "--navi-nav-to:/games/42" — which is how
|
|
22768
|
+
// it can also be what follows a form submission: the form has answered its
|
|
22769
|
+
// question, and the answer to "what now" is a page.
|
|
22770
|
+
//
|
|
22771
|
+
// A destination fixed at the call site, so it is for a page known before the
|
|
22772
|
+
// send — which is what a form needs, since it must also know where to go when
|
|
22773
|
+
// the press had nothing to send. A destination the response decides (a
|
|
22774
|
+
// creation, whose id comes back with it) is the action's own business: it
|
|
22775
|
+
// navigates itself.
|
|
22776
|
+
registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
|
|
22777
|
+
if (!argument) {
|
|
22778
|
+
console.warn(
|
|
22779
|
+
`[navi] "--navi-nav-to" needs a destination: --navi-nav-to:/the/url (relative to the current page, or absolute).`,
|
|
22780
|
+
);
|
|
22781
|
+
return undefined;
|
|
22782
|
+
}
|
|
22783
|
+
const target = resolveExplicitTarget(source) || source;
|
|
22784
|
+
return {
|
|
22785
|
+
target,
|
|
22786
|
+
implementation: () => navTo(argument),
|
|
22787
|
+
};
|
|
22788
|
+
});
|
|
22789
|
+
|
|
22191
22790
|
registerNaviCommand("--navi-toggle", (source, event, { anchor } = {}) => {
|
|
22192
22791
|
const target =
|
|
22193
22792
|
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
@@ -24272,6 +24871,7 @@ const useUIGroupStateController = (
|
|
|
24272
24871
|
pendingChangeRef.current = null;
|
|
24273
24872
|
const batchedEvent = new CustomEvent(
|
|
24274
24873
|
`${controlType}_batched_ui_state_update`,
|
|
24874
|
+
{ detail: {} },
|
|
24275
24875
|
);
|
|
24276
24876
|
chainEvent(batchedEvent, pendingChange.e);
|
|
24277
24877
|
scope._onChange(batchedEvent, {
|
|
@@ -28218,15 +28818,26 @@ const css$V = /* css */`
|
|
|
28218
28818
|
|
|
28219
28819
|
Capping the *size* here rather than only offsetting the position is
|
|
28220
28820
|
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
|
-
|
|
28821
|
+
free: --navi-app-width/--navi-app-height track the visual viewport, so
|
|
28822
|
+
the browser reflows the dialog itself as the keyboard opens.
|
|
28823
|
+
|
|
28824
|
+
A share of the app's own screen, not of the window (hence
|
|
28825
|
+
--navi-app-width rather than 3vvw): the gap must read as a small
|
|
28826
|
+
margin around the dialog, and 3% of a 1500px window is a 45px gap
|
|
28827
|
+
around a 600px app. Identical to 3vvw until the app declares
|
|
28828
|
+
--navi-app-max-width. */
|
|
28829
|
+
--x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
|
|
28830
|
+
|
|
28831
|
+
/* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
|
|
28832
|
+
the app's own screen, which is the viewport unless the app declared a
|
|
28833
|
+
narrower one (see navi_css_vars.js). An app-width cap alone never
|
|
28834
|
+
costs the gap below — it is subtracted from whichever of the two ends
|
|
28835
|
+
up smaller. */
|
|
28225
28836
|
--dialog-maxmax-width: calc(
|
|
28226
|
-
var(--navi-
|
|
28837
|
+
var(--navi-app-width) - 2 * var(--x-dialog-container-spacing)
|
|
28227
28838
|
);
|
|
28228
28839
|
--dialog-maxmax-height: calc(
|
|
28229
|
-
var(--navi-
|
|
28840
|
+
var(--navi-app-height) - 2 * var(--x-dialog-container-spacing)
|
|
28230
28841
|
);
|
|
28231
28842
|
|
|
28232
28843
|
--dialog-border-radius: var(--navi-popup-border-radius);
|
|
@@ -28585,13 +29196,15 @@ const css$V = /* css */`
|
|
|
28585
29196
|
* touch device.
|
|
28586
29197
|
* @param {boolean} [props.expandY] - Same, vertically
|
|
28587
29198
|
* (`--dialog-maxmax-height`).
|
|
28588
|
-
* @param {string|number} [props.marginWithContainer="
|
|
29199
|
+
* @param {string|number} [props.marginWithContainer="3appw"] - Minimum gap kept
|
|
28589
29200
|
* between the dialog and the edges of its container, whatever its
|
|
28590
29201
|
* `positionArea`: it both caps the dialog's own size (via
|
|
28591
29202
|
* `--x-dialog-container-spacing`, written from this prop) and offsets a docked
|
|
28592
29203
|
* one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
|
|
28593
|
-
* number of pixels, or a viewport length — "
|
|
28594
|
-
* viewport,
|
|
29204
|
+
* number of pixels, or a viewport length — "appw"/"apph" being the app's own
|
|
29205
|
+
* screen (the visual viewport, or the narrower one the app declared with
|
|
29206
|
+
* --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
|
|
29207
|
+
* shrinks when the mobile keyboard opens. Pass 0 for a dialog
|
|
28595
29208
|
* meant to sit flush (a side panel).
|
|
28596
29209
|
* @param {"close"|"cancel"|"capture"|"none"} [props.pointerInteractionOutsideEffect="close"]
|
|
28597
29210
|
* - `"close"` closes the dialog on an outside click. `"capture"`/`"none"`
|
|
@@ -28892,11 +29505,12 @@ const useDialogProps = props => {
|
|
|
28892
29505
|
const isDocked = dockedOnTouch && coarsePointerSignal.value;
|
|
28893
29506
|
const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
|
|
28894
29507
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
28895
|
-
// A share of whatever holds the dialog: the
|
|
28896
|
-
// one — where
|
|
28897
|
-
//
|
|
28898
|
-
//
|
|
28899
|
-
|
|
29508
|
+
// A share of whatever holds the dialog: the app's own screen for a
|
|
29509
|
+
// top-layer one — where appw is exactly "3% of the container", the
|
|
29510
|
+
// container being that screen (the viewport, unless the app declared a
|
|
29511
|
+
// narrower one) — and the positioned ancestor for a local one, where
|
|
29512
|
+
// reading 3% of the screen gives an absurd gap inside a small box.
|
|
29513
|
+
isModal ? "3appw" : "3cqw");
|
|
28900
29514
|
// "expand || expandX", the shorthand semantics Popup used to apply before
|
|
28901
29515
|
// handing them over — the docked default only applies when neither was said
|
|
28902
29516
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
@@ -29133,7 +29747,7 @@ const useDialogProps = props => {
|
|
|
29133
29747
|
// A value only CSS could evaluate (a spacing token resolving to a var(),
|
|
29134
29748
|
// a percentage…) — the placement below needs a real number, and letting
|
|
29135
29749
|
// 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").`);
|
|
29750
|
+
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
29751
|
marginWithContainerInPixels = 0;
|
|
29138
29752
|
}
|
|
29139
29753
|
// The size caps read the same gap in CSS as the placement below applies
|
|
@@ -29560,8 +30174,10 @@ const css$U = /* css */`
|
|
|
29560
30174
|
rather than a value so an outer component can bridge its own prop into
|
|
29561
30175
|
--popover-max-height without having to restate 300px (see picker). */
|
|
29562
30176
|
--popover-max-height-default: 300px;
|
|
29563
|
-
--
|
|
29564
|
-
|
|
30177
|
+
/* --navi-app-*, not --navi-vvw/vvh: the app's own screen, which is the
|
|
30178
|
+
viewport unless the app declared a narrower one (navi_css_vars.js). */
|
|
30179
|
+
--popover-maxmax-height: calc(0.95 * var(--navi-app-height));
|
|
30180
|
+
--popover-maxmax-width: calc(0.95 * var(--navi-app-width));
|
|
29565
30181
|
|
|
29566
30182
|
--popover-box-shadow: var(--navi-popup-box-shadow);
|
|
29567
30183
|
--popover-border-radius: var(--navi-popup-border-radius);
|
|
@@ -36036,705 +36652,147 @@ const UITransition = ({
|
|
|
36036
36652
|
debugContent,
|
|
36037
36653
|
debugSize,
|
|
36038
36654
|
disabled,
|
|
36039
|
-
uiTransitionRef,
|
|
36040
|
-
alignX,
|
|
36041
|
-
alignY,
|
|
36042
|
-
...props
|
|
36043
|
-
}) => {
|
|
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}`);
|
|
36655
|
+
uiTransitionRef,
|
|
36656
|
+
alignX,
|
|
36657
|
+
alignY,
|
|
36658
|
+
...props
|
|
36659
|
+
}) => {
|
|
36660
|
+
const contentIdRef = useRef(contentId);
|
|
36661
|
+
const updateContentId = () => {
|
|
36662
|
+
const uiTransition = uiTransitionRef.current;
|
|
36663
|
+
if (!uiTransition) {
|
|
36664
|
+
return;
|
|
36334
36665
|
}
|
|
36335
|
-
|
|
36336
|
-
|
|
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;
|
|
36666
|
+
const value = contentIdRef.current;
|
|
36667
|
+
uiTransition.updateContentId(value);
|
|
36352
36668
|
};
|
|
36353
|
-
|
|
36354
|
-
|
|
36355
|
-
|
|
36356
|
-
|
|
36357
|
-
|
|
36358
|
-
|
|
36359
|
-
|
|
36360
|
-
|
|
36361
|
-
|
|
36362
|
-
|
|
36363
|
-
|
|
36364
|
-
|
|
36365
|
-
|
|
36366
|
-
|
|
36367
|
-
return;
|
|
36368
|
-
}
|
|
36369
|
-
const linkElement = e.target.closest("a");
|
|
36370
|
-
if (!linkElement) {
|
|
36669
|
+
const uiTransitionContentIdContextValue = useMemo(() => {
|
|
36670
|
+
const set = new Set();
|
|
36671
|
+
const onSetChange = () => {
|
|
36672
|
+
const value = Array.from(set).join("|");
|
|
36673
|
+
contentIdRef.current = value;
|
|
36674
|
+
updateContentId();
|
|
36675
|
+
};
|
|
36676
|
+
const update = (part, newPart) => {
|
|
36677
|
+
if (!set.has(part)) {
|
|
36678
|
+
if (set.size === 0) {
|
|
36679
|
+
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id set is empty`);
|
|
36680
|
+
return;
|
|
36681
|
+
}
|
|
36682
|
+
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id not found in set, only got [${Array.from(set).join(", ")}]`);
|
|
36371
36683
|
return;
|
|
36372
36684
|
}
|
|
36373
|
-
|
|
36685
|
+
set.delete(part);
|
|
36686
|
+
set.add(newPart);
|
|
36687
|
+
onSetChange();
|
|
36688
|
+
};
|
|
36689
|
+
const add = part => {
|
|
36690
|
+
if (!part) {
|
|
36374
36691
|
return;
|
|
36375
36692
|
}
|
|
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
|
|
36693
|
+
if (set.has(part)) {
|
|
36381
36694
|
return;
|
|
36382
36695
|
}
|
|
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
|
-
}
|
|
36696
|
+
set.add(part);
|
|
36697
|
+
onSetChange();
|
|
36698
|
+
};
|
|
36699
|
+
const remove = part => {
|
|
36700
|
+
if (!part) {
|
|
36393
36701
|
return;
|
|
36394
36702
|
}
|
|
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()) {
|
|
36703
|
+
if (!set.has(part)) {
|
|
36401
36704
|
return;
|
|
36402
36705
|
}
|
|
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
|
-
) {
|
|
36706
|
+
set.delete(part);
|
|
36707
|
+
onSetChange();
|
|
36708
|
+
};
|
|
36536
36709
|
return {
|
|
36537
|
-
|
|
36538
|
-
|
|
36539
|
-
|
|
36710
|
+
add,
|
|
36711
|
+
update,
|
|
36712
|
+
remove
|
|
36540
36713
|
};
|
|
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,
|
|
36714
|
+
}, []);
|
|
36715
|
+
const ref = useRef();
|
|
36716
|
+
const uiTransitionRefDefault = useRef();
|
|
36717
|
+
uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
|
|
36718
|
+
useLayoutEffect(() => {
|
|
36719
|
+
const uiTransition = createUITransitionController(ref.current, {
|
|
36720
|
+
alignX,
|
|
36721
|
+
alignY
|
|
36678
36722
|
});
|
|
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
|
-
|
|
36723
|
+
uiTransitionRef.current = uiTransition;
|
|
36724
|
+
return () => {
|
|
36725
|
+
uiTransition.cleanup();
|
|
36726
|
+
};
|
|
36727
|
+
}, [disabled, alignX, alignY]);
|
|
36728
|
+
return jsxs("div", {
|
|
36729
|
+
ref: ref,
|
|
36730
|
+
...props,
|
|
36731
|
+
className: "ui_transition",
|
|
36732
|
+
"data-disabled": disabled ? "" : undefined,
|
|
36733
|
+
"data-transition-type": type,
|
|
36734
|
+
"data-transition-duration": duration,
|
|
36735
|
+
"data-debug-detection": debugDetection ? "" : undefined,
|
|
36736
|
+
"data-debug-size": debugSize ? "" : undefined,
|
|
36737
|
+
"data-debug-content": debugContent ? "" : undefined,
|
|
36738
|
+
children: [jsxs("div", {
|
|
36739
|
+
className: "ui_transition_active_group",
|
|
36740
|
+
children: [jsx("div", {
|
|
36741
|
+
className: "ui_transition_target_slot",
|
|
36742
|
+
"data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
|
|
36743
|
+
children: jsx(UITransitionContentIdContext.Provider, {
|
|
36744
|
+
value: uiTransitionContentIdContextValue,
|
|
36745
|
+
children: children
|
|
36746
|
+
})
|
|
36747
|
+
}), jsx("div", {
|
|
36748
|
+
className: "ui_transition_outgoing_slot",
|
|
36749
|
+
inert: true
|
|
36750
|
+
})]
|
|
36751
|
+
}), jsxs("div", {
|
|
36752
|
+
className: "ui_transition_previous_group",
|
|
36753
|
+
inert: true,
|
|
36754
|
+
children: [jsx("div", {
|
|
36755
|
+
className: "ui_transition_previous_target_slot"
|
|
36756
|
+
}), jsx("div", {
|
|
36757
|
+
className: "ui_transition_previous_outgoing_slot"
|
|
36758
|
+
})]
|
|
36759
|
+
})]
|
|
36760
|
+
});
|
|
36704
36761
|
};
|
|
36705
36762
|
|
|
36706
36763
|
/**
|
|
36707
|
-
*
|
|
36708
|
-
*
|
|
36764
|
+
* The goal of this hook is to allow a component to set a "content key"
|
|
36765
|
+
* Meaning all content within the component is identified by that key
|
|
36709
36766
|
*
|
|
36710
|
-
*
|
|
36711
|
-
*
|
|
36767
|
+
* When the key changes, UITransition will be able to detect that and consider the content
|
|
36768
|
+
* as changed even if the component is still the same
|
|
36712
36769
|
*
|
|
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`.
|
|
36770
|
+
* This is used by <Route> to set the content key to the route path
|
|
36771
|
+
* When the route becomes inactive it will call useUITransitionContentId(undefined)
|
|
36772
|
+
* And if a sibling route becones active it will call useUITransitionContentId with its own path
|
|
36728
36773
|
*
|
|
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
36774
|
*/
|
|
36737
|
-
const
|
|
36775
|
+
const useUITransitionContentId = value => {
|
|
36776
|
+
const contentId = useContext(UITransitionContentIdContext);
|
|
36777
|
+
const valueRef = useRef();
|
|
36778
|
+
if (contentId !== undefined && valueRef.current !== value) {
|
|
36779
|
+
const previousValue = valueRef.current;
|
|
36780
|
+
valueRef.current = value;
|
|
36781
|
+
if (previousValue === undefined) {
|
|
36782
|
+
contentId.add(value);
|
|
36783
|
+
} else {
|
|
36784
|
+
contentId.update(previousValue, value);
|
|
36785
|
+
}
|
|
36786
|
+
}
|
|
36787
|
+
useLayoutEffect(() => {
|
|
36788
|
+
if (contentId === undefined) {
|
|
36789
|
+
return null;
|
|
36790
|
+
}
|
|
36791
|
+
return () => {
|
|
36792
|
+
contentId.remove(valueRef.current);
|
|
36793
|
+
};
|
|
36794
|
+
}, []);
|
|
36795
|
+
};
|
|
36738
36796
|
|
|
36739
36797
|
const NEVER_SET = {};
|
|
36740
36798
|
const useUrlSearchParam = (paramName, defaultValue) => {
|
|
@@ -47865,21 +47923,51 @@ const withoutEmptyFields = uiState => {
|
|
|
47865
47923
|
// register themselves in their own effects, which run first — this is the
|
|
47866
47924
|
// earliest moment the form knows what it holds. Everything after this baseline
|
|
47867
47925
|
// is a real send moving it forward (see useFormGroup's own onnavi_action_end).
|
|
47926
|
+
//
|
|
47927
|
+
// Taken a second time at the end of the tick, because "the earliest moment" is
|
|
47928
|
+
// not always late enough: a field that re-renders on its own schedule rather
|
|
47929
|
+
// than with the form — a row whose value is computed from signals, sitting
|
|
47930
|
+
// behind a memo — brings its value in a render of its own, which lands after
|
|
47931
|
+
// these effects. A form measured before it would open already changed, and
|
|
47932
|
+
// would never take the reference again. Both takes are the same arrival, so the
|
|
47933
|
+
// second one costs a render only when it moves something.
|
|
47868
47934
|
const useHeldUIStateAsSent = (uiStateController, pristineKey) => {
|
|
47869
47935
|
// The render that brought a new pristineKey read `changed` against the
|
|
47870
47936
|
// previous baseline, and nothing else is going to move: the button would stay
|
|
47871
47937
|
// 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.
|
|
47938
|
+
// render that reads the new baseline — the first take on mount has nobody to
|
|
47939
|
+
// tell, every field it is waiting for re-renders the form as it registers.
|
|
47874
47940
|
const [, rereadBaseline] = useState(0);
|
|
47875
47941
|
const isFirstRef = useRef(true);
|
|
47876
47942
|
useLayoutEffect(() => {
|
|
47877
|
-
|
|
47878
|
-
|
|
47879
|
-
|
|
47880
|
-
|
|
47881
|
-
|
|
47882
|
-
|
|
47943
|
+
const takeBaseline = () => {
|
|
47944
|
+
const baselineBefore = uiStateController.sentUIState;
|
|
47945
|
+
const baseline = readHeldUIState(uiStateController);
|
|
47946
|
+
uiStateController.sentUIState = baseline;
|
|
47947
|
+
return !compareTwoJsValues(baselineBefore, baseline);
|
|
47948
|
+
};
|
|
47949
|
+
const moved = takeBaseline();
|
|
47950
|
+
const isFirst = isFirstRef.current;
|
|
47951
|
+
isFirstRef.current = false;
|
|
47952
|
+
if (moved && !isFirst) {
|
|
47953
|
+
rereadBaseline(count => count + 1);
|
|
47954
|
+
}
|
|
47955
|
+
// A microtask, not a timeout: everything that belongs to this arrival —
|
|
47956
|
+
// the renders preact still has queued, the state they push into the form —
|
|
47957
|
+
// happens before the tick ends, and nothing a person does can land in
|
|
47958
|
+
// between.
|
|
47959
|
+
let abandoned = false;
|
|
47960
|
+
queueMicrotask(() => {
|
|
47961
|
+
if (abandoned) {
|
|
47962
|
+
return;
|
|
47963
|
+
}
|
|
47964
|
+
if (takeBaseline()) {
|
|
47965
|
+
rereadBaseline(count => count + 1);
|
|
47966
|
+
}
|
|
47967
|
+
});
|
|
47968
|
+
return () => {
|
|
47969
|
+
abandoned = true;
|
|
47970
|
+
};
|
|
47883
47971
|
}, [uiStateController, pristineKey]);
|
|
47884
47972
|
};
|
|
47885
47973
|
const useUnregisteredControlWarning = ref => {
|