@jsenv/navi 0.29.43 → 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 +1021 -761
- package/dist/jsenv_navi.js.map +40 -27
- package/dist/jsenv_navi_side_effects.js +86 -1
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +12 -4
- package/docs/control_group.md +5 -0
- package/docs/control_object.md +133 -0
- package/docs/control_value.md +19 -0
- package/docs/css_architecture.md +66 -0
- package/docs/form_changed.md +52 -0
- package/docs/list_refresh.md +61 -12
- 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);
|
|
@@ -23758,9 +24357,9 @@ const useUIGroupStateController = (
|
|
|
23758
24357
|
const delegatedChildrenRef = useRef(new Map());
|
|
23759
24358
|
|
|
23760
24359
|
const groupIsRenderingRef = useRef(false);
|
|
23761
|
-
const pendingChangeRef = useRef(
|
|
24360
|
+
const pendingChangeRef = useRef(null);
|
|
23762
24361
|
groupIsRenderingRef.current = true;
|
|
23763
|
-
pendingChangeRef.current =
|
|
24362
|
+
pendingChangeRef.current = null;
|
|
23764
24363
|
|
|
23765
24364
|
const isMonitoringChild = (childUIStateController) => {
|
|
23766
24365
|
if (childUIStateController.isProxy) return false;
|
|
@@ -23792,7 +24391,18 @@ const useUIGroupStateController = (
|
|
|
23792
24391
|
// signals/pubsub without needing external refs.
|
|
23793
24392
|
const onChange = (e, { notifyExternal }) => {
|
|
23794
24393
|
if (groupIsRenderingRef.current) {
|
|
23795
|
-
|
|
24394
|
+
// Held until the layout effect below, WITH what it asked for: a child
|
|
24395
|
+
// whose bound signal was written from the outside changes during the
|
|
24396
|
+
// render that follows, and replaying that as a mount sync is what
|
|
24397
|
+
// makes a group silently drift — its own state comes up to date while
|
|
24398
|
+
// the form around it is never told anything moved. A real change
|
|
24399
|
+
// deferred alongside a mount sync stays a real change.
|
|
24400
|
+
const pendingChange = pendingChangeRef.current;
|
|
24401
|
+
pendingChangeRef.current = {
|
|
24402
|
+
e,
|
|
24403
|
+
notifyExternal:
|
|
24404
|
+
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
24405
|
+
};
|
|
23796
24406
|
return;
|
|
23797
24407
|
}
|
|
23798
24408
|
const aggChildState = resolvedAggregateChildStates(
|
|
@@ -24256,12 +24866,17 @@ const useUIGroupStateController = (
|
|
|
24256
24866
|
|
|
24257
24867
|
useLayoutEffect(() => {
|
|
24258
24868
|
groupIsRenderingRef.current = false;
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24263
|
-
{
|
|
24869
|
+
const pendingChange = pendingChangeRef.current;
|
|
24870
|
+
if (pendingChange) {
|
|
24871
|
+
pendingChangeRef.current = null;
|
|
24872
|
+
const batchedEvent = new CustomEvent(
|
|
24873
|
+
`${controlType}_batched_ui_state_update`,
|
|
24874
|
+
{ detail: {} },
|
|
24264
24875
|
);
|
|
24876
|
+
chainEvent(batchedEvent, pendingChange.e);
|
|
24877
|
+
scope._onChange(batchedEvent, {
|
|
24878
|
+
notifyExternal: pendingChange.notifyExternal,
|
|
24879
|
+
});
|
|
24265
24880
|
}
|
|
24266
24881
|
});
|
|
24267
24882
|
|
|
@@ -24322,20 +24937,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24322
24937
|
// not be treated as the picker's synced child.
|
|
24323
24938
|
return false;
|
|
24324
24939
|
}
|
|
24325
|
-
if (props.type === "form" && childController.controlType !== "form") {
|
|
24326
|
-
// Only a form: what a type="form" picker syncs with is the form in
|
|
24327
|
-
// its popup, not any control that happens to be in there (an input
|
|
24328
|
-
// sitting outside the form, a ControlGroup — which is a way of
|
|
24329
|
-
// grouping controls INSIDE a form, not a thing a picker talks to).
|
|
24330
|
-
return false;
|
|
24331
|
-
}
|
|
24332
|
-
if (
|
|
24333
|
-
props.type === "array" &&
|
|
24334
|
-
childController.controlType !== "checkbox_group"
|
|
24335
|
-
) {
|
|
24336
|
-
// only selectable list expose array, ignore others
|
|
24337
|
-
return false;
|
|
24338
|
-
}
|
|
24339
24940
|
return true;
|
|
24340
24941
|
};
|
|
24341
24942
|
|
|
@@ -24355,7 +24956,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24355
24956
|
`[navi] a second control ("${childType}"${child.name ? ` name="${child.name}"` : ""}) registered in the ${describePicker(props)} popup. ` +
|
|
24356
24957
|
`A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
|
|
24357
24958
|
`so this one is neither filled nor collected. ` +
|
|
24358
|
-
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="
|
|
24959
|
+
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object".`,
|
|
24359
24960
|
child,
|
|
24360
24961
|
);
|
|
24361
24962
|
} else {
|
|
@@ -28217,15 +28818,26 @@ const css$V = /* css */`
|
|
|
28217
28818
|
|
|
28218
28819
|
Capping the *size* here rather than only offsetting the position is
|
|
28219
28820
|
what makes a centered dialog follow the mobile virtual keyboard for
|
|
28220
|
-
free: --navi-
|
|
28221
|
-
reflows the dialog itself as the keyboard opens.
|
|
28222
|
-
|
|
28223
|
-
|
|
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. */
|
|
28224
28836
|
--dialog-maxmax-width: calc(
|
|
28225
|
-
var(--navi-
|
|
28837
|
+
var(--navi-app-width) - 2 * var(--x-dialog-container-spacing)
|
|
28226
28838
|
);
|
|
28227
28839
|
--dialog-maxmax-height: calc(
|
|
28228
|
-
var(--navi-
|
|
28840
|
+
var(--navi-app-height) - 2 * var(--x-dialog-container-spacing)
|
|
28229
28841
|
);
|
|
28230
28842
|
|
|
28231
28843
|
--dialog-border-radius: var(--navi-popup-border-radius);
|
|
@@ -28584,13 +29196,15 @@ const css$V = /* css */`
|
|
|
28584
29196
|
* touch device.
|
|
28585
29197
|
* @param {boolean} [props.expandY] - Same, vertically
|
|
28586
29198
|
* (`--dialog-maxmax-height`).
|
|
28587
|
-
* @param {string|number} [props.marginWithContainer="
|
|
29199
|
+
* @param {string|number} [props.marginWithContainer="3appw"] - Minimum gap kept
|
|
28588
29200
|
* between the dialog and the edges of its container, whatever its
|
|
28589
29201
|
* `positionArea`: it both caps the dialog's own size (via
|
|
28590
29202
|
* `--x-dialog-container-spacing`, written from this prop) and offsets a docked
|
|
28591
29203
|
* one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
|
|
28592
|
-
* number of pixels, or a viewport length — "
|
|
28593
|
-
* 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
|
|
28594
29208
|
* meant to sit flush (a side panel).
|
|
28595
29209
|
* @param {"close"|"cancel"|"capture"|"none"} [props.pointerInteractionOutsideEffect="close"]
|
|
28596
29210
|
* - `"close"` closes the dialog on an outside click. `"capture"`/`"none"`
|
|
@@ -28891,11 +29505,12 @@ const useDialogProps = props => {
|
|
|
28891
29505
|
const isDocked = dockedOnTouch && coarsePointerSignal.value;
|
|
28892
29506
|
const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
|
|
28893
29507
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
28894
|
-
// A share of whatever holds the dialog: the
|
|
28895
|
-
// one — where
|
|
28896
|
-
//
|
|
28897
|
-
//
|
|
28898
|
-
|
|
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");
|
|
28899
29514
|
// "expand || expandX", the shorthand semantics Popup used to apply before
|
|
28900
29515
|
// handing them over — the docked default only applies when neither was said
|
|
28901
29516
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
@@ -29132,7 +29747,7 @@ const useDialogProps = props => {
|
|
|
29132
29747
|
// A value only CSS could evaluate (a spacing token resolving to a var(),
|
|
29133
29748
|
// a percentage…) — the placement below needs a real number, and letting
|
|
29134
29749
|
// it through would put the dialog at NaN.
|
|
29135
|
-
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").`);
|
|
29136
29751
|
marginWithContainerInPixels = 0;
|
|
29137
29752
|
}
|
|
29138
29753
|
// The size caps read the same gap in CSS as the placement below applies
|
|
@@ -29559,8 +30174,10 @@ const css$U = /* css */`
|
|
|
29559
30174
|
rather than a value so an outer component can bridge its own prop into
|
|
29560
30175
|
--popover-max-height without having to restate 300px (see picker). */
|
|
29561
30176
|
--popover-max-height-default: 300px;
|
|
29562
|
-
--
|
|
29563
|
-
|
|
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));
|
|
29564
30181
|
|
|
29565
30182
|
--popover-box-shadow: var(--navi-popup-box-shadow);
|
|
29566
30183
|
--popover-border-radius: var(--navi-popup-border-radius);
|
|
@@ -32346,6 +32963,12 @@ const defaultRerunOn = {
|
|
|
32346
32963
|
GET: false,
|
|
32347
32964
|
GET_MANY: ["POST"],
|
|
32348
32965
|
};
|
|
32966
|
+
// What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
|
|
32967
|
+
// in there, unlike for GET_MANY: an action holds ids and the store drops the
|
|
32968
|
+
// deleted one out of every list holding it, while a list reading by slices
|
|
32969
|
+
// holds places — the row that left takes the ones after it one rank up, and
|
|
32970
|
+
// only the collection knows who fills the last one.
|
|
32971
|
+
const defaultInvalidateRangeOn = ["POST", "DELETE"];
|
|
32349
32972
|
|
|
32350
32973
|
// This handles ALL resource lifecycle logic (rerun/reset) across all resources
|
|
32351
32974
|
const createResourceLifecycleManager = () => {
|
|
@@ -32366,6 +32989,7 @@ const createResourceLifecycleManager = () => {
|
|
|
32366
32989
|
paramScope,
|
|
32367
32990
|
uniqueKeys,
|
|
32368
32991
|
restActionSet: new Set(),
|
|
32992
|
+
rangeReaderSet: new Set(),
|
|
32369
32993
|
});
|
|
32370
32994
|
|
|
32371
32995
|
// Register dependencies
|
|
@@ -32378,6 +33002,14 @@ const createResourceLifecycleManager = () => {
|
|
|
32378
33002
|
}
|
|
32379
33003
|
}
|
|
32380
33004
|
};
|
|
33005
|
+
// Only the reader the resource exposes is registered; the ones `bindParams`
|
|
33006
|
+
// makes share its signal (see resource_range_reader.js).
|
|
33007
|
+
const registerRangeReader = (resourceScope, rangeReader) => {
|
|
33008
|
+
const config = registeredResources.get(resourceScope);
|
|
33009
|
+
if (config) {
|
|
33010
|
+
config.rangeReaderSet.add(rangeReader);
|
|
33011
|
+
}
|
|
33012
|
+
};
|
|
32381
33013
|
const registerAction = (resourceScope, restAction) => {
|
|
32382
33014
|
const config = registeredResources.get(resourceScope);
|
|
32383
33015
|
if (config) {
|
|
@@ -32608,7 +33240,39 @@ const createResourceLifecycleManager = () => {
|
|
|
32608
33240
|
};
|
|
32609
33241
|
};
|
|
32610
33242
|
|
|
33243
|
+
// Which readers hold slices of a collection the mutation just changed: the
|
|
33244
|
+
// one of the resource itself, and the ones of the resources that said they
|
|
33245
|
+
// depend on it.
|
|
33246
|
+
const invalidateRangeReaders = (
|
|
33247
|
+
triggeringAction,
|
|
33248
|
+
triggeringActionContext,
|
|
33249
|
+
) => {
|
|
33250
|
+
const triggerVerb = triggeringAction.meta.verb;
|
|
33251
|
+
const triggerResourceScope = triggeringActionContext.resourceScope;
|
|
33252
|
+
for (const [resourceScope, config] of registeredResources) {
|
|
33253
|
+
if (config.rangeReaderSet.size === 0) {
|
|
33254
|
+
continue;
|
|
33255
|
+
}
|
|
33256
|
+
const isSameResource = resourceScope === triggerResourceScope;
|
|
33257
|
+
const isDependent = Boolean(
|
|
33258
|
+
triggerResourceScope &&
|
|
33259
|
+
resourceDependencies.get(triggerResourceScope)?.has(resourceScope),
|
|
33260
|
+
);
|
|
33261
|
+
if (!isSameResource && !isDependent) {
|
|
33262
|
+
continue;
|
|
33263
|
+
}
|
|
33264
|
+
const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
|
|
33265
|
+
if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
|
|
33266
|
+
continue;
|
|
33267
|
+
}
|
|
33268
|
+
for (const rangeReader of config.rangeReaderSet) {
|
|
33269
|
+
rangeReader.invalidate();
|
|
33270
|
+
}
|
|
33271
|
+
}
|
|
33272
|
+
};
|
|
33273
|
+
|
|
32611
33274
|
const onActionComplete = (restActionWhoJustCompleted, restActionContext) => {
|
|
33275
|
+
invalidateRangeReaders(restActionWhoJustCompleted, restActionContext);
|
|
32612
33276
|
const { actionsToRerun, actionsToReset, reasons } = findEffectOnActions(
|
|
32613
33277
|
restActionWhoJustCompleted,
|
|
32614
33278
|
restActionContext,
|
|
@@ -32627,6 +33291,7 @@ const createResourceLifecycleManager = () => {
|
|
|
32627
33291
|
return {
|
|
32628
33292
|
registerResource,
|
|
32629
33293
|
registerAction,
|
|
33294
|
+
registerRangeReader,
|
|
32630
33295
|
onActionComplete,
|
|
32631
33296
|
// Registers: when `triggerResource` fires, rerun `dependentResource`'s actions.
|
|
32632
33297
|
// Used by scopedMany to make the parent GET rerun when a child mutation completes.
|
|
@@ -32704,13 +33369,20 @@ const getParamScope = (params) => {
|
|
|
32704
33369
|
*
|
|
32705
33370
|
* The reader is a function, so a list feeds on it the way it feeds on any other
|
|
32706
33371
|
* source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
|
|
33372
|
+
*
|
|
33373
|
+
* Keeping nothing does not mean hearing nothing: a mutation that decides who
|
|
33374
|
+
* belongs to the collection (a POST, a DELETE, whatever `rerunOn.GET_RANGE`
|
|
33375
|
+
* says) bumps `invalidationSignal`, and whoever reads slices through it goes
|
|
33376
|
+
* and asks again — the counterpart, for a reader, of what a rerun is for an
|
|
33377
|
+
* action. Every reader made by `bindParams` shares the signal of the one it
|
|
33378
|
+
* comes from: the params say which slices are read, not which collection.
|
|
32707
33379
|
*/
|
|
32708
33380
|
|
|
32709
33381
|
|
|
32710
33382
|
const createRangeReader = (
|
|
32711
33383
|
actionName,
|
|
32712
33384
|
callback,
|
|
32713
|
-
{ store, params: boundParams },
|
|
33385
|
+
{ store, params: boundParams, invalidationSignal = signal(0) },
|
|
32714
33386
|
) => {
|
|
32715
33387
|
const readRange = async (range = {}) => {
|
|
32716
33388
|
const { signal, ...rangeParams } = range;
|
|
@@ -32739,10 +33411,17 @@ const createRangeReader = (
|
|
|
32739
33411
|
};
|
|
32740
33412
|
Object.defineProperty(readRange, "name", { value: actionName });
|
|
32741
33413
|
readRange.isRangeReader = true;
|
|
33414
|
+
// Bumped when the collection this reads has moved: the slices anyone holds
|
|
33415
|
+
// stand for a composition that is gone.
|
|
33416
|
+
readRange.invalidationSignal = invalidationSignal;
|
|
33417
|
+
readRange.invalidate = () => {
|
|
33418
|
+
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
33419
|
+
};
|
|
32742
33420
|
readRange.bindParams = (paramsToBind) => {
|
|
32743
33421
|
return createRangeReader(actionName, callback, {
|
|
32744
33422
|
store,
|
|
32745
33423
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
33424
|
+
invalidationSignal,
|
|
32746
33425
|
});
|
|
32747
33426
|
};
|
|
32748
33427
|
return readRange;
|
|
@@ -32809,9 +33488,11 @@ const debug$2 = (args) => {
|
|
|
32809
33488
|
* - GET_MANY / POST_MANY / … → an array of item objects
|
|
32810
33489
|
* - GET_RANGE → `{ items, start, count }`, one slice of the collection
|
|
32811
33490
|
*
|
|
32812
|
-
* `GET_RANGE` is a reader rather than an action: it keeps no value and
|
|
32813
|
-
*
|
|
32814
|
-
* (`itemsAction={USER.GET_RANGE.bindParams({ team })}`).
|
|
33491
|
+
* `GET_RANGE` is a reader rather than an action: it keeps no value and has nothing to
|
|
33492
|
+
* rerun, so a `<List.Items>` can feed on it slice by slice
|
|
33493
|
+
* (`itemsAction={USER.GET_RANGE.bindParams({ team })}`). A mutation listed in
|
|
33494
|
+
* `rerunOn.GET_RANGE` (`["POST", "DELETE"]` by default) tells it the collection moved,
|
|
33495
|
+
* and whoever holds slices reads them again.
|
|
32815
33496
|
*
|
|
32816
33497
|
* A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
|
|
32817
33498
|
* relationship method, never as an `op`/`type` discriminator dispatched inside one
|
|
@@ -33991,13 +34672,18 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
33991
34672
|
continue;
|
|
33992
34673
|
}
|
|
33993
34674
|
if (restCallbackKey === "GET_RANGE") {
|
|
33994
|
-
// A range is read, never kept: no action,
|
|
34675
|
+
// A range is read, never kept: no action, nothing to rerun — only a
|
|
34676
|
+
// signal saying the slices anyone holds are out of date
|
|
33995
34677
|
// (see resource_range_reader.js).
|
|
33996
34678
|
stateFacade.GET_RANGE = createRangeReader(
|
|
33997
34679
|
`${name}.GET_RANGE`,
|
|
33998
34680
|
restCallback,
|
|
33999
34681
|
{ store, params },
|
|
34000
34682
|
);
|
|
34683
|
+
resourceLifecycleManager.registerRangeReader(
|
|
34684
|
+
stateFacade,
|
|
34685
|
+
stateFacade.GET_RANGE,
|
|
34686
|
+
);
|
|
34001
34687
|
continue;
|
|
34002
34688
|
}
|
|
34003
34689
|
const isMany = restCallbackKey.endsWith("_MANY");
|
|
@@ -35970,701 +36656,143 @@ const UITransition = ({
|
|
|
35970
36656
|
alignX,
|
|
35971
36657
|
alignY,
|
|
35972
36658
|
...props
|
|
35973
|
-
}) => {
|
|
35974
|
-
const contentIdRef = useRef(contentId);
|
|
35975
|
-
const updateContentId = () => {
|
|
35976
|
-
const uiTransition = uiTransitionRef.current;
|
|
35977
|
-
if (!uiTransition) {
|
|
35978
|
-
return;
|
|
35979
|
-
}
|
|
35980
|
-
const value = contentIdRef.current;
|
|
35981
|
-
uiTransition.updateContentId(value);
|
|
35982
|
-
};
|
|
35983
|
-
const uiTransitionContentIdContextValue = useMemo(() => {
|
|
35984
|
-
const set = new Set();
|
|
35985
|
-
const onSetChange = () => {
|
|
35986
|
-
const value = Array.from(set).join("|");
|
|
35987
|
-
contentIdRef.current = value;
|
|
35988
|
-
updateContentId();
|
|
35989
|
-
};
|
|
35990
|
-
const update = (part, newPart) => {
|
|
35991
|
-
if (!set.has(part)) {
|
|
35992
|
-
if (set.size === 0) {
|
|
35993
|
-
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id set is empty`);
|
|
35994
|
-
return;
|
|
35995
|
-
}
|
|
35996
|
-
console.warn(`UITransition: content id update "${part}" -> "${newPart}" ignored because content id not found in set, only got [${Array.from(set).join(", ")}]`);
|
|
35997
|
-
return;
|
|
35998
|
-
}
|
|
35999
|
-
set.delete(part);
|
|
36000
|
-
set.add(newPart);
|
|
36001
|
-
onSetChange();
|
|
36002
|
-
};
|
|
36003
|
-
const add = part => {
|
|
36004
|
-
if (!part) {
|
|
36005
|
-
return;
|
|
36006
|
-
}
|
|
36007
|
-
if (set.has(part)) {
|
|
36008
|
-
return;
|
|
36009
|
-
}
|
|
36010
|
-
set.add(part);
|
|
36011
|
-
onSetChange();
|
|
36012
|
-
};
|
|
36013
|
-
const remove = part => {
|
|
36014
|
-
if (!part) {
|
|
36015
|
-
return;
|
|
36016
|
-
}
|
|
36017
|
-
if (!set.has(part)) {
|
|
36018
|
-
return;
|
|
36019
|
-
}
|
|
36020
|
-
set.delete(part);
|
|
36021
|
-
onSetChange();
|
|
36022
|
-
};
|
|
36023
|
-
return {
|
|
36024
|
-
add,
|
|
36025
|
-
update,
|
|
36026
|
-
remove
|
|
36027
|
-
};
|
|
36028
|
-
}, []);
|
|
36029
|
-
const ref = useRef();
|
|
36030
|
-
const uiTransitionRefDefault = useRef();
|
|
36031
|
-
uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
|
|
36032
|
-
useLayoutEffect(() => {
|
|
36033
|
-
const uiTransition = createUITransitionController(ref.current, {
|
|
36034
|
-
alignX,
|
|
36035
|
-
alignY
|
|
36036
|
-
});
|
|
36037
|
-
uiTransitionRef.current = uiTransition;
|
|
36038
|
-
return () => {
|
|
36039
|
-
uiTransition.cleanup();
|
|
36040
|
-
};
|
|
36041
|
-
}, [disabled, alignX, alignY]);
|
|
36042
|
-
return jsxs("div", {
|
|
36043
|
-
ref: ref,
|
|
36044
|
-
...props,
|
|
36045
|
-
className: "ui_transition",
|
|
36046
|
-
"data-disabled": disabled ? "" : undefined,
|
|
36047
|
-
"data-transition-type": type,
|
|
36048
|
-
"data-transition-duration": duration,
|
|
36049
|
-
"data-debug-detection": debugDetection ? "" : undefined,
|
|
36050
|
-
"data-debug-size": debugSize ? "" : undefined,
|
|
36051
|
-
"data-debug-content": debugContent ? "" : undefined,
|
|
36052
|
-
children: [jsxs("div", {
|
|
36053
|
-
className: "ui_transition_active_group",
|
|
36054
|
-
children: [jsx("div", {
|
|
36055
|
-
className: "ui_transition_target_slot",
|
|
36056
|
-
"data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
|
|
36057
|
-
children: jsx(UITransitionContentIdContext.Provider, {
|
|
36058
|
-
value: uiTransitionContentIdContextValue,
|
|
36059
|
-
children: children
|
|
36060
|
-
})
|
|
36061
|
-
}), jsx("div", {
|
|
36062
|
-
className: "ui_transition_outgoing_slot",
|
|
36063
|
-
inert: true
|
|
36064
|
-
})]
|
|
36065
|
-
}), jsxs("div", {
|
|
36066
|
-
className: "ui_transition_previous_group",
|
|
36067
|
-
inert: true,
|
|
36068
|
-
children: [jsx("div", {
|
|
36069
|
-
className: "ui_transition_previous_target_slot"
|
|
36070
|
-
}), jsx("div", {
|
|
36071
|
-
className: "ui_transition_previous_outgoing_slot"
|
|
36072
|
-
})]
|
|
36073
|
-
})]
|
|
36074
|
-
});
|
|
36075
|
-
};
|
|
36076
|
-
|
|
36077
|
-
/**
|
|
36078
|
-
* The goal of this hook is to allow a component to set a "content key"
|
|
36079
|
-
* Meaning all content within the component is identified by that key
|
|
36080
|
-
*
|
|
36081
|
-
* When the key changes, UITransition will be able to detect that and consider the content
|
|
36082
|
-
* as changed even if the component is still the same
|
|
36083
|
-
*
|
|
36084
|
-
* This is used by <Route> to set the content key to the route path
|
|
36085
|
-
* When the route becomes inactive it will call useUITransitionContentId(undefined)
|
|
36086
|
-
* And if a sibling route becones active it will call useUITransitionContentId with its own path
|
|
36087
|
-
*
|
|
36088
|
-
*/
|
|
36089
|
-
const useUITransitionContentId = value => {
|
|
36090
|
-
const contentId = useContext(UITransitionContentIdContext);
|
|
36091
|
-
const valueRef = useRef();
|
|
36092
|
-
if (contentId !== undefined && valueRef.current !== value) {
|
|
36093
|
-
const previousValue = valueRef.current;
|
|
36094
|
-
valueRef.current = value;
|
|
36095
|
-
if (previousValue === undefined) {
|
|
36096
|
-
contentId.add(value);
|
|
36097
|
-
} else {
|
|
36098
|
-
contentId.update(previousValue, value);
|
|
36099
|
-
}
|
|
36100
|
-
}
|
|
36101
|
-
useLayoutEffect(() => {
|
|
36102
|
-
if (contentId === undefined) {
|
|
36103
|
-
return null;
|
|
36104
|
-
}
|
|
36105
|
-
return () => {
|
|
36106
|
-
contentId.remove(valueRef.current);
|
|
36107
|
-
};
|
|
36108
|
-
}, []);
|
|
36109
|
-
};
|
|
36110
|
-
|
|
36111
|
-
const documentStateSignal = signal(null);
|
|
36112
|
-
const useDocumentState = () => {
|
|
36113
|
-
return documentStateSignal.value;
|
|
36114
|
-
};
|
|
36115
|
-
const updateDocumentState = (value) => {
|
|
36116
|
-
documentStateSignal.value = value;
|
|
36117
|
-
};
|
|
36118
|
-
|
|
36119
|
-
/**
|
|
36120
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
36121
|
-
*
|
|
36122
|
-
* Everything else a router says arrives once the change is made: a route
|
|
36123
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
36124
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
36125
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
36126
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
36127
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
36128
|
-
*
|
|
36129
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
36130
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
36131
|
-
* before the URL, the visited set, or any route has changed.
|
|
36132
|
-
*
|
|
36133
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
36134
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
36135
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
36136
|
-
*/
|
|
36137
|
-
|
|
36138
|
-
|
|
36139
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
36140
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
36141
|
-
|
|
36142
|
-
const setupBrowserIntegrationViaHistory = ({
|
|
36143
|
-
applyActions,
|
|
36144
|
-
applyRouting,
|
|
36145
|
-
isRouting,
|
|
36146
|
-
}) => {
|
|
36147
|
-
const { history } = window;
|
|
36148
|
-
|
|
36149
|
-
let globalAbortController = new AbortController();
|
|
36150
|
-
const triggerGlobalAbort = (reason) => {
|
|
36151
|
-
globalAbortController.abort(reason);
|
|
36152
|
-
globalAbortController = new AbortController();
|
|
36153
|
-
};
|
|
36154
|
-
|
|
36155
|
-
const dispatchActions = (params) => {
|
|
36156
|
-
const { requestedResult } = applyActions({
|
|
36157
|
-
globalAbortSignal: globalAbortController.signal,
|
|
36158
|
-
abortSignal: new AbortController().signal,
|
|
36159
|
-
...params,
|
|
36160
|
-
});
|
|
36161
|
-
return requestedResult;
|
|
36162
|
-
};
|
|
36163
|
-
setActionDispatcher(dispatchActions);
|
|
36164
|
-
|
|
36165
|
-
const getDocumentState = () => {
|
|
36166
|
-
return window.history.state ? { ...window.history.state } : null;
|
|
36167
|
-
};
|
|
36168
|
-
|
|
36169
|
-
const historyStartAtStart = getDocumentState();
|
|
36170
|
-
const visitedUrlSet = historyStartAtStart
|
|
36171
|
-
? new Set(historyStartAtStart.jsenv_visited_urls || [])
|
|
36172
|
-
: new Set();
|
|
36173
|
-
|
|
36174
|
-
// Create a signal that tracks visited URLs for reactive updates
|
|
36175
|
-
// Using a counter instead of the Set directly for better performance
|
|
36176
|
-
// Links will check isVisited() when this signal changes
|
|
36177
|
-
const visitedUrlsSignal = signal(0);
|
|
36178
|
-
|
|
36179
|
-
const isVisited = (url) => {
|
|
36180
|
-
url = new URL(url, window.location.href).href;
|
|
36181
|
-
return visitedUrlSet.has(url);
|
|
36182
|
-
};
|
|
36183
|
-
const markUrlAsVisited = (url) => {
|
|
36184
|
-
if (visitedUrlSet.has(url)) {
|
|
36185
|
-
return;
|
|
36186
|
-
}
|
|
36187
|
-
visitedUrlSet.add(url);
|
|
36188
|
-
visitedUrlsSignal.value++;
|
|
36189
|
-
};
|
|
36190
|
-
|
|
36191
|
-
let abortController = null;
|
|
36192
|
-
const handleRoutingTask = (url, options) => {
|
|
36193
|
-
// Before anything is written: the visited set, the URL and every route are
|
|
36194
|
-
// about to change, and this is the last moment the page still stands as it
|
|
36195
|
-
// was. And after, whichever way the change went out — so that whoever took
|
|
36196
|
-
// something at the first announcement has a definite place to give it back.
|
|
36197
|
-
publishBeforeRouting({ url, ...options });
|
|
36198
|
-
try {
|
|
36199
|
-
return applyRoutingTask(url, options);
|
|
36200
|
-
} finally {
|
|
36201
|
-
publishAfterRouting({ url, ...options });
|
|
36202
|
-
}
|
|
36203
|
-
};
|
|
36204
|
-
|
|
36205
|
-
const applyRoutingTask = (url, options) => {
|
|
36206
|
-
const isSameUrl = url === window.location.href;
|
|
36207
|
-
const {
|
|
36208
|
-
reason,
|
|
36209
|
-
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
36210
|
-
state,
|
|
36211
|
-
} = options;
|
|
36212
|
-
|
|
36213
|
-
if (navigationType === "push" || navigationType === "replace") {
|
|
36214
|
-
markUrlAsVisited(url);
|
|
36215
|
-
// undefined → inherit current state (link click, neutral navigation)
|
|
36216
|
-
// null → explicit reset (no nav-state keys carried over)
|
|
36217
|
-
// {...} → explicit state from enter()/leave(), already built from currentState
|
|
36218
|
-
// 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)
|
|
36219
|
-
let effectiveState;
|
|
36220
|
-
const sharedState = {
|
|
36221
|
-
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
36222
|
-
};
|
|
36223
|
-
if (state === undefined) {
|
|
36224
|
-
effectiveState = {
|
|
36225
|
-
...(getDocumentState() || {}),
|
|
36226
|
-
...sharedState,
|
|
36227
|
-
};
|
|
36228
|
-
} else if (state === null) {
|
|
36229
|
-
effectiveState = sharedState;
|
|
36230
|
-
} else if (state) {
|
|
36231
|
-
effectiveState = {
|
|
36232
|
-
...state,
|
|
36233
|
-
...sharedState,
|
|
36234
|
-
};
|
|
36235
|
-
}
|
|
36236
|
-
if (navigationType === "push") {
|
|
36237
|
-
window.history.pushState(effectiveState, null, url);
|
|
36238
|
-
} else {
|
|
36239
|
-
window.history.replaceState(effectiveState, null, url);
|
|
36240
|
-
}
|
|
36241
|
-
updateDocumentUrl(url);
|
|
36242
|
-
updateDocumentState(effectiveState);
|
|
36243
|
-
} else {
|
|
36244
|
-
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
36245
|
-
markUrlAsVisited(url);
|
|
36246
|
-
updateDocumentUrl(url);
|
|
36247
|
-
updateDocumentState(state);
|
|
36248
|
-
}
|
|
36249
|
-
|
|
36250
|
-
// Skip route matching for state-only changes: push/replace to the same URL
|
|
36251
|
-
// (e.g. useNavState updating document state without changing the route).
|
|
36252
|
-
// Do NOT apply for "traverse" — window.location.href is already updated by
|
|
36253
|
-
// the browser before the popstate handler runs, so isSameUrl is always true
|
|
36254
|
-
// for back/forward navigation regardless of whether the URL actually changed.
|
|
36255
|
-
if (
|
|
36256
|
-
isSameUrl &&
|
|
36257
|
-
(navigationType === "push" || navigationType === "replace")
|
|
36258
|
-
) {
|
|
36259
|
-
return undefined;
|
|
36260
|
-
}
|
|
36261
|
-
|
|
36262
|
-
if (abortController) {
|
|
36263
|
-
abortController.abort(`navigating to ${url}`);
|
|
36264
|
-
}
|
|
36265
|
-
abortController = new AbortController();
|
|
36266
|
-
const abortSignal = abortController.signal;
|
|
36267
|
-
const { allResult, requestedResult } = applyRouting(url, {
|
|
36268
|
-
globalAbortSignal: globalAbortController.signal,
|
|
36269
|
-
abortSignal,
|
|
36270
|
-
reason,
|
|
36271
|
-
navigationType,
|
|
36272
|
-
isVisited,
|
|
36273
|
-
state,
|
|
36274
|
-
});
|
|
36275
|
-
executeWithCleanup(
|
|
36276
|
-
() => allResult,
|
|
36277
|
-
() => {
|
|
36278
|
-
abortController = undefined;
|
|
36279
|
-
},
|
|
36280
|
-
);
|
|
36281
|
-
return requestedResult;
|
|
36659
|
+
}) => {
|
|
36660
|
+
const contentIdRef = useRef(contentId);
|
|
36661
|
+
const updateContentId = () => {
|
|
36662
|
+
const uiTransition = uiTransitionRef.current;
|
|
36663
|
+
if (!uiTransition) {
|
|
36664
|
+
return;
|
|
36665
|
+
}
|
|
36666
|
+
const value = contentIdRef.current;
|
|
36667
|
+
uiTransition.updateContentId(value);
|
|
36282
36668
|
};
|
|
36283
|
-
|
|
36284
|
-
|
|
36285
|
-
|
|
36286
|
-
|
|
36287
|
-
|
|
36288
|
-
|
|
36289
|
-
|
|
36290
|
-
|
|
36291
|
-
|
|
36292
|
-
|
|
36293
|
-
|
|
36294
|
-
|
|
36295
|
-
|
|
36296
|
-
|
|
36297
|
-
return;
|
|
36298
|
-
}
|
|
36299
|
-
const linkElement = e.target.closest("a");
|
|
36300
|
-
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(", ")}]`);
|
|
36301
36683
|
return;
|
|
36302
36684
|
}
|
|
36303
|
-
|
|
36685
|
+
set.delete(part);
|
|
36686
|
+
set.add(newPart);
|
|
36687
|
+
onSetChange();
|
|
36688
|
+
};
|
|
36689
|
+
const add = part => {
|
|
36690
|
+
if (!part) {
|
|
36304
36691
|
return;
|
|
36305
36692
|
}
|
|
36306
|
-
|
|
36307
|
-
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
36308
|
-
getHrefTargetInfo(href);
|
|
36309
|
-
if (isEmpty || !isSameOrigin) {
|
|
36310
|
-
// Let link to other origins be handled by the browser
|
|
36693
|
+
if (set.has(part)) {
|
|
36311
36694
|
return;
|
|
36312
36695
|
}
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
|
|
36316
|
-
|
|
36317
|
-
|
|
36318
|
-
// Except this one, which the browser answers with a scroll and
|
|
36319
|
-
// nothing else: same pathname, same hash, so no event and no url
|
|
36320
|
-
// change reaches whoever is waiting on the designated element.
|
|
36321
|
-
rearmUrlTarget();
|
|
36322
|
-
}
|
|
36696
|
+
set.add(part);
|
|
36697
|
+
onSetChange();
|
|
36698
|
+
};
|
|
36699
|
+
const remove = part => {
|
|
36700
|
+
if (!part) {
|
|
36323
36701
|
return;
|
|
36324
36702
|
}
|
|
36325
|
-
|
|
36326
|
-
// page is a plain document and a link in it is a plain link. Taking it
|
|
36327
|
-
// over anyway would push the url and then have nothing to show for it —
|
|
36328
|
-
// the address bar moves and the page does not (see applyRouting's own
|
|
36329
|
-
// "not called yet" branch, which is where that used to end up).
|
|
36330
|
-
if (!isRouting()) {
|
|
36703
|
+
if (!set.has(part)) {
|
|
36331
36704
|
return;
|
|
36332
36705
|
}
|
|
36333
|
-
|
|
36334
|
-
|
|
36335
|
-
|
|
36336
|
-
navigationType: "push",
|
|
36337
|
-
});
|
|
36338
|
-
},
|
|
36339
|
-
{ capture: true },
|
|
36340
|
-
);
|
|
36341
|
-
|
|
36342
|
-
window.addEventListener(
|
|
36343
|
-
"submit",
|
|
36344
|
-
() => {
|
|
36345
|
-
// Handle form submissions?
|
|
36346
|
-
// Not needed yet
|
|
36347
|
-
},
|
|
36348
|
-
{ capture: true },
|
|
36349
|
-
);
|
|
36350
|
-
|
|
36351
|
-
window.addEventListener("popstate", (popstateEvent) => {
|
|
36352
|
-
const url = window.location.href;
|
|
36353
|
-
const state = popstateEvent.state;
|
|
36354
|
-
handleRoutingTask(url, {
|
|
36355
|
-
reason: `"popstate" event for ${url}`,
|
|
36356
|
-
navigationType: "traverse",
|
|
36357
|
-
state,
|
|
36358
|
-
});
|
|
36359
|
-
});
|
|
36360
|
-
|
|
36361
|
-
// A fragment navigation is left to the browser (see the click handler above):
|
|
36362
|
-
// it owns the indicated part of the document, and taking it over would cost
|
|
36363
|
-
// `:target` and the focus handling that come with it. The document url still
|
|
36364
|
-
// has to follow it — nothing else here would notice that it moved.
|
|
36365
|
-
window.addEventListener("hashchange", () => {
|
|
36366
|
-
updateDocumentUrl(window.location.href);
|
|
36367
|
-
});
|
|
36368
|
-
|
|
36369
|
-
const navTo = async (url, { replace, state } = {}) => {
|
|
36370
|
-
handleRoutingTask(url, {
|
|
36371
|
-
reason: `navTo called with "${url}"`,
|
|
36372
|
-
navigationType: replace ? "replace" : "push",
|
|
36373
|
-
state,
|
|
36374
|
-
});
|
|
36375
|
-
};
|
|
36376
|
-
|
|
36377
|
-
const stop = (reason = "stop called") => {
|
|
36378
|
-
triggerGlobalAbort(reason);
|
|
36379
|
-
};
|
|
36380
|
-
|
|
36381
|
-
const reload = () => {
|
|
36382
|
-
const url = window.location.href;
|
|
36383
|
-
const state = history.state;
|
|
36384
|
-
handleRoutingTask(url, {
|
|
36385
|
-
reason: "reload called",
|
|
36386
|
-
navigationType: "reload",
|
|
36387
|
-
state,
|
|
36388
|
-
});
|
|
36389
|
-
};
|
|
36390
|
-
|
|
36391
|
-
const navBack = () => {
|
|
36392
|
-
window.history.back();
|
|
36393
|
-
};
|
|
36394
|
-
|
|
36395
|
-
const navForward = () => {
|
|
36396
|
-
window.history.forward();
|
|
36397
|
-
};
|
|
36398
|
-
|
|
36399
|
-
const init = () => {
|
|
36400
|
-
const url = window.location.href;
|
|
36401
|
-
const state = history.state;
|
|
36402
|
-
handleRoutingTask(url, {
|
|
36403
|
-
reason: "routing initialization",
|
|
36404
|
-
navigationType: "load",
|
|
36405
|
-
state,
|
|
36406
|
-
});
|
|
36407
|
-
};
|
|
36408
|
-
|
|
36409
|
-
return {
|
|
36410
|
-
integration: "browser_history_api",
|
|
36411
|
-
init,
|
|
36412
|
-
navTo,
|
|
36413
|
-
stop,
|
|
36414
|
-
reload,
|
|
36415
|
-
navBack,
|
|
36416
|
-
navForward,
|
|
36417
|
-
getDocumentState,
|
|
36418
|
-
isVisited,
|
|
36419
|
-
visitedUrlsSignal,
|
|
36420
|
-
};
|
|
36421
|
-
};
|
|
36422
|
-
|
|
36423
|
-
let updateRoutes;
|
|
36424
|
-
|
|
36425
|
-
const applyActions = (params) => {
|
|
36426
|
-
const updateActionsResult = updateActions(params);
|
|
36427
|
-
const { allResult, runningActionSet } = updateActionsResult;
|
|
36428
|
-
const pendingTaskNameArray = [];
|
|
36429
|
-
for (const runningAction of runningActionSet) {
|
|
36430
|
-
pendingTaskNameArray.push(runningAction.name);
|
|
36431
|
-
}
|
|
36432
|
-
workingWhile(() => allResult, pendingTaskNameArray);
|
|
36433
|
-
return updateActionsResult;
|
|
36434
|
-
};
|
|
36435
|
-
const applyRouting = (
|
|
36436
|
-
url,
|
|
36437
|
-
{
|
|
36438
|
-
globalAbortSignal,
|
|
36439
|
-
abortSignal,
|
|
36440
|
-
// state
|
|
36441
|
-
navigationType,
|
|
36442
|
-
isVisited,
|
|
36443
|
-
reason,
|
|
36444
|
-
},
|
|
36445
|
-
) => {
|
|
36446
|
-
if (!updateRoutes) {
|
|
36447
|
-
// .init() not called yet
|
|
36448
|
-
// likely because code does not uses routing at all
|
|
36449
|
-
return {};
|
|
36450
|
-
}
|
|
36451
|
-
const {
|
|
36452
|
-
loadSet,
|
|
36453
|
-
reloadSet,
|
|
36454
|
-
abortSignalMap,
|
|
36455
|
-
routeLoadRequestedMap,
|
|
36456
|
-
activeRouteSet,
|
|
36457
|
-
} = updateRoutes(url, {
|
|
36458
|
-
navigationType,
|
|
36459
|
-
isVisited,
|
|
36460
|
-
// state,
|
|
36461
|
-
});
|
|
36462
|
-
if (
|
|
36463
|
-
(!loadSet || loadSet.size === 0) &&
|
|
36464
|
-
(!reloadSet || reloadSet.size === 0)
|
|
36465
|
-
) {
|
|
36706
|
+
set.delete(part);
|
|
36707
|
+
onSetChange();
|
|
36708
|
+
};
|
|
36466
36709
|
return {
|
|
36467
|
-
|
|
36468
|
-
|
|
36469
|
-
|
|
36710
|
+
add,
|
|
36711
|
+
update,
|
|
36712
|
+
remove
|
|
36470
36713
|
};
|
|
36471
|
-
}
|
|
36472
|
-
const
|
|
36473
|
-
|
|
36474
|
-
|
|
36475
|
-
|
|
36476
|
-
|
|
36477
|
-
|
|
36478
|
-
|
|
36479
|
-
isReplace: navigationType === "replace",
|
|
36480
|
-
});
|
|
36481
|
-
const { allResult, runningActionSet } = updateActionsResult;
|
|
36482
|
-
const pendingTaskNameArray = [];
|
|
36483
|
-
for (const [route, routeAction] of routeLoadRequestedMap) {
|
|
36484
|
-
if (runningActionSet.has(routeAction)) {
|
|
36485
|
-
pendingTaskNameArray.push(`${route.relativeUrl} -> ${routeAction.name}`);
|
|
36486
|
-
}
|
|
36487
|
-
}
|
|
36488
|
-
routingWhile(() => allResult, pendingTaskNameArray);
|
|
36489
|
-
return { ...updateActionsResult, activeRouteSet };
|
|
36490
|
-
};
|
|
36491
|
-
|
|
36492
|
-
const browserIntegration = setupBrowserIntegrationViaHistory({
|
|
36493
|
-
applyActions,
|
|
36494
|
-
applyRouting,
|
|
36495
|
-
// Routes are declared by the consumer and registered through
|
|
36496
|
-
// setOnAllRouteReady below, so "does this document route at all?" is only
|
|
36497
|
-
// answerable once that has run — hence a function, read at click time rather
|
|
36498
|
-
// than a value read at setup time.
|
|
36499
|
-
isRouting: () => Boolean(updateRoutes),
|
|
36500
|
-
});
|
|
36501
|
-
|
|
36502
|
-
setOnAllRouteReady((v) => {
|
|
36503
|
-
updateRoutes = v;
|
|
36504
|
-
browserIntegration.init();
|
|
36505
|
-
});
|
|
36506
|
-
setRouteIntegration(browserIntegration);
|
|
36507
|
-
|
|
36508
|
-
const navIntegratedVia = browserIntegration.integration;
|
|
36509
|
-
const navTo = (target, options) => {
|
|
36510
|
-
const url = new URL(target, window.location.href).href;
|
|
36511
|
-
const currentUrl = documentUrlSignal.peek();
|
|
36512
|
-
if (url === currentUrl) {
|
|
36513
|
-
if (options?.state === undefined) {
|
|
36514
|
-
return null;
|
|
36515
|
-
}
|
|
36516
|
-
// State-only update on same URL: skip if state is identical to current.
|
|
36517
|
-
const currentState = browserIntegration.getDocumentState();
|
|
36518
|
-
if (compareTwoJsValues(options.state, currentState)) {
|
|
36519
|
-
return null;
|
|
36520
|
-
}
|
|
36521
|
-
}
|
|
36522
|
-
return browserIntegration.navTo(url, options);
|
|
36523
|
-
};
|
|
36524
|
-
const stopLoad = (reason = "stopLoad() called") => {
|
|
36525
|
-
const windowIsLoading = windowIsLoadingSignal.value;
|
|
36526
|
-
if (windowIsLoading) {
|
|
36527
|
-
window.stop();
|
|
36528
|
-
}
|
|
36529
|
-
const documentIsBusy = documentIsBusySignal.value;
|
|
36530
|
-
if (documentIsBusy) {
|
|
36531
|
-
browserIntegration.stop(reason);
|
|
36532
|
-
}
|
|
36533
|
-
};
|
|
36534
|
-
const reload = browserIntegration.reload;
|
|
36535
|
-
const navBack = browserIntegration.navBack;
|
|
36536
|
-
const navForward = browserIntegration.navForward;
|
|
36537
|
-
const isVisited = browserIntegration.isVisited;
|
|
36538
|
-
const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
|
|
36539
|
-
browserIntegration.handleActionTask;
|
|
36540
|
-
|
|
36541
|
-
// Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where
|
|
36542
|
-
// the mask is derived from render order within the nearest root/async
|
|
36543
|
-
// boundary — stable across re-renders of the *same* mount, but not across a
|
|
36544
|
-
// reload (render order can differ) or even across two mounts on the same
|
|
36545
|
-
// page (two components hitting useId() in the same relative order get the
|
|
36546
|
-
// same string). Storing one of these under type: "push" bakes it into a
|
|
36547
|
-
// history entry: reload the page and the entry's key may now belong to a
|
|
36548
|
-
// completely different component (or none), silently auto-opening whatever
|
|
36549
|
-
// happens to render at that same position instead.
|
|
36550
|
-
const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
|
|
36551
|
-
const isLikelyPreactGeneratedId = (id) => PREACT_GENERATED_ID_REGEX.test(id);
|
|
36552
|
-
|
|
36553
|
-
const NO_OP = () => {};
|
|
36554
|
-
const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
|
|
36555
|
-
const useNavStateBasic = (
|
|
36556
|
-
id,
|
|
36557
|
-
{ debug, type = "replace", onLeave, defaultValue } = {},
|
|
36558
|
-
) => {
|
|
36559
|
-
// Hooks must be called unconditionally — before the !id early return.
|
|
36560
|
-
const state = documentStateSignal.value;
|
|
36561
|
-
// Key presence is the flag — the value may be anything, including undefined.
|
|
36562
|
-
const keyInState = Boolean(id && state && Object.hasOwn(state, id));
|
|
36563
|
-
const onLeaveRef = useRef(onLeave);
|
|
36564
|
-
onLeaveRef.current = onLeave;
|
|
36565
|
-
const prevKeyInStateRef = useRef(keyInState);
|
|
36566
|
-
// enteredRef tracks whether enter() was called without a matching leave() yet.
|
|
36567
|
-
// It lets the effect distinguish an external disappearance (back button → fire onLeave)
|
|
36568
|
-
// from a programmatic one (leave() already set it to false before the state updates).
|
|
36569
|
-
const enteredRef = useRef(false);
|
|
36570
|
-
useEffect(() => {
|
|
36571
|
-
const prevKeyInState = prevKeyInStateRef.current;
|
|
36572
|
-
prevKeyInStateRef.current = keyInState;
|
|
36573
|
-
if (prevKeyInState && !keyInState && enteredRef.current) {
|
|
36574
|
-
enteredRef.current = false;
|
|
36575
|
-
onLeaveRef.current?.();
|
|
36576
|
-
}
|
|
36577
|
-
}, [keyInState]);
|
|
36578
|
-
|
|
36579
|
-
if (!id) {
|
|
36580
|
-
return NO_ID_GIVEN;
|
|
36581
|
-
}
|
|
36582
|
-
|
|
36583
|
-
let effectiveType = type;
|
|
36584
|
-
if (type === "push" && isLikelyPreactGeneratedId(id)) {
|
|
36585
|
-
effectiveType = "replace";
|
|
36586
|
-
}
|
|
36587
|
-
|
|
36588
|
-
const currentValue = keyInState ? state[id] : defaultValue;
|
|
36589
|
-
|
|
36590
|
-
if (debug) {
|
|
36591
|
-
console.debug(`useNavState(${id}) current value is ${currentValue}`);
|
|
36592
|
-
}
|
|
36593
|
-
|
|
36594
|
-
// enter(value): navigate TO this state (push or replace depending on type).
|
|
36595
|
-
// Calling enter() without a value stores "on" — the mere presence of the key
|
|
36596
|
-
// in the document state is enough to match; the value just allows associating
|
|
36597
|
-
// extra data with the entry when needed.
|
|
36598
|
-
const enter = (value = "on") => {
|
|
36599
|
-
enteredRef.current = true;
|
|
36600
|
-
const currentStateCopy = browserIntegration.getDocumentState() || {};
|
|
36601
|
-
if (Object.hasOwn(currentStateCopy, id) && currentStateCopy[id] === value) {
|
|
36602
|
-
return;
|
|
36603
|
-
}
|
|
36604
|
-
currentStateCopy[id] = value;
|
|
36605
|
-
navTo(window.location.href, {
|
|
36606
|
-
replace: effectiveType !== "push",
|
|
36607
|
-
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
|
|
36608
36722
|
});
|
|
36609
|
-
|
|
36610
|
-
|
|
36611
|
-
|
|
36612
|
-
|
|
36613
|
-
|
|
36614
|
-
|
|
36615
|
-
|
|
36616
|
-
|
|
36617
|
-
|
|
36618
|
-
|
|
36619
|
-
|
|
36620
|
-
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
|
|
36626
|
-
|
|
36627
|
-
|
|
36628
|
-
|
|
36629
|
-
|
|
36630
|
-
|
|
36631
|
-
|
|
36632
|
-
|
|
36633
|
-
|
|
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
|
+
});
|
|
36634
36761
|
};
|
|
36635
36762
|
|
|
36636
36763
|
/**
|
|
36637
|
-
*
|
|
36638
|
-
*
|
|
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
|
|
36639
36766
|
*
|
|
36640
|
-
*
|
|
36641
|
-
*
|
|
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
|
|
36642
36769
|
*
|
|
36643
|
-
*
|
|
36644
|
-
*
|
|
36645
|
-
*
|
|
36646
|
-
* - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
|
|
36647
|
-
* - "replace": updates the current history entry — no extra history entry is created.
|
|
36648
|
-
* Silently downgraded to "replace" (with a dev-only console.warn) when `id`
|
|
36649
|
-
* looks auto-generated (e.g. preact's own useId()) — an unstable id baked
|
|
36650
|
-
* into a pushed history entry won't survive a reload correctly, and could
|
|
36651
|
-
* even collide with a different component's own auto-generated id. Pass a
|
|
36652
|
-
* stable, explicit id to actually get "push" behavior.
|
|
36653
|
-
* @param {() => void} [options.onLeave]
|
|
36654
|
-
* Called when the state key disappears **externally** — e.g. the user presses the browser
|
|
36655
|
-
* back button. Not called when leave() is invoked programmatically.
|
|
36656
|
-
* @param {*} [options.defaultValue]
|
|
36657
|
-
* 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
|
|
36658
36773
|
*
|
|
36659
|
-
* @returns {[value, enter, leave]}
|
|
36660
|
-
* - `value`: current value from document state, or `defaultValue` when the key is absent.
|
|
36661
|
-
* - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
|
|
36662
|
-
* Calling without an argument stores `"on"` — the presence of the key is enough to match;
|
|
36663
|
-
* the value allows associating extra data when needed.
|
|
36664
|
-
* - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
|
|
36665
|
-
* or goes back in history when `type` is "push").
|
|
36666
36774
|
*/
|
|
36667
|
-
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
|
+
};
|
|
36668
36796
|
|
|
36669
36797
|
const NEVER_SET = {};
|
|
36670
36798
|
const useUrlSearchParam = (paramName, defaultValue) => {
|
|
@@ -47795,21 +47923,51 @@ const withoutEmptyFields = uiState => {
|
|
|
47795
47923
|
// register themselves in their own effects, which run first — this is the
|
|
47796
47924
|
// earliest moment the form knows what it holds. Everything after this baseline
|
|
47797
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.
|
|
47798
47934
|
const useHeldUIStateAsSent = (uiStateController, pristineKey) => {
|
|
47799
47935
|
// The render that brought a new pristineKey read `changed` against the
|
|
47800
47936
|
// previous baseline, and nothing else is going to move: the button would stay
|
|
47801
47937
|
// lit on a form that holds exactly what it was just given. So ask for the one
|
|
47802
|
-
// render that reads the new baseline — the first
|
|
47803
|
-
// 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.
|
|
47804
47940
|
const [, rereadBaseline] = useState(0);
|
|
47805
47941
|
const isFirstRef = useRef(true);
|
|
47806
47942
|
useLayoutEffect(() => {
|
|
47807
|
-
|
|
47808
|
-
|
|
47809
|
-
|
|
47810
|
-
|
|
47811
|
-
|
|
47812
|
-
|
|
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
|
+
};
|
|
47813
47971
|
}, [uiStateController, pristineKey]);
|
|
47814
47972
|
};
|
|
47815
47973
|
const useUnregisteredControlWarning = ref => {
|
|
@@ -54720,6 +54878,7 @@ const ListUI = props => {
|
|
|
54720
54878
|
expand: expand,
|
|
54721
54879
|
"navi-nothing-to-display": nothingToDisplay ? "" : undefined,
|
|
54722
54880
|
"navi-loading": loading ? "" : undefined,
|
|
54881
|
+
"navi-refreshing": virtual.refreshingSignal.value ? "" : undefined,
|
|
54723
54882
|
"navi-error": error ? "" : undefined,
|
|
54724
54883
|
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
54725
54884
|
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
@@ -57044,6 +57203,10 @@ const createListVirtual = () => {
|
|
|
57044
57203
|
// registers, nothing is drawn), and yet they are what it may have been
|
|
57045
57204
|
// waiting for — the row it was told to open on, for one.
|
|
57046
57205
|
const pagesSignal = signal(0);
|
|
57206
|
+
// How many runs are re-reading rows they already show. The list wears it as
|
|
57207
|
+
// an attribute: what is drawn is from before, and the app may want to say so
|
|
57208
|
+
// without taking anything away.
|
|
57209
|
+
const refreshingSignal = signal(0);
|
|
57047
57210
|
const placeByOwner = new Map();
|
|
57048
57211
|
const locatorByOwner = new Map();
|
|
57049
57212
|
let passId = 0;
|
|
@@ -57051,6 +57214,7 @@ const createListVirtual = () => {
|
|
|
57051
57214
|
const virtual = {
|
|
57052
57215
|
totalSignal,
|
|
57053
57216
|
pagesSignal,
|
|
57217
|
+
refreshingSignal,
|
|
57054
57218
|
// What a run needs to know about the list it lives in: how many rows the
|
|
57055
57219
|
// list is willing to draw at once, which end it opens on, and how much
|
|
57056
57220
|
// room one row is given — a row whose content has not arrived must take
|
|
@@ -57153,7 +57317,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
57153
57317
|
* inside `<List.Group>`s; each takes its place in declaration order.
|
|
57154
57318
|
*
|
|
57155
57319
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
57156
|
-
* renderItem: (item: any, index: number) => import("ignore:preact").ComponentChildren,
|
|
57320
|
+
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
57157
57321
|
* itemsAction: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
57158
57322
|
* count?: number,
|
|
57159
57323
|
* groupBy?: (item: any, index: number) => any,
|
|
@@ -57163,6 +57327,10 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
57163
57327
|
* renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
|
|
57164
57328
|
* renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
|
|
57165
57329
|
* }>}
|
|
57330
|
+
* @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
|
|
57331
|
+
* What one row is, given the item and where it sits. `state.refreshing` says
|
|
57332
|
+
* the rows drawn are the ones from before while the run reads the collection
|
|
57333
|
+
* again — the list carries `navi-refreshing` for the same reason.
|
|
57166
57334
|
* @param {(item: any, index: number) => any} [props.groupBy]
|
|
57167
57335
|
* What tells rows that belong together apart from the others — the day of a
|
|
57168
57336
|
* message, the month of a game. Consecutive rows sharing it are wrapped in a
|
|
@@ -57369,6 +57537,9 @@ const ListItems = ({
|
|
|
57369
57537
|
virtualItemSize: virtualItemSize
|
|
57370
57538
|
}, "navi-list-filler-before"));
|
|
57371
57539
|
}
|
|
57540
|
+
const renderItemState = {
|
|
57541
|
+
refreshing: store.refreshing
|
|
57542
|
+
};
|
|
57372
57543
|
let rowIndex = windowFrom;
|
|
57373
57544
|
while (rowIndex < windowTo) {
|
|
57374
57545
|
if (rowIndex >= failureFrom && rowIndex <= failureTo) {
|
|
@@ -57396,7 +57567,7 @@ const ListItems = ({
|
|
|
57396
57567
|
const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
|
|
57397
57568
|
let rowVnode;
|
|
57398
57569
|
if (item !== undefined) {
|
|
57399
|
-
rowVnode = renderItem(item, rowIndex);
|
|
57570
|
+
rowVnode = renderItem(item, rowIndex, renderItemState);
|
|
57400
57571
|
} else if (renderRowSkeleton === false) {
|
|
57401
57572
|
// The row must still take its room: without it the rows below would
|
|
57402
57573
|
// climb up and slide back down as the answer arrives.
|
|
@@ -57497,6 +57668,22 @@ const useItemStore = ({
|
|
|
57497
57668
|
}
|
|
57498
57669
|
const pages = pagesRef.current;
|
|
57499
57670
|
const [, setPageVersion] = useState(0);
|
|
57671
|
+
// The rows held are out of date and the run has not asked for the new ones
|
|
57672
|
+
// yet. They stay on screen until the answer comes: what is drawn is from
|
|
57673
|
+
// before, which is not the same thing as nothing to draw.
|
|
57674
|
+
const staleRef = useRef(false);
|
|
57675
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
57676
|
+
// A source that says when what it reads has moved (a resource range reader
|
|
57677
|
+
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
57678
|
+
// to the collection is exactly what a run cannot deduce from the rows it
|
|
57679
|
+
// holds. A source that says nothing is read once and stays as it is.
|
|
57680
|
+
const invalidationSignal = typeof itemsAction === "function" ? itemsAction.invalidationSignal : null;
|
|
57681
|
+
const invalidation = invalidationSignal ? invalidationSignal.value : 0;
|
|
57682
|
+
const invalidationRef = useRef(invalidation);
|
|
57683
|
+
if (invalidationRef.current !== invalidation) {
|
|
57684
|
+
invalidationRef.current = invalidation;
|
|
57685
|
+
staleRef.current = true;
|
|
57686
|
+
}
|
|
57500
57687
|
// The one request in flight, with the means to call it off: a page asked for
|
|
57501
57688
|
// a window the list has left is work the server and the browser are doing for
|
|
57502
57689
|
// nothing.
|
|
@@ -57506,7 +57693,8 @@ const useItemStore = ({
|
|
|
57506
57693
|
end: -1,
|
|
57507
57694
|
held: -1,
|
|
57508
57695
|
controller: null,
|
|
57509
|
-
generation: 0
|
|
57696
|
+
generation: 0,
|
|
57697
|
+
revalidating: false
|
|
57510
57698
|
});
|
|
57511
57699
|
// The rows asked for that never came. Kept as a range so the list can say
|
|
57512
57700
|
// where the hole is, and cleared by a retry — which is what makes the same
|
|
@@ -57517,9 +57705,24 @@ const useItemStore = ({
|
|
|
57517
57705
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
57518
57706
|
// like rows on their way, not like an empty list.
|
|
57519
57707
|
const rowCount = pages.count ?? count ?? virtual.renderBudget;
|
|
57708
|
+
// A run that never received anything has nothing to keep on screen: asking
|
|
57709
|
+
// again is its first ask, not a refresh.
|
|
57710
|
+
if (staleRef.current && pages.count === undefined) {
|
|
57711
|
+
staleRef.current = false;
|
|
57712
|
+
}
|
|
57713
|
+
useLayoutEffect(() => {
|
|
57714
|
+
if (!refreshing) {
|
|
57715
|
+
return null;
|
|
57716
|
+
}
|
|
57717
|
+
virtual.refreshingSignal.value = virtual.refreshingSignal.peek() + 1;
|
|
57718
|
+
return () => {
|
|
57719
|
+
virtual.refreshingSignal.value = virtual.refreshingSignal.peek() - 1;
|
|
57720
|
+
};
|
|
57721
|
+
}, [refreshing]);
|
|
57520
57722
|
const store = {
|
|
57521
57723
|
rowCount,
|
|
57522
57724
|
failure,
|
|
57725
|
+
refreshing,
|
|
57523
57726
|
// JS memory is cheap next to the DOM, but a long enough scroll accumulates
|
|
57524
57727
|
// everything it ever went through. Rows far from what is on screen are
|
|
57525
57728
|
// dropped and simply asked for again if the user goes back — the same
|
|
@@ -57560,7 +57763,25 @@ const useItemStore = ({
|
|
|
57560
57763
|
let start = missingStart;
|
|
57561
57764
|
let end = missingEnd;
|
|
57562
57765
|
let around;
|
|
57563
|
-
|
|
57766
|
+
// Rows that are all there but out of date: the ask is the window itself,
|
|
57767
|
+
// anchored on the row at its top — a source paginating by cursor gets a
|
|
57768
|
+
// row to count from, and the reading position is what must survive.
|
|
57769
|
+
const revalidating = staleRef.current;
|
|
57770
|
+
if (revalidating) {
|
|
57771
|
+
start = windowFrom;
|
|
57772
|
+
end = windowTo - 1;
|
|
57773
|
+
if (end < start) {
|
|
57774
|
+
// Nothing of this run is on screen (the window frames another one, or
|
|
57775
|
+
// the list is scrolled past it): its own first rows are what it will
|
|
57776
|
+
// draw next.
|
|
57777
|
+
start = 0;
|
|
57778
|
+
end = budget - 1;
|
|
57779
|
+
}
|
|
57780
|
+
const firstHeld = pages.byIndex.get(windowFrom);
|
|
57781
|
+
if (firstHeld && firstHeld.id !== undefined) {
|
|
57782
|
+
around = firstHeld.id;
|
|
57783
|
+
}
|
|
57784
|
+
} else if (pages.count === undefined) {
|
|
57564
57785
|
const scrolled = virtual.scrolled;
|
|
57565
57786
|
if (scrolled === "end") {
|
|
57566
57787
|
// Counting back from the end, the way an HTTP range does: a list
|
|
@@ -57591,6 +57812,14 @@ const useItemStore = ({
|
|
|
57591
57812
|
return;
|
|
57592
57813
|
}
|
|
57593
57814
|
const request = requestRef.current;
|
|
57815
|
+
if (revalidating && request.busy) {
|
|
57816
|
+
if (request.revalidating) {
|
|
57817
|
+
return;
|
|
57818
|
+
}
|
|
57819
|
+
// A page for a window that is about to be replaced wholesale.
|
|
57820
|
+
request.controller?.abort();
|
|
57821
|
+
request.busy = false;
|
|
57822
|
+
}
|
|
57594
57823
|
if (request.busy) {
|
|
57595
57824
|
// Still worth waiting for as long as what it went to fetch is still
|
|
57596
57825
|
// what the list would draw. Once it is not, it is called off — and
|
|
@@ -57609,8 +57838,9 @@ const useItemStore = ({
|
|
|
57609
57838
|
}
|
|
57610
57839
|
const held = pages.byIndex.size;
|
|
57611
57840
|
// Asking again for a range that was already asked for, having received
|
|
57612
|
-
// nothing since, can only produce the same answer.
|
|
57613
|
-
|
|
57841
|
+
// nothing since, can only produce the same answer. A revalidation is
|
|
57842
|
+
// exactly the case where it produces another one.
|
|
57843
|
+
if (!revalidating && request.start === start && request.end === end && request.held === held) {
|
|
57614
57844
|
return;
|
|
57615
57845
|
}
|
|
57616
57846
|
request.start = start;
|
|
@@ -57625,17 +57855,33 @@ const useItemStore = ({
|
|
|
57625
57855
|
end,
|
|
57626
57856
|
around,
|
|
57627
57857
|
limit: end - start + 1,
|
|
57628
|
-
|
|
57629
|
-
|
|
57858
|
+
// A cursor names a row of the collection as it was; a revalidation
|
|
57859
|
+
// is asked precisely because that is what changed.
|
|
57860
|
+
before: revalidating ? undefined : cursor.before,
|
|
57861
|
+
after: revalidating ? undefined : cursor.after,
|
|
57630
57862
|
count: pages.count,
|
|
57631
57863
|
signal: controller.signal
|
|
57632
57864
|
};
|
|
57633
57865
|
request.busy = true;
|
|
57866
|
+
request.revalidating = revalidating;
|
|
57867
|
+
if (revalidating) {
|
|
57868
|
+
setRefreshing(true);
|
|
57869
|
+
}
|
|
57634
57870
|
const done = page => {
|
|
57635
57871
|
const current = generation === request.generation;
|
|
57636
57872
|
if (current) {
|
|
57637
57873
|
request.busy = false;
|
|
57874
|
+
request.revalidating = false;
|
|
57638
57875
|
setFailure(null);
|
|
57876
|
+
if (revalidating) {
|
|
57877
|
+
staleRef.current = false;
|
|
57878
|
+
setRefreshing(false);
|
|
57879
|
+
}
|
|
57880
|
+
}
|
|
57881
|
+
if (revalidating && !current) {
|
|
57882
|
+
// Rows of a composition already superseded by a newer ask: keeping
|
|
57883
|
+
// them would mix two states of the collection.
|
|
57884
|
+
return;
|
|
57639
57885
|
}
|
|
57640
57886
|
if (!page) {
|
|
57641
57887
|
return;
|
|
@@ -57646,6 +57892,12 @@ const useItemStore = ({
|
|
|
57646
57892
|
// Before the rows land: what is on screen has to stay where it is,
|
|
57647
57893
|
// and the DOM still shows the state to hold onto.
|
|
57648
57894
|
virtual.captureAnchor();
|
|
57895
|
+
if (revalidating) {
|
|
57896
|
+
// The rows held stood for a composition that has moved on; the
|
|
57897
|
+
// ones outside the window are forgotten and asked for again if the
|
|
57898
|
+
// user goes back to them.
|
|
57899
|
+
pages.byIndex = new Map();
|
|
57900
|
+
}
|
|
57649
57901
|
let i = 0;
|
|
57650
57902
|
while (i < pageItems.length) {
|
|
57651
57903
|
pages.byIndex.set(pageStart + i, pageItems[i]);
|
|
@@ -57661,6 +57913,14 @@ const useItemStore = ({
|
|
|
57661
57913
|
return;
|
|
57662
57914
|
}
|
|
57663
57915
|
request.busy = false;
|
|
57916
|
+
request.revalidating = false;
|
|
57917
|
+
if (revalidating) {
|
|
57918
|
+
// The rows from before stay: a revalidation that failed has
|
|
57919
|
+
// nothing better to put in their place.
|
|
57920
|
+
staleRef.current = false;
|
|
57921
|
+
setRefreshing(false);
|
|
57922
|
+
return;
|
|
57923
|
+
}
|
|
57664
57924
|
setFailure({
|
|
57665
57925
|
start,
|
|
57666
57926
|
end,
|
|
@@ -58265,8 +58525,8 @@ const PickerTypeResolver = props => {
|
|
|
58265
58525
|
...props
|
|
58266
58526
|
});
|
|
58267
58527
|
}
|
|
58268
|
-
if (props.type === "
|
|
58269
|
-
return jsx(
|
|
58528
|
+
if (props.type === "object") {
|
|
58529
|
+
return jsx(PickerObject, {
|
|
58270
58530
|
...props
|
|
58271
58531
|
});
|
|
58272
58532
|
}
|
|
@@ -58282,19 +58542,19 @@ const PickerText = props => {
|
|
|
58282
58542
|
});
|
|
58283
58543
|
};
|
|
58284
58544
|
|
|
58285
|
-
// The popup holds a group of named controls — a `<
|
|
58286
|
-
// when
|
|
58287
|
-
//
|
|
58288
|
-
// surface (see dialog.jsx), so there is nothing to tell it about the shape.
|
|
58289
|
-
const
|
|
58545
|
+
// The popup holds a group of named controls — a `<ControlGroup>`, or a `<Form>`
|
|
58546
|
+
// when that group is a question with a send of its own — and this picker's
|
|
58547
|
+
// value is the object that group aggregates. The popup itself holds nothing: it
|
|
58548
|
+
// is a surface (see dialog.jsx), so there is nothing to tell it about the shape.
|
|
58549
|
+
const PickerObject = props => {
|
|
58290
58550
|
const Next = useNextResolver();
|
|
58291
58551
|
return jsx(Next, {
|
|
58292
|
-
ui: jsx(
|
|
58552
|
+
ui: jsx(PickerObjectUI, {}),
|
|
58293
58553
|
...props,
|
|
58294
58554
|
type: "navi_js"
|
|
58295
58555
|
});
|
|
58296
58556
|
};
|
|
58297
|
-
const
|
|
58557
|
+
const PickerObjectUI = () => {
|
|
58298
58558
|
const {
|
|
58299
58559
|
value,
|
|
58300
58560
|
placeholder
|
|
@@ -59304,7 +59564,7 @@ Picker.UI.Week = PickerWeekUI;
|
|
|
59304
59564
|
Picker.UI.Datetime = PickerDatetimeUI;
|
|
59305
59565
|
Picker.UI.File = PickerFileUI;
|
|
59306
59566
|
Picker.UI.Color = PickerColorUI;
|
|
59307
|
-
Picker.UI.
|
|
59567
|
+
Picker.UI.Object = PickerObjectUI;
|
|
59308
59568
|
Picker.UI.Multiple = PickerArrayUI;
|
|
59309
59569
|
Picker.UI.PencilSvg = PencilSvg;
|
|
59310
59570
|
Picker.UI.ChevronDownSvg = ChevronDownSvg$1;
|