@jsenv/navi 0.29.398 → 0.29.399
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +384 -73
- package/dist/dev/jsenv_navi.js.map +14 -14
- package/dist/jsenv_navi.js +384 -73
- package/dist/jsenv_navi.js.map +14 -14
- package/docs/dynamic_import.md +3 -2
- package/docs/popup_open.md +46 -9
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -17751,6 +17751,33 @@ const syncOwnedResourceToSignals = (
|
|
|
17751
17751
|
|
|
17752
17752
|
// Global signal registry for route template detection
|
|
17753
17753
|
const globalSignalRegistry = new Map();
|
|
17754
|
+
|
|
17755
|
+
// The values some states are about to hold, answered in place of what they
|
|
17756
|
+
// hold for the length of one readStateAsIf.
|
|
17757
|
+
let valuesAsIf = null;
|
|
17758
|
+
/**
|
|
17759
|
+
* Runs `read` as if each state in `values` held the value given for it: the
|
|
17760
|
+
* address a press is about to write, read before it is written. Nothing is
|
|
17761
|
+
* written, so whoever watches those states hears nothing, and `read`
|
|
17762
|
+
* subscribes to nothing.
|
|
17763
|
+
*
|
|
17764
|
+
* Only a read of the state itself is answered this way. A `computed` derived
|
|
17765
|
+
* from one answers what it last computed — or, evaluated for the first time
|
|
17766
|
+
* inside `read`, caches a value derived from this answer: `read` reads the
|
|
17767
|
+
* states it depends on directly (a route action's params do).
|
|
17768
|
+
*
|
|
17769
|
+
* @param {Map<import("@preact/signals").Signal, any>} values
|
|
17770
|
+
* @param {() => any} read
|
|
17771
|
+
*/
|
|
17772
|
+
const readStateAsIf = (values, read) => {
|
|
17773
|
+
const previous = valuesAsIf;
|
|
17774
|
+
valuesAsIf = values;
|
|
17775
|
+
try {
|
|
17776
|
+
return untracked(read);
|
|
17777
|
+
} finally {
|
|
17778
|
+
valuesAsIf = previous;
|
|
17779
|
+
}
|
|
17780
|
+
};
|
|
17754
17781
|
let signalIdCounter = 0;
|
|
17755
17782
|
const generateSignalId = () => {
|
|
17756
17783
|
const id = signalIdCounter++;
|
|
@@ -18074,6 +18101,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18074
18101
|
const valueDescriptor = Object.getOwnPropertyDescriptor(signalProto, "value");
|
|
18075
18102
|
Object.defineProperty(preactSignal, "value", {
|
|
18076
18103
|
get() {
|
|
18104
|
+
if (valuesAsIf !== null && valuesAsIf.has(preactSignal)) {
|
|
18105
|
+
return valuesAsIf.get(preactSignal);
|
|
18106
|
+
}
|
|
18077
18107
|
return valueDescriptor.get.call(preactSignal);
|
|
18078
18108
|
},
|
|
18079
18109
|
set(newValue) {
|
|
@@ -26332,7 +26362,8 @@ const route = (
|
|
|
26332
26362
|
};
|
|
26333
26363
|
// Asks ahead of the arrival for what this route's actions ask without the
|
|
26334
26364
|
// address — a page's code, a read without params (see routeAction). An
|
|
26335
|
-
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
26365
|
+
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
26366
|
+
// (preloadState is the one that has one).
|
|
26336
26367
|
route.preload = () => {
|
|
26337
26368
|
const preloadSet = routePreloadMap.get(route);
|
|
26338
26369
|
if (!preloadSet) {
|
|
@@ -26899,6 +26930,40 @@ const preloadUrl = (url) => {
|
|
|
26899
26930
|
}
|
|
26900
26931
|
}
|
|
26901
26932
|
};
|
|
26933
|
+
|
|
26934
|
+
const routeStatePreloadMap = new WeakMap();
|
|
26935
|
+
const registerRouteStatePreload = (route, preload) => {
|
|
26936
|
+
let preloadSet = routeStatePreloadMap.get(route);
|
|
26937
|
+
if (!preloadSet) {
|
|
26938
|
+
preloadSet = new Set();
|
|
26939
|
+
routeStatePreloadMap.set(route, preloadSet);
|
|
26940
|
+
}
|
|
26941
|
+
preloadSet.add(preload);
|
|
26942
|
+
};
|
|
26943
|
+
/**
|
|
26944
|
+
* Preloads what the routes one is on will ask for once `signal` holds
|
|
26945
|
+
* `value`: the address a popup's opening is about to write (see
|
|
26946
|
+
* open_controller.js), known at the press that opens it. Only the routes
|
|
26947
|
+
* matching now are asked — writing a state keeps one on the same page.
|
|
26948
|
+
*/
|
|
26949
|
+
const preloadState = (signal, value) => {
|
|
26950
|
+
if (!activeRouteSet) {
|
|
26951
|
+
return;
|
|
26952
|
+
}
|
|
26953
|
+
const values = new Map([[signal, value]]);
|
|
26954
|
+
for (const route of activeRouteSet) {
|
|
26955
|
+
if (!route.matchingSignal.peek()) {
|
|
26956
|
+
continue;
|
|
26957
|
+
}
|
|
26958
|
+
const preloadSet = routeStatePreloadMap.get(route);
|
|
26959
|
+
if (!preloadSet) {
|
|
26960
|
+
continue;
|
|
26961
|
+
}
|
|
26962
|
+
for (const preload of preloadSet) {
|
|
26963
|
+
preload(values);
|
|
26964
|
+
}
|
|
26965
|
+
}
|
|
26966
|
+
};
|
|
26902
26967
|
const setupRoutes = (routes) => {
|
|
26903
26968
|
if (setupRoutesCalled) {
|
|
26904
26969
|
throw new Error(
|
|
@@ -33880,10 +33945,14 @@ const writeCover = (edge, element, size) => {
|
|
|
33880
33945
|
* the address, which is also what makes it worth fetching AHEAD of the
|
|
33881
33946
|
* arrival: a link to the route preruns it when the pointer or the focus
|
|
33882
33947
|
* reaches the link (see docs/dynamic_import.md). An action whose params
|
|
33883
|
-
* come from the address is
|
|
33948
|
+
* come from the address is not prefetched by a link — a link's prefetch has
|
|
33949
|
+
* no address. It is by the press that opens a popup bound to a state these
|
|
33950
|
+
* params read: that address is the current one with the state set to the
|
|
33951
|
+
* popup's value (see `prefetch` on Button).
|
|
33884
33952
|
* @param {object} [options]
|
|
33885
|
-
* @param {boolean} [options.prefetch=true] - `false` keeps
|
|
33886
|
-
*
|
|
33953
|
+
* @param {boolean} [options.prefetch=true] - `false` keeps the action from
|
|
33954
|
+
* being prerun ahead of what asks for it, when the read is not worth a
|
|
33955
|
+
* hover or a press that may not end in an opening.
|
|
33887
33956
|
*/
|
|
33888
33957
|
const routeAction = (
|
|
33889
33958
|
routeOrRoutes,
|
|
@@ -33912,27 +33981,47 @@ const routeAction = (
|
|
|
33912
33981
|
},
|
|
33913
33982
|
options,
|
|
33914
33983
|
);
|
|
33915
|
-
if (
|
|
33984
|
+
if (prefetch) {
|
|
33916
33985
|
for (const route of routes) {
|
|
33917
|
-
|
|
33918
|
-
|
|
33919
|
-
|
|
33986
|
+
if (paramsEffect) {
|
|
33987
|
+
registerRouteStatePreload(route, (values) => {
|
|
33988
|
+
prefetchAsIf(action, paramsEffect, values);
|
|
33989
|
+
});
|
|
33990
|
+
} else {
|
|
33991
|
+
registerRoutePreload(route, () => {
|
|
33992
|
+
prerunAhead(action.bindParams(true));
|
|
33993
|
+
});
|
|
33994
|
+
}
|
|
33920
33995
|
}
|
|
33921
33996
|
}
|
|
33922
33997
|
return actionBoundToRoute;
|
|
33923
33998
|
};
|
|
33924
33999
|
|
|
33925
|
-
// The
|
|
33926
|
-
// (
|
|
33927
|
-
//
|
|
33928
|
-
// and
|
|
33929
|
-
|
|
33930
|
-
|
|
33931
|
-
|
|
33932
|
-
|
|
33933
|
-
|
|
33934
|
-
|
|
33935
|
-
|
|
34000
|
+
// The params as they will read once the states hold what they are about to
|
|
34001
|
+
// (see readStateAsIf), prerun only when that changes them: params the state
|
|
34002
|
+
// leaves alone are the ones the action already runs on — or is meant not to,
|
|
34003
|
+
// and a prerun would retry a FAILED one behind the screen's back.
|
|
34004
|
+
const prefetchAsIf = (action, paramsEffect, values) => {
|
|
34005
|
+
const params = readStateAsIf(values, paramsEffect);
|
|
34006
|
+
if (!params) {
|
|
34007
|
+
return;
|
|
34008
|
+
}
|
|
34009
|
+
if (compareTwoJsValues(params, untracked(paramsEffect))) {
|
|
34010
|
+
return;
|
|
34011
|
+
}
|
|
34012
|
+
prerunAhead(action.bindParams(params));
|
|
34013
|
+
};
|
|
34014
|
+
|
|
34015
|
+
// The instance prerun is the one the arrival runs on — the one bound to the
|
|
34016
|
+
// params it reads (`true` for a param-less action) — so the arrival promotes
|
|
34017
|
+
// it to a run. A prefetch that fails is not reported: nothing on screen asked
|
|
34018
|
+
// for it, and the failure stays on the instance, where a run asks again — a
|
|
34019
|
+
// FAILED action is run, only RUNNING and COMPLETED are left alone — and the
|
|
34020
|
+
// arrival shows its own. It is left FAILED on purpose, not reset: a prerun is
|
|
34021
|
+
// often still in flight when the arrival promotes it (the press that brings
|
|
34022
|
+
// the arrival focuses the link, or is the one that asked), and a reset
|
|
34023
|
+
// landing after that would pull the answer from under the page reading it.
|
|
34024
|
+
const prerunAhead = (instance) => {
|
|
33936
34025
|
let result;
|
|
33937
34026
|
try {
|
|
33938
34027
|
result = instance.prerun({ reason: "intent" });
|
|
@@ -34930,6 +35019,30 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
34930
35019
|
},
|
|
34931
35020
|
};
|
|
34932
35021
|
});
|
|
35022
|
+
/**
|
|
35023
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
35024
|
+
* release that runs the command: what the opening will read can be asked for
|
|
35025
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
35026
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
35027
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
35028
|
+
* press is the first thing the finger says anyway.
|
|
35029
|
+
*
|
|
35030
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
35031
|
+
* runs here, and a source with no target says so at the release.
|
|
35032
|
+
*/
|
|
35033
|
+
const announceOpeningPress = (source, command) => {
|
|
35034
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
35035
|
+
return;
|
|
35036
|
+
}
|
|
35037
|
+
const commandFor = source.getAttribute("commandfor");
|
|
35038
|
+
const target = commandFor
|
|
35039
|
+
? document.getElementById(commandFor)
|
|
35040
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
35041
|
+
if (!target) {
|
|
35042
|
+
return;
|
|
35043
|
+
}
|
|
35044
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
35045
|
+
};
|
|
34933
35046
|
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
34934
35047
|
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
34935
35048
|
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
@@ -36383,7 +36496,7 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
36383
36496
|
return;
|
|
36384
36497
|
}
|
|
36385
36498
|
if (opened) {
|
|
36386
|
-
signal.value = popupValue
|
|
36499
|
+
signal.value = readOpenValue(popupValue);
|
|
36387
36500
|
return;
|
|
36388
36501
|
}
|
|
36389
36502
|
const closedValue = readClosedValue(signalValue);
|
|
@@ -36415,6 +36528,9 @@ const readOpened = (signalValue, popupValue) => {
|
|
|
36415
36528
|
}
|
|
36416
36529
|
return signalValue === popupValue;
|
|
36417
36530
|
};
|
|
36531
|
+
const readOpenValue = (popupValue) => {
|
|
36532
|
+
return popupValue === undefined ? true : popupValue;
|
|
36533
|
+
};
|
|
36418
36534
|
const readClosedValue = (openSignalValue) => {
|
|
36419
36535
|
return openSignalValue === true ? false : undefined;
|
|
36420
36536
|
};
|
|
@@ -36488,6 +36604,29 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
36488
36604
|
}
|
|
36489
36605
|
}
|
|
36490
36606
|
: null;
|
|
36607
|
+
// A press that will open this popup has started (see announceOpeningPress
|
|
36608
|
+
// in commands.js). What the opening writes — the popup's value, in its
|
|
36609
|
+
// signal — is known before the release, so what the screen reads off it is
|
|
36610
|
+
// asked for now (see preloadState).
|
|
36611
|
+
useEffect(() => {
|
|
36612
|
+
if (!signal) {
|
|
36613
|
+
return undefined;
|
|
36614
|
+
}
|
|
36615
|
+
const element = openController.getElement?.();
|
|
36616
|
+
if (!element) {
|
|
36617
|
+
return undefined;
|
|
36618
|
+
}
|
|
36619
|
+
const onOpenPress = () => {
|
|
36620
|
+
if (openController.opened) {
|
|
36621
|
+
return;
|
|
36622
|
+
}
|
|
36623
|
+
preloadState(signal, readOpenValue(value));
|
|
36624
|
+
};
|
|
36625
|
+
element.addEventListener("navi_open_press", onOpenPress);
|
|
36626
|
+
return () => {
|
|
36627
|
+
element.removeEventListener("navi_open_press", onOpenPress);
|
|
36628
|
+
};
|
|
36629
|
+
}, [signal, value]);
|
|
36491
36630
|
// Tracks whether the effect below has ever run before — only the very
|
|
36492
36631
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
36493
36632
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -37182,17 +37321,30 @@ const armOutsidePressClose = (
|
|
|
37182
37321
|
* content ahead of the click that will open it. Deferring the build to the
|
|
37183
37322
|
* opening puts its whole cost in the frame right after the click — the frame
|
|
37184
37323
|
* where a delay is felt hardest — while the ~100-300ms between hovering a
|
|
37185
|
-
* trigger and pressing it are free. The warming render
|
|
37186
|
-
*
|
|
37187
|
-
* the click, only before the open that follows it
|
|
37188
|
-
*
|
|
37189
|
-
*
|
|
37190
|
-
*
|
|
37191
|
-
*
|
|
37192
|
-
*
|
|
37193
|
-
*
|
|
37194
|
-
*
|
|
37195
|
-
*
|
|
37324
|
+
* trigger and pressing it are free. The warming render starts at the frame
|
|
37325
|
+
* after the one that follows the intent: nothing here needs the content in the
|
|
37326
|
+
* DOM before the click, only before the open that follows it, and on a touch
|
|
37327
|
+
* screen `pointerenter` arrives together with the `pointerdown` — a build
|
|
37328
|
+
* started there would hold back the frame that shows the press. Two animation
|
|
37329
|
+
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
37330
|
+
* and a timeout started by the press fires at its release, on top of the click.
|
|
37331
|
+
*
|
|
37332
|
+
* "while-opened" content is warmed by a press on the anchor, never by a hover
|
|
37333
|
+
* or a focus. That mode promises content built fresh for the gesture that
|
|
37334
|
+
* opens it, and mounted only while that gesture and its opening last: callers
|
|
37335
|
+
* lean on it (several pickers sharing one set of content ids, because only one
|
|
37336
|
+
* content exists at a time). A pointer crossing four anchors would build four
|
|
37337
|
+
* contents; a press is the start of one opening. So the press builds it, and
|
|
37338
|
+
* the press ending without an open throws it away — its click reaching the
|
|
37339
|
+
* document with the popup still closed, no click coming after the release,
|
|
37340
|
+
* the browser taking the gesture (`pointercancel`), or the next press
|
|
37341
|
+
* starting, before any of its own handlers run: one press at a time holds a
|
|
37342
|
+
* content. A keyboard opening
|
|
37343
|
+
* builds at open time, as without warming. What this gives up: a press-warmed
|
|
37344
|
+
* content is built before `onOpen` runs. The popups seeding their content from
|
|
37345
|
+
* `onOpen` are opened ON something by a command, whose press is not on their
|
|
37346
|
+
* anchor, so they are not warmed; one pressed on its own anchor that still
|
|
37347
|
+
* seeds from `onOpen` reads the value from before it.
|
|
37196
37348
|
*/
|
|
37197
37349
|
|
|
37198
37350
|
|
|
@@ -37234,26 +37386,35 @@ const usePopupContentMount = (
|
|
|
37234
37386
|
{ mount = MOUNT_DEFAULT, anchor },
|
|
37235
37387
|
) => {
|
|
37236
37388
|
const mountedAlways = mount === "always";
|
|
37237
|
-
const [contentMounted,
|
|
37389
|
+
const [contentMounted, setContentMountedState] = useState(
|
|
37238
37390
|
() => mountedAlways || openController.opened,
|
|
37239
37391
|
);
|
|
37240
|
-
|
|
37241
|
-
|
|
37242
|
-
|
|
37243
|
-
|
|
37244
|
-
|
|
37245
|
-
|
|
37246
|
-
|
|
37247
|
-
|
|
37248
|
-
|
|
37249
|
-
|
|
37250
|
-
|
|
37251
|
-
|
|
37252
|
-
|
|
37253
|
-
|
|
37254
|
-
|
|
37255
|
-
|
|
37256
|
-
|
|
37392
|
+
// What was last asked for, ahead of the render that performs it: an open
|
|
37393
|
+
// landing in between (a press-warmed content given up just before the open
|
|
37394
|
+
// that wanted it) must still find it has to build.
|
|
37395
|
+
const contentMountedRef = useRef(contentMounted);
|
|
37396
|
+
const setContentMounted = (value) => {
|
|
37397
|
+
contentMountedRef.current = value;
|
|
37398
|
+
setContentMountedState(value);
|
|
37399
|
+
};
|
|
37400
|
+
openController.mountContent = () => {
|
|
37401
|
+
if (contentMountedRef.current) {
|
|
37402
|
+
return;
|
|
37403
|
+
}
|
|
37404
|
+
const popupElement = ref?.current;
|
|
37405
|
+
if (popupElement) {
|
|
37406
|
+
popupsMountingContentForOpen.add(popupElement);
|
|
37407
|
+
}
|
|
37408
|
+
try {
|
|
37409
|
+
flushSyncRendering(() => {
|
|
37410
|
+
setContentMounted(true);
|
|
37411
|
+
});
|
|
37412
|
+
} finally {
|
|
37413
|
+
if (popupElement) {
|
|
37414
|
+
popupsMountingContentForOpen.delete(popupElement);
|
|
37415
|
+
}
|
|
37416
|
+
}
|
|
37417
|
+
};
|
|
37257
37418
|
openController.unmountContent =
|
|
37258
37419
|
mount === "while-opened"
|
|
37259
37420
|
? () => {
|
|
@@ -37291,41 +37452,140 @@ const usePopupContentMount = (
|
|
|
37291
37452
|
cancelIdle(idleId);
|
|
37292
37453
|
};
|
|
37293
37454
|
}, [mount, contentMounted]);
|
|
37294
|
-
// Warm on intent (see the top comment
|
|
37295
|
-
// The anchor accepts the same shapes Popover resolves at open time — a
|
|
37296
|
-
// string id, a ref, an element — but is resolved here at effect time: an id
|
|
37297
|
-
// that matches nothing yet simply doesn't warm, the open still mounts the
|
|
37298
|
-
// content like it always does.
|
|
37455
|
+
// Warm on intent (see the top comment).
|
|
37299
37456
|
useEffect(() => {
|
|
37300
37457
|
if (contentMounted || !anchor || mount === "while-opened") {
|
|
37301
37458
|
return undefined;
|
|
37302
37459
|
}
|
|
37303
|
-
const anchorElement =
|
|
37304
|
-
typeof anchor === "string"
|
|
37305
|
-
? document.getElementById(anchor)
|
|
37306
|
-
: // A ref is unwrapped even when it holds nothing: an expandable with
|
|
37307
|
-
// no UI part hands an empty ref over, and the ref object itself is
|
|
37308
|
-
// truthy — it would reach addEventListener below and throw.
|
|
37309
|
-
"current" in anchor
|
|
37310
|
-
? anchor.current
|
|
37311
|
-
: anchor;
|
|
37460
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
37312
37461
|
if (!anchorElement) {
|
|
37313
37462
|
return undefined;
|
|
37314
37463
|
}
|
|
37464
|
+
let cancelWarm = null;
|
|
37315
37465
|
const warm = () => {
|
|
37316
|
-
|
|
37466
|
+
if (cancelWarm) {
|
|
37467
|
+
return;
|
|
37468
|
+
}
|
|
37469
|
+
cancelWarm = requestFrameAfterNext(() => {
|
|
37470
|
+
setContentMounted(true);
|
|
37471
|
+
});
|
|
37317
37472
|
};
|
|
37318
37473
|
anchorElement.addEventListener("pointerenter", warm);
|
|
37319
37474
|
anchorElement.addEventListener("focusin", warm);
|
|
37320
37475
|
return () => {
|
|
37476
|
+
cancelWarm?.();
|
|
37321
37477
|
anchorElement.removeEventListener("pointerenter", warm);
|
|
37322
37478
|
anchorElement.removeEventListener("focusin", warm);
|
|
37323
37479
|
};
|
|
37324
37480
|
}, [contentMounted, anchor, mount]);
|
|
37481
|
+
// Warm on the press, for "while-opened" (see the top comment). Listening for
|
|
37482
|
+
// as long as the anchor is there, not only while the content is unmounted:
|
|
37483
|
+
// the press that throws a warmed content away can be a new press on this
|
|
37484
|
+
// same anchor, and it must warm again.
|
|
37485
|
+
useEffect(() => {
|
|
37486
|
+
if (!anchor || mount !== "while-opened") {
|
|
37487
|
+
return undefined;
|
|
37488
|
+
}
|
|
37489
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
37490
|
+
if (!anchorElement) {
|
|
37491
|
+
return undefined;
|
|
37492
|
+
}
|
|
37493
|
+
let stopPressWarm = null;
|
|
37494
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
37495
|
+
if (pointerdownEvent.button !== 0 || contentMountedRef.current) {
|
|
37496
|
+
return;
|
|
37497
|
+
}
|
|
37498
|
+
const cancelBuild = requestFrameAfterNext(() => {
|
|
37499
|
+
setContentMounted(true);
|
|
37500
|
+
});
|
|
37501
|
+
const stopWatching = watchPressEnd(() => {
|
|
37502
|
+
stopPressWarm = null;
|
|
37503
|
+
cancelBuild();
|
|
37504
|
+
if (!openController.opened) {
|
|
37505
|
+
setContentMounted(false);
|
|
37506
|
+
}
|
|
37507
|
+
});
|
|
37508
|
+
stopPressWarm = () => {
|
|
37509
|
+
cancelBuild();
|
|
37510
|
+
stopWatching();
|
|
37511
|
+
};
|
|
37512
|
+
};
|
|
37513
|
+
anchorElement.addEventListener("pointerdown", onPointerDown);
|
|
37514
|
+
return () => {
|
|
37515
|
+
stopPressWarm?.();
|
|
37516
|
+
anchorElement.removeEventListener("pointerdown", onPointerDown);
|
|
37517
|
+
};
|
|
37518
|
+
}, [anchor, mount]);
|
|
37325
37519
|
|
|
37326
37520
|
return contentMounted;
|
|
37327
37521
|
};
|
|
37328
37522
|
|
|
37523
|
+
// The anchor accepts the same shapes Popover resolves at open time — a string
|
|
37524
|
+
// id, a ref, an element — but is resolved at effect time: an id that matches
|
|
37525
|
+
// nothing yet simply doesn't warm, the open still mounts the content.
|
|
37526
|
+
const resolveAnchorElement = (anchor) => {
|
|
37527
|
+
if (typeof anchor === "string") {
|
|
37528
|
+
return document.getElementById(anchor);
|
|
37529
|
+
}
|
|
37530
|
+
// A ref is unwrapped even when it holds nothing: an expandable with no UI
|
|
37531
|
+
// part hands an empty ref over, and the ref object itself is truthy — it
|
|
37532
|
+
// would reach addEventListener and throw.
|
|
37533
|
+
if ("current" in anchor) {
|
|
37534
|
+
return anchor.current;
|
|
37535
|
+
}
|
|
37536
|
+
return anchor;
|
|
37537
|
+
};
|
|
37538
|
+
|
|
37539
|
+
// The next frame paints what the input just changed (the pressed trigger); the
|
|
37540
|
+
// callback runs at the start of the one after.
|
|
37541
|
+
const requestFrameAfterNext = (callback) => {
|
|
37542
|
+
let frame = requestAnimationFrame(() => {
|
|
37543
|
+
frame = requestAnimationFrame(callback);
|
|
37544
|
+
});
|
|
37545
|
+
return () => {
|
|
37546
|
+
cancelAnimationFrame(frame);
|
|
37547
|
+
};
|
|
37548
|
+
};
|
|
37549
|
+
|
|
37550
|
+
// How long the click of a released press may take to arrive. Nothing tells a
|
|
37551
|
+
// page that a release brings no click — a press that became a drag has its
|
|
37552
|
+
// click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
|
|
37553
|
+
// press is taken to have ended without one. Longer than the 300ms a browser
|
|
37554
|
+
// still waits before the click of a tap on a page not sized for phones.
|
|
37555
|
+
const PRESS_CLICK_WAIT_MS = 500;
|
|
37556
|
+
|
|
37557
|
+
// Installed during the press's own pointerdown: the capture listeners below
|
|
37558
|
+
// only hear the presses after it.
|
|
37559
|
+
const watchPressEnd = (onEnd) => {
|
|
37560
|
+
let timeout;
|
|
37561
|
+
const stop = () => {
|
|
37562
|
+
clearTimeout(timeout);
|
|
37563
|
+
document.removeEventListener("click", onClick);
|
|
37564
|
+
document.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
37565
|
+
document.removeEventListener("pointercancel", end, { capture: true });
|
|
37566
|
+
document.removeEventListener("pointerdown", end, { capture: true });
|
|
37567
|
+
};
|
|
37568
|
+
const end = () => {
|
|
37569
|
+
stop();
|
|
37570
|
+
onEnd();
|
|
37571
|
+
};
|
|
37572
|
+
// Bubble phase, on the document: every click handler that could open the
|
|
37573
|
+
// popup has run by then.
|
|
37574
|
+
const onClick = (clickEvent) => {
|
|
37575
|
+
if (isPressDrivenClick(clickEvent)) {
|
|
37576
|
+
end();
|
|
37577
|
+
}
|
|
37578
|
+
};
|
|
37579
|
+
const onPointerUp = () => {
|
|
37580
|
+
timeout = setTimeout(end, PRESS_CLICK_WAIT_MS);
|
|
37581
|
+
};
|
|
37582
|
+
document.addEventListener("click", onClick);
|
|
37583
|
+
document.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
37584
|
+
document.addEventListener("pointercancel", end, { capture: true });
|
|
37585
|
+
document.addEventListener("pointerdown", end, { capture: true });
|
|
37586
|
+
return stop;
|
|
37587
|
+
};
|
|
37588
|
+
|
|
37329
37589
|
/**
|
|
37330
37590
|
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
37331
37591
|
* or popover visibility.
|
|
@@ -45965,6 +46225,31 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
|
|
|
45965
46225
|
}, [ref, href, prefetch]);
|
|
45966
46226
|
};
|
|
45967
46227
|
|
|
46228
|
+
/**
|
|
46229
|
+
* What a popup's opening will read, asked for when the press on the element
|
|
46230
|
+
* whose command opens it starts (see announceOpeningPress): the popup, the
|
|
46231
|
+
* value it opens on and so the address it will write are all known before
|
|
46232
|
+
* the release.
|
|
46233
|
+
*/
|
|
46234
|
+
const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
46235
|
+
useEffect(() => {
|
|
46236
|
+
if (!prefetch || !command) {
|
|
46237
|
+
return undefined;
|
|
46238
|
+
}
|
|
46239
|
+
const element = ref.current;
|
|
46240
|
+
if (!element) {
|
|
46241
|
+
return undefined;
|
|
46242
|
+
}
|
|
46243
|
+
const onPointerDown = () => {
|
|
46244
|
+
announceOpeningPress(element, command);
|
|
46245
|
+
};
|
|
46246
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
46247
|
+
return () => {
|
|
46248
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
46249
|
+
};
|
|
46250
|
+
}, [ref, command, prefetch]);
|
|
46251
|
+
};
|
|
46252
|
+
|
|
45968
46253
|
/**
|
|
45969
46254
|
* Hook that reactively checks if a URL is visited.
|
|
45970
46255
|
* Re-renders when the visited URL set changes.
|
|
@@ -49965,7 +50250,8 @@ const useExpandableContext = partName => {
|
|
|
49965
50250
|
* content — and rebuilds it from scratch on every expansion. Under the
|
|
49966
50251
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
49967
50252
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
49968
|
-
*
|
|
50253
|
+
* built by a press on it only, and thrown away if that press does not
|
|
50254
|
+
* expand it.
|
|
49969
50255
|
*/
|
|
49970
50256
|
const Expandable = props => {
|
|
49971
50257
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -51277,6 +51563,9 @@ const ButtonUI = props => {
|
|
|
51277
51563
|
// Nothing to prefetch on the way to another document: the routes do not
|
|
51278
51564
|
// lead there.
|
|
51279
51565
|
usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
|
|
51566
|
+
// A press the button refuses opens nothing: nothing to ask ahead for.
|
|
51567
|
+
const refusesPress = basePseudoState[":read-only"] || basePseudoState[":disabled"] || loading;
|
|
51568
|
+
usePreloadOpeningOnPress(ref, props.command, prefetch && !refusesPress);
|
|
51280
51569
|
useAccentColorAttributes(ref, null, {
|
|
51281
51570
|
elementSelector: visualSelector
|
|
51282
51571
|
});
|
|
@@ -51768,8 +52057,11 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
51768
52057
|
* pointer or the focus arrives, ahead of the press (see
|
|
51769
52058
|
* docs/dynamic_import.md): the route actions that ask nothing of the
|
|
51770
52059
|
* address — a page's code, a read without params — are prerun; one keyed by
|
|
51771
|
-
* a param waits for the arrival.
|
|
51772
|
-
*
|
|
52060
|
+
* a param waits for the arrival. With a `--navi-open`/`--navi-toggle`
|
|
52061
|
+
* command instead, the press — not the hover: what it asks is keyed by the
|
|
52062
|
+
* popup — preruns the route actions whose params the popup's opening
|
|
52063
|
+
* changes, read as if its `signal` held its `value`. `false` for a
|
|
52064
|
+
* destination not worth fetching ahead.
|
|
51773
52065
|
* @param {boolean} [pressableDuringRouteTransition] Keep answering presses
|
|
51774
52066
|
* while a route transition plays: what a movement photographs goes deaf to
|
|
51775
52067
|
* the pointer for its whole length, and the door that opened the page — a
|
|
@@ -62537,9 +62829,10 @@ const css$E = /* css */`
|
|
|
62537
62829
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
62538
62830
|
* the popup has finished closing, for content whose fresh state is its
|
|
62539
62831
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
62540
|
-
* changed while the popup was closed.
|
|
62541
|
-
*
|
|
62542
|
-
*
|
|
62832
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
62833
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
62834
|
+
* under `"while-opened"`, only a press on it does, and a press that does not
|
|
62835
|
+
* open the popup throws it away.
|
|
62543
62836
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
62544
62837
|
*/
|
|
62545
62838
|
const Dialog = props => {
|
|
@@ -64217,9 +64510,10 @@ const css$D = /* css */`
|
|
|
64217
64510
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
64218
64511
|
* the popup has finished closing, for content whose fresh state is its
|
|
64219
64512
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
64220
|
-
* changed while the popup was closed.
|
|
64221
|
-
*
|
|
64222
|
-
*
|
|
64513
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
64514
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
64515
|
+
* under `"while-opened"`, only a press on it does, and a press that does not
|
|
64516
|
+
* open the popup throws it away.
|
|
64223
64517
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
64224
64518
|
*/
|
|
64225
64519
|
const Popover = props => {
|
|
@@ -78354,6 +78648,9 @@ const css$n = /* css */`.navi_split_button {
|
|
|
78354
78648
|
* defaultValue?: any,
|
|
78355
78649
|
* label?: import("ignore:preact").ComponentChildren,
|
|
78356
78650
|
* action?: (value: any, event: Event) => void | Promise<void>,
|
|
78651
|
+
* command?: string,
|
|
78652
|
+
* commandFor?: string,
|
|
78653
|
+
* prefetch?: boolean,
|
|
78357
78654
|
* onValueChange?: (value: any, event: Event) => void,
|
|
78358
78655
|
* chooseEffect?: "select" | "run",
|
|
78359
78656
|
* menuLabel?: string,
|
|
@@ -78407,6 +78704,12 @@ const css$n = /* css */`.navi_split_button {
|
|
|
78407
78704
|
* entry given. Pressed on the button it is the entry the button stands for;
|
|
78408
78705
|
* with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
|
|
78409
78706
|
* whole split button is busy until it settles.
|
|
78707
|
+
* @param {string} [command] What a press on the button does once its `action`
|
|
78708
|
+
* is done, as on a `<Button>` (with `commandFor`, and `command-value` when
|
|
78709
|
+
* what the command is about is not the entry the button stands for). A
|
|
78710
|
+
* split button whose button opens a popup says it here rather than from its
|
|
78711
|
+
* `action`: the press is then read as an opening before it runs, and asks
|
|
78712
|
+
* ahead for what the popup's opening reads (see `prefetch` on Button).
|
|
78410
78713
|
* @param {"select"|"run"} [chooseEffect="select"] What choosing an entry does.
|
|
78411
78714
|
* "select" hands the button that entry, to be run by a press on it — for
|
|
78412
78715
|
* something one does again and again, where the last choice is the likely
|
|
@@ -78448,6 +78751,10 @@ const SplitButton = props => {
|
|
|
78448
78751
|
defaultValue,
|
|
78449
78752
|
label,
|
|
78450
78753
|
action,
|
|
78754
|
+
command,
|
|
78755
|
+
commandFor,
|
|
78756
|
+
"command-value": commandValue,
|
|
78757
|
+
prefetch,
|
|
78451
78758
|
onValueChange,
|
|
78452
78759
|
chooseEffect = "select",
|
|
78453
78760
|
menuLabel = naviI18n("button.more_actions"),
|
|
@@ -78541,6 +78848,10 @@ const SplitButton = props => {
|
|
|
78541
78848
|
id: idResolved,
|
|
78542
78849
|
value: valueResolved,
|
|
78543
78850
|
action: actionResolved,
|
|
78851
|
+
command: command,
|
|
78852
|
+
commandFor: commandFor,
|
|
78853
|
+
"command-value": commandValue,
|
|
78854
|
+
prefetch: prefetch,
|
|
78544
78855
|
...halfProps,
|
|
78545
78856
|
children: label === undefined ? optionShown?.label : label
|
|
78546
78857
|
}), jsxs("div", {
|