@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/dev/jsenv_navi.js
CHANGED
|
@@ -18348,6 +18348,33 @@ const syncOwnedResourceToSignals = (
|
|
|
18348
18348
|
|
|
18349
18349
|
// Global signal registry for route template detection
|
|
18350
18350
|
const globalSignalRegistry = new Map();
|
|
18351
|
+
|
|
18352
|
+
// The values some states are about to hold, answered in place of what they
|
|
18353
|
+
// hold for the length of one readStateAsIf.
|
|
18354
|
+
let valuesAsIf = null;
|
|
18355
|
+
/**
|
|
18356
|
+
* Runs `read` as if each state in `values` held the value given for it: the
|
|
18357
|
+
* address a press is about to write, read before it is written. Nothing is
|
|
18358
|
+
* written, so whoever watches those states hears nothing, and `read`
|
|
18359
|
+
* subscribes to nothing.
|
|
18360
|
+
*
|
|
18361
|
+
* Only a read of the state itself is answered this way. A `computed` derived
|
|
18362
|
+
* from one answers what it last computed — or, evaluated for the first time
|
|
18363
|
+
* inside `read`, caches a value derived from this answer: `read` reads the
|
|
18364
|
+
* states it depends on directly (a route action's params do).
|
|
18365
|
+
*
|
|
18366
|
+
* @param {Map<import("@preact/signals").Signal, any>} values
|
|
18367
|
+
* @param {() => any} read
|
|
18368
|
+
*/
|
|
18369
|
+
const readStateAsIf = (values, read) => {
|
|
18370
|
+
const previous = valuesAsIf;
|
|
18371
|
+
valuesAsIf = values;
|
|
18372
|
+
try {
|
|
18373
|
+
return untracked(read);
|
|
18374
|
+
} finally {
|
|
18375
|
+
valuesAsIf = previous;
|
|
18376
|
+
}
|
|
18377
|
+
};
|
|
18351
18378
|
let signalIdCounter = 0;
|
|
18352
18379
|
const generateSignalId = () => {
|
|
18353
18380
|
const id = signalIdCounter++;
|
|
@@ -18671,6 +18698,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18671
18698
|
const valueDescriptor = Object.getOwnPropertyDescriptor(signalProto, "value");
|
|
18672
18699
|
Object.defineProperty(preactSignal, "value", {
|
|
18673
18700
|
get() {
|
|
18701
|
+
if (valuesAsIf !== null && valuesAsIf.has(preactSignal)) {
|
|
18702
|
+
return valuesAsIf.get(preactSignal);
|
|
18703
|
+
}
|
|
18674
18704
|
return valueDescriptor.get.call(preactSignal);
|
|
18675
18705
|
},
|
|
18676
18706
|
set(newValue) {
|
|
@@ -27055,7 +27085,8 @@ const route = (
|
|
|
27055
27085
|
};
|
|
27056
27086
|
// Asks ahead of the arrival for what this route's actions ask without the
|
|
27057
27087
|
// address — a page's code, a read without params (see routeAction). An
|
|
27058
|
-
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
27088
|
+
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
27089
|
+
// (preloadState is the one that has one).
|
|
27059
27090
|
route.preload = () => {
|
|
27060
27091
|
const preloadSet = routePreloadMap.get(route);
|
|
27061
27092
|
if (!preloadSet) {
|
|
@@ -27635,6 +27666,40 @@ const preloadUrl = (url) => {
|
|
|
27635
27666
|
}
|
|
27636
27667
|
}
|
|
27637
27668
|
};
|
|
27669
|
+
|
|
27670
|
+
const routeStatePreloadMap = new WeakMap();
|
|
27671
|
+
const registerRouteStatePreload = (route, preload) => {
|
|
27672
|
+
let preloadSet = routeStatePreloadMap.get(route);
|
|
27673
|
+
if (!preloadSet) {
|
|
27674
|
+
preloadSet = new Set();
|
|
27675
|
+
routeStatePreloadMap.set(route, preloadSet);
|
|
27676
|
+
}
|
|
27677
|
+
preloadSet.add(preload);
|
|
27678
|
+
};
|
|
27679
|
+
/**
|
|
27680
|
+
* Preloads what the routes one is on will ask for once `signal` holds
|
|
27681
|
+
* `value`: the address a popup's opening is about to write (see
|
|
27682
|
+
* open_controller.js), known at the press that opens it. Only the routes
|
|
27683
|
+
* matching now are asked — writing a state keeps one on the same page.
|
|
27684
|
+
*/
|
|
27685
|
+
const preloadState = (signal, value) => {
|
|
27686
|
+
if (!activeRouteSet) {
|
|
27687
|
+
return;
|
|
27688
|
+
}
|
|
27689
|
+
const values = new Map([[signal, value]]);
|
|
27690
|
+
for (const route of activeRouteSet) {
|
|
27691
|
+
if (!route.matchingSignal.peek()) {
|
|
27692
|
+
continue;
|
|
27693
|
+
}
|
|
27694
|
+
const preloadSet = routeStatePreloadMap.get(route);
|
|
27695
|
+
if (!preloadSet) {
|
|
27696
|
+
continue;
|
|
27697
|
+
}
|
|
27698
|
+
for (const preload of preloadSet) {
|
|
27699
|
+
preload(values);
|
|
27700
|
+
}
|
|
27701
|
+
}
|
|
27702
|
+
};
|
|
27638
27703
|
const setupRoutes = (routes) => {
|
|
27639
27704
|
if (setupRoutesCalled) {
|
|
27640
27705
|
throw new Error(
|
|
@@ -34806,10 +34871,14 @@ const writeCover = (edge, element, size) => {
|
|
|
34806
34871
|
* the address, which is also what makes it worth fetching AHEAD of the
|
|
34807
34872
|
* arrival: a link to the route preruns it when the pointer or the focus
|
|
34808
34873
|
* reaches the link (see docs/dynamic_import.md). An action whose params
|
|
34809
|
-
* come from the address is
|
|
34874
|
+
* come from the address is not prefetched by a link — a link's prefetch has
|
|
34875
|
+
* no address. It is by the press that opens a popup bound to a state these
|
|
34876
|
+
* params read: that address is the current one with the state set to the
|
|
34877
|
+
* popup's value (see `prefetch` on Button).
|
|
34810
34878
|
* @param {object} [options]
|
|
34811
|
-
* @param {boolean} [options.prefetch=true] - `false` keeps
|
|
34812
|
-
*
|
|
34879
|
+
* @param {boolean} [options.prefetch=true] - `false` keeps the action from
|
|
34880
|
+
* being prerun ahead of what asks for it, when the read is not worth a
|
|
34881
|
+
* hover or a press that may not end in an opening.
|
|
34813
34882
|
*/
|
|
34814
34883
|
const routeAction = (
|
|
34815
34884
|
routeOrRoutes,
|
|
@@ -34838,27 +34907,47 @@ const routeAction = (
|
|
|
34838
34907
|
},
|
|
34839
34908
|
options,
|
|
34840
34909
|
);
|
|
34841
|
-
if (
|
|
34910
|
+
if (prefetch) {
|
|
34842
34911
|
for (const route of routes) {
|
|
34843
|
-
|
|
34844
|
-
|
|
34845
|
-
|
|
34912
|
+
if (paramsEffect) {
|
|
34913
|
+
registerRouteStatePreload(route, (values) => {
|
|
34914
|
+
prefetchAsIf(action, paramsEffect, values);
|
|
34915
|
+
});
|
|
34916
|
+
} else {
|
|
34917
|
+
registerRoutePreload(route, () => {
|
|
34918
|
+
prerunAhead(action.bindParams(true));
|
|
34919
|
+
});
|
|
34920
|
+
}
|
|
34846
34921
|
}
|
|
34847
34922
|
}
|
|
34848
34923
|
return actionBoundToRoute;
|
|
34849
34924
|
};
|
|
34850
34925
|
|
|
34851
|
-
// The
|
|
34852
|
-
// (
|
|
34853
|
-
//
|
|
34854
|
-
// and
|
|
34855
|
-
|
|
34856
|
-
|
|
34857
|
-
|
|
34858
|
-
|
|
34859
|
-
|
|
34860
|
-
|
|
34861
|
-
|
|
34926
|
+
// The params as they will read once the states hold what they are about to
|
|
34927
|
+
// (see readStateAsIf), prerun only when that changes them: params the state
|
|
34928
|
+
// leaves alone are the ones the action already runs on — or is meant not to,
|
|
34929
|
+
// and a prerun would retry a FAILED one behind the screen's back.
|
|
34930
|
+
const prefetchAsIf = (action, paramsEffect, values) => {
|
|
34931
|
+
const params = readStateAsIf(values, paramsEffect);
|
|
34932
|
+
if (!params) {
|
|
34933
|
+
return;
|
|
34934
|
+
}
|
|
34935
|
+
if (compareTwoJsValues(params, untracked(paramsEffect))) {
|
|
34936
|
+
return;
|
|
34937
|
+
}
|
|
34938
|
+
prerunAhead(action.bindParams(params));
|
|
34939
|
+
};
|
|
34940
|
+
|
|
34941
|
+
// The instance prerun is the one the arrival runs on — the one bound to the
|
|
34942
|
+
// params it reads (`true` for a param-less action) — so the arrival promotes
|
|
34943
|
+
// it to a run. A prefetch that fails is not reported: nothing on screen asked
|
|
34944
|
+
// for it, and the failure stays on the instance, where a run asks again — a
|
|
34945
|
+
// FAILED action is run, only RUNNING and COMPLETED are left alone — and the
|
|
34946
|
+
// arrival shows its own. It is left FAILED on purpose, not reset: a prerun is
|
|
34947
|
+
// often still in flight when the arrival promotes it (the press that brings
|
|
34948
|
+
// the arrival focuses the link, or is the one that asked), and a reset
|
|
34949
|
+
// landing after that would pull the answer from under the page reading it.
|
|
34950
|
+
const prerunAhead = (instance) => {
|
|
34862
34951
|
let result;
|
|
34863
34952
|
try {
|
|
34864
34953
|
result = instance.prerun({ reason: "intent" });
|
|
@@ -35872,6 +35961,30 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
35872
35961
|
},
|
|
35873
35962
|
};
|
|
35874
35963
|
});
|
|
35964
|
+
/**
|
|
35965
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
35966
|
+
* release that runs the command: what the opening will read can be asked for
|
|
35967
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
35968
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
35969
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
35970
|
+
* press is the first thing the finger says anyway.
|
|
35971
|
+
*
|
|
35972
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
35973
|
+
* runs here, and a source with no target says so at the release.
|
|
35974
|
+
*/
|
|
35975
|
+
const announceOpeningPress = (source, command) => {
|
|
35976
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
35977
|
+
return;
|
|
35978
|
+
}
|
|
35979
|
+
const commandFor = source.getAttribute("commandfor");
|
|
35980
|
+
const target = commandFor
|
|
35981
|
+
? document.getElementById(commandFor)
|
|
35982
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
35983
|
+
if (!target) {
|
|
35984
|
+
return;
|
|
35985
|
+
}
|
|
35986
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
35987
|
+
};
|
|
35875
35988
|
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
35876
35989
|
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
35877
35990
|
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
@@ -37330,7 +37443,7 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
37330
37443
|
return;
|
|
37331
37444
|
}
|
|
37332
37445
|
if (opened) {
|
|
37333
|
-
signal.value = popupValue
|
|
37446
|
+
signal.value = readOpenValue(popupValue);
|
|
37334
37447
|
return;
|
|
37335
37448
|
}
|
|
37336
37449
|
const closedValue = readClosedValue(signalValue);
|
|
@@ -37362,6 +37475,9 @@ const readOpened = (signalValue, popupValue) => {
|
|
|
37362
37475
|
}
|
|
37363
37476
|
return signalValue === popupValue;
|
|
37364
37477
|
};
|
|
37478
|
+
const readOpenValue = (popupValue) => {
|
|
37479
|
+
return popupValue === undefined ? true : popupValue;
|
|
37480
|
+
};
|
|
37365
37481
|
const readClosedValue = (openSignalValue) => {
|
|
37366
37482
|
return openSignalValue === true ? false : undefined;
|
|
37367
37483
|
};
|
|
@@ -37448,6 +37564,29 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
37448
37564
|
}
|
|
37449
37565
|
}
|
|
37450
37566
|
: null;
|
|
37567
|
+
// A press that will open this popup has started (see announceOpeningPress
|
|
37568
|
+
// in commands.js). What the opening writes — the popup's value, in its
|
|
37569
|
+
// signal — is known before the release, so what the screen reads off it is
|
|
37570
|
+
// asked for now (see preloadState).
|
|
37571
|
+
useEffect(() => {
|
|
37572
|
+
if (!signal) {
|
|
37573
|
+
return undefined;
|
|
37574
|
+
}
|
|
37575
|
+
const element = openController.getElement?.();
|
|
37576
|
+
if (!element) {
|
|
37577
|
+
return undefined;
|
|
37578
|
+
}
|
|
37579
|
+
const onOpenPress = () => {
|
|
37580
|
+
if (openController.opened) {
|
|
37581
|
+
return;
|
|
37582
|
+
}
|
|
37583
|
+
preloadState(signal, readOpenValue(value));
|
|
37584
|
+
};
|
|
37585
|
+
element.addEventListener("navi_open_press", onOpenPress);
|
|
37586
|
+
return () => {
|
|
37587
|
+
element.removeEventListener("navi_open_press", onOpenPress);
|
|
37588
|
+
};
|
|
37589
|
+
}, [signal, value]);
|
|
37451
37590
|
// Tracks whether the effect below has ever run before — only the very
|
|
37452
37591
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
37453
37592
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -38180,17 +38319,30 @@ const armOutsidePressClose = (
|
|
|
38180
38319
|
* content ahead of the click that will open it. Deferring the build to the
|
|
38181
38320
|
* opening puts its whole cost in the frame right after the click — the frame
|
|
38182
38321
|
* where a delay is felt hardest — while the ~100-300ms between hovering a
|
|
38183
|
-
* trigger and pressing it are free. The warming render
|
|
38184
|
-
*
|
|
38185
|
-
* the click, only before the open that follows it
|
|
38186
|
-
*
|
|
38187
|
-
*
|
|
38188
|
-
*
|
|
38189
|
-
*
|
|
38190
|
-
*
|
|
38191
|
-
*
|
|
38192
|
-
*
|
|
38193
|
-
*
|
|
38322
|
+
* trigger and pressing it are free. The warming render starts at the frame
|
|
38323
|
+
* after the one that follows the intent: nothing here needs the content in the
|
|
38324
|
+
* DOM before the click, only before the open that follows it, and on a touch
|
|
38325
|
+
* screen `pointerenter` arrives together with the `pointerdown` — a build
|
|
38326
|
+
* started there would hold back the frame that shows the press. Two animation
|
|
38327
|
+
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
38328
|
+
* and a timeout started by the press fires at its release, on top of the click.
|
|
38329
|
+
*
|
|
38330
|
+
* "while-opened" content is warmed by a press on the anchor, never by a hover
|
|
38331
|
+
* or a focus. That mode promises content built fresh for the gesture that
|
|
38332
|
+
* opens it, and mounted only while that gesture and its opening last: callers
|
|
38333
|
+
* lean on it (several pickers sharing one set of content ids, because only one
|
|
38334
|
+
* content exists at a time). A pointer crossing four anchors would build four
|
|
38335
|
+
* contents; a press is the start of one opening. So the press builds it, and
|
|
38336
|
+
* the press ending without an open throws it away — its click reaching the
|
|
38337
|
+
* document with the popup still closed, no click coming after the release,
|
|
38338
|
+
* the browser taking the gesture (`pointercancel`), or the next press
|
|
38339
|
+
* starting, before any of its own handlers run: one press at a time holds a
|
|
38340
|
+
* content. A keyboard opening
|
|
38341
|
+
* builds at open time, as without warming. What this gives up: a press-warmed
|
|
38342
|
+
* content is built before `onOpen` runs. The popups seeding their content from
|
|
38343
|
+
* `onOpen` are opened ON something by a command, whose press is not on their
|
|
38344
|
+
* anchor, so they are not warmed; one pressed on its own anchor that still
|
|
38345
|
+
* seeds from `onOpen` reads the value from before it.
|
|
38194
38346
|
*/
|
|
38195
38347
|
|
|
38196
38348
|
|
|
@@ -38232,26 +38384,35 @@ const usePopupContentMount = (
|
|
|
38232
38384
|
{ mount = MOUNT_DEFAULT, anchor },
|
|
38233
38385
|
) => {
|
|
38234
38386
|
const mountedAlways = mount === "always";
|
|
38235
|
-
const [contentMounted,
|
|
38387
|
+
const [contentMounted, setContentMountedState] = useState(
|
|
38236
38388
|
() => mountedAlways || openController.opened,
|
|
38237
38389
|
);
|
|
38238
|
-
|
|
38239
|
-
|
|
38240
|
-
|
|
38241
|
-
|
|
38242
|
-
|
|
38243
|
-
|
|
38244
|
-
|
|
38245
|
-
|
|
38246
|
-
|
|
38247
|
-
|
|
38248
|
-
|
|
38249
|
-
|
|
38250
|
-
|
|
38251
|
-
|
|
38252
|
-
|
|
38253
|
-
|
|
38254
|
-
|
|
38390
|
+
// What was last asked for, ahead of the render that performs it: an open
|
|
38391
|
+
// landing in between (a press-warmed content given up just before the open
|
|
38392
|
+
// that wanted it) must still find it has to build.
|
|
38393
|
+
const contentMountedRef = useRef(contentMounted);
|
|
38394
|
+
const setContentMounted = (value) => {
|
|
38395
|
+
contentMountedRef.current = value;
|
|
38396
|
+
setContentMountedState(value);
|
|
38397
|
+
};
|
|
38398
|
+
openController.mountContent = () => {
|
|
38399
|
+
if (contentMountedRef.current) {
|
|
38400
|
+
return;
|
|
38401
|
+
}
|
|
38402
|
+
const popupElement = ref?.current;
|
|
38403
|
+
if (popupElement) {
|
|
38404
|
+
popupsMountingContentForOpen.add(popupElement);
|
|
38405
|
+
}
|
|
38406
|
+
try {
|
|
38407
|
+
flushSyncRendering(() => {
|
|
38408
|
+
setContentMounted(true);
|
|
38409
|
+
});
|
|
38410
|
+
} finally {
|
|
38411
|
+
if (popupElement) {
|
|
38412
|
+
popupsMountingContentForOpen.delete(popupElement);
|
|
38413
|
+
}
|
|
38414
|
+
}
|
|
38415
|
+
};
|
|
38255
38416
|
openController.unmountContent =
|
|
38256
38417
|
mount === "while-opened"
|
|
38257
38418
|
? () => {
|
|
@@ -38289,41 +38450,140 @@ const usePopupContentMount = (
|
|
|
38289
38450
|
cancelIdle(idleId);
|
|
38290
38451
|
};
|
|
38291
38452
|
}, [mount, contentMounted]);
|
|
38292
|
-
// Warm on intent (see the top comment
|
|
38293
|
-
// The anchor accepts the same shapes Popover resolves at open time — a
|
|
38294
|
-
// string id, a ref, an element — but is resolved here at effect time: an id
|
|
38295
|
-
// that matches nothing yet simply doesn't warm, the open still mounts the
|
|
38296
|
-
// content like it always does.
|
|
38453
|
+
// Warm on intent (see the top comment).
|
|
38297
38454
|
useEffect(() => {
|
|
38298
38455
|
if (contentMounted || !anchor || mount === "while-opened") {
|
|
38299
38456
|
return undefined;
|
|
38300
38457
|
}
|
|
38301
|
-
const anchorElement =
|
|
38302
|
-
typeof anchor === "string"
|
|
38303
|
-
? document.getElementById(anchor)
|
|
38304
|
-
: // A ref is unwrapped even when it holds nothing: an expandable with
|
|
38305
|
-
// no UI part hands an empty ref over, and the ref object itself is
|
|
38306
|
-
// truthy — it would reach addEventListener below and throw.
|
|
38307
|
-
"current" in anchor
|
|
38308
|
-
? anchor.current
|
|
38309
|
-
: anchor;
|
|
38458
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
38310
38459
|
if (!anchorElement) {
|
|
38311
38460
|
return undefined;
|
|
38312
38461
|
}
|
|
38462
|
+
let cancelWarm = null;
|
|
38313
38463
|
const warm = () => {
|
|
38314
|
-
|
|
38464
|
+
if (cancelWarm) {
|
|
38465
|
+
return;
|
|
38466
|
+
}
|
|
38467
|
+
cancelWarm = requestFrameAfterNext(() => {
|
|
38468
|
+
setContentMounted(true);
|
|
38469
|
+
});
|
|
38315
38470
|
};
|
|
38316
38471
|
anchorElement.addEventListener("pointerenter", warm);
|
|
38317
38472
|
anchorElement.addEventListener("focusin", warm);
|
|
38318
38473
|
return () => {
|
|
38474
|
+
cancelWarm?.();
|
|
38319
38475
|
anchorElement.removeEventListener("pointerenter", warm);
|
|
38320
38476
|
anchorElement.removeEventListener("focusin", warm);
|
|
38321
38477
|
};
|
|
38322
38478
|
}, [contentMounted, anchor, mount]);
|
|
38479
|
+
// Warm on the press, for "while-opened" (see the top comment). Listening for
|
|
38480
|
+
// as long as the anchor is there, not only while the content is unmounted:
|
|
38481
|
+
// the press that throws a warmed content away can be a new press on this
|
|
38482
|
+
// same anchor, and it must warm again.
|
|
38483
|
+
useEffect(() => {
|
|
38484
|
+
if (!anchor || mount !== "while-opened") {
|
|
38485
|
+
return undefined;
|
|
38486
|
+
}
|
|
38487
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
38488
|
+
if (!anchorElement) {
|
|
38489
|
+
return undefined;
|
|
38490
|
+
}
|
|
38491
|
+
let stopPressWarm = null;
|
|
38492
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
38493
|
+
if (pointerdownEvent.button !== 0 || contentMountedRef.current) {
|
|
38494
|
+
return;
|
|
38495
|
+
}
|
|
38496
|
+
const cancelBuild = requestFrameAfterNext(() => {
|
|
38497
|
+
setContentMounted(true);
|
|
38498
|
+
});
|
|
38499
|
+
const stopWatching = watchPressEnd(() => {
|
|
38500
|
+
stopPressWarm = null;
|
|
38501
|
+
cancelBuild();
|
|
38502
|
+
if (!openController.opened) {
|
|
38503
|
+
setContentMounted(false);
|
|
38504
|
+
}
|
|
38505
|
+
});
|
|
38506
|
+
stopPressWarm = () => {
|
|
38507
|
+
cancelBuild();
|
|
38508
|
+
stopWatching();
|
|
38509
|
+
};
|
|
38510
|
+
};
|
|
38511
|
+
anchorElement.addEventListener("pointerdown", onPointerDown);
|
|
38512
|
+
return () => {
|
|
38513
|
+
stopPressWarm?.();
|
|
38514
|
+
anchorElement.removeEventListener("pointerdown", onPointerDown);
|
|
38515
|
+
};
|
|
38516
|
+
}, [anchor, mount]);
|
|
38323
38517
|
|
|
38324
38518
|
return contentMounted;
|
|
38325
38519
|
};
|
|
38326
38520
|
|
|
38521
|
+
// The anchor accepts the same shapes Popover resolves at open time — a string
|
|
38522
|
+
// id, a ref, an element — but is resolved at effect time: an id that matches
|
|
38523
|
+
// nothing yet simply doesn't warm, the open still mounts the content.
|
|
38524
|
+
const resolveAnchorElement = (anchor) => {
|
|
38525
|
+
if (typeof anchor === "string") {
|
|
38526
|
+
return document.getElementById(anchor);
|
|
38527
|
+
}
|
|
38528
|
+
// A ref is unwrapped even when it holds nothing: an expandable with no UI
|
|
38529
|
+
// part hands an empty ref over, and the ref object itself is truthy — it
|
|
38530
|
+
// would reach addEventListener and throw.
|
|
38531
|
+
if ("current" in anchor) {
|
|
38532
|
+
return anchor.current;
|
|
38533
|
+
}
|
|
38534
|
+
return anchor;
|
|
38535
|
+
};
|
|
38536
|
+
|
|
38537
|
+
// The next frame paints what the input just changed (the pressed trigger); the
|
|
38538
|
+
// callback runs at the start of the one after.
|
|
38539
|
+
const requestFrameAfterNext = (callback) => {
|
|
38540
|
+
let frame = requestAnimationFrame(() => {
|
|
38541
|
+
frame = requestAnimationFrame(callback);
|
|
38542
|
+
});
|
|
38543
|
+
return () => {
|
|
38544
|
+
cancelAnimationFrame(frame);
|
|
38545
|
+
};
|
|
38546
|
+
};
|
|
38547
|
+
|
|
38548
|
+
// How long the click of a released press may take to arrive. Nothing tells a
|
|
38549
|
+
// page that a release brings no click — a press that became a drag has its
|
|
38550
|
+
// click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
|
|
38551
|
+
// press is taken to have ended without one. Longer than the 300ms a browser
|
|
38552
|
+
// still waits before the click of a tap on a page not sized for phones.
|
|
38553
|
+
const PRESS_CLICK_WAIT_MS = 500;
|
|
38554
|
+
|
|
38555
|
+
// Installed during the press's own pointerdown: the capture listeners below
|
|
38556
|
+
// only hear the presses after it.
|
|
38557
|
+
const watchPressEnd = (onEnd) => {
|
|
38558
|
+
let timeout;
|
|
38559
|
+
const stop = () => {
|
|
38560
|
+
clearTimeout(timeout);
|
|
38561
|
+
document.removeEventListener("click", onClick);
|
|
38562
|
+
document.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
38563
|
+
document.removeEventListener("pointercancel", end, { capture: true });
|
|
38564
|
+
document.removeEventListener("pointerdown", end, { capture: true });
|
|
38565
|
+
};
|
|
38566
|
+
const end = () => {
|
|
38567
|
+
stop();
|
|
38568
|
+
onEnd();
|
|
38569
|
+
};
|
|
38570
|
+
// Bubble phase, on the document: every click handler that could open the
|
|
38571
|
+
// popup has run by then.
|
|
38572
|
+
const onClick = (clickEvent) => {
|
|
38573
|
+
if (isPressDrivenClick(clickEvent)) {
|
|
38574
|
+
end();
|
|
38575
|
+
}
|
|
38576
|
+
};
|
|
38577
|
+
const onPointerUp = () => {
|
|
38578
|
+
timeout = setTimeout(end, PRESS_CLICK_WAIT_MS);
|
|
38579
|
+
};
|
|
38580
|
+
document.addEventListener("click", onClick);
|
|
38581
|
+
document.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
38582
|
+
document.addEventListener("pointercancel", end, { capture: true });
|
|
38583
|
+
document.addEventListener("pointerdown", end, { capture: true });
|
|
38584
|
+
return stop;
|
|
38585
|
+
};
|
|
38586
|
+
|
|
38327
38587
|
/**
|
|
38328
38588
|
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
38329
38589
|
* or popover visibility.
|
|
@@ -47106,6 +47366,31 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
|
|
|
47106
47366
|
}, [ref, href, prefetch]);
|
|
47107
47367
|
};
|
|
47108
47368
|
|
|
47369
|
+
/**
|
|
47370
|
+
* What a popup's opening will read, asked for when the press on the element
|
|
47371
|
+
* whose command opens it starts (see announceOpeningPress): the popup, the
|
|
47372
|
+
* value it opens on and so the address it will write are all known before
|
|
47373
|
+
* the release.
|
|
47374
|
+
*/
|
|
47375
|
+
const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
47376
|
+
useEffect(() => {
|
|
47377
|
+
if (!prefetch || !command) {
|
|
47378
|
+
return undefined;
|
|
47379
|
+
}
|
|
47380
|
+
const element = ref.current;
|
|
47381
|
+
if (!element) {
|
|
47382
|
+
return undefined;
|
|
47383
|
+
}
|
|
47384
|
+
const onPointerDown = () => {
|
|
47385
|
+
announceOpeningPress(element, command);
|
|
47386
|
+
};
|
|
47387
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
47388
|
+
return () => {
|
|
47389
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
47390
|
+
};
|
|
47391
|
+
}, [ref, command, prefetch]);
|
|
47392
|
+
};
|
|
47393
|
+
|
|
47109
47394
|
/**
|
|
47110
47395
|
* Hook that reactively checks if a URL is visited.
|
|
47111
47396
|
* Re-renders when the visited URL set changes.
|
|
@@ -51115,7 +51400,8 @@ const useExpandableContext = partName => {
|
|
|
51115
51400
|
* content — and rebuilds it from scratch on every expansion. Under the
|
|
51116
51401
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
51117
51402
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
51118
|
-
*
|
|
51403
|
+
* built by a press on it only, and thrown away if that press does not
|
|
51404
|
+
* expand it.
|
|
51119
51405
|
*/
|
|
51120
51406
|
const Expandable = props => {
|
|
51121
51407
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -52427,6 +52713,9 @@ const ButtonUI = props => {
|
|
|
52427
52713
|
// Nothing to prefetch on the way to another document: the routes do not
|
|
52428
52714
|
// lead there.
|
|
52429
52715
|
usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
|
|
52716
|
+
// A press the button refuses opens nothing: nothing to ask ahead for.
|
|
52717
|
+
const refusesPress = basePseudoState[":read-only"] || basePseudoState[":disabled"] || loading;
|
|
52718
|
+
usePreloadOpeningOnPress(ref, props.command, prefetch && !refusesPress);
|
|
52430
52719
|
useAccentColorAttributes(ref, null, {
|
|
52431
52720
|
elementSelector: visualSelector
|
|
52432
52721
|
});
|
|
@@ -52921,8 +53210,11 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
52921
53210
|
* pointer or the focus arrives, ahead of the press (see
|
|
52922
53211
|
* docs/dynamic_import.md): the route actions that ask nothing of the
|
|
52923
53212
|
* address — a page's code, a read without params — are prerun; one keyed by
|
|
52924
|
-
* a param waits for the arrival.
|
|
52925
|
-
*
|
|
53213
|
+
* a param waits for the arrival. With a `--navi-open`/`--navi-toggle`
|
|
53214
|
+
* command instead, the press — not the hover: what it asks is keyed by the
|
|
53215
|
+
* popup — preruns the route actions whose params the popup's opening
|
|
53216
|
+
* changes, read as if its `signal` held its `value`. `false` for a
|
|
53217
|
+
* destination not worth fetching ahead.
|
|
52926
53218
|
* @param {boolean} [pressableDuringRouteTransition] Keep answering presses
|
|
52927
53219
|
* while a route transition plays: what a movement photographs goes deaf to
|
|
52928
53220
|
* the pointer for its whole length, and the door that opened the page — a
|
|
@@ -63805,9 +64097,10 @@ const css$E = /* css */`
|
|
|
63805
64097
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
63806
64098
|
* the popup has finished closing, for content whose fresh state is its
|
|
63807
64099
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
63808
|
-
* changed while the popup was closed.
|
|
63809
|
-
*
|
|
63810
|
-
*
|
|
64100
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
64101
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
64102
|
+
* under `"while-opened"`, only a press on it does, and a press that does not
|
|
64103
|
+
* open the popup throws it away.
|
|
63811
64104
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
63812
64105
|
*/
|
|
63813
64106
|
const Dialog = props => {
|
|
@@ -65567,9 +65860,10 @@ const css$D = /* css */`
|
|
|
65567
65860
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
65568
65861
|
* the popup has finished closing, for content whose fresh state is its
|
|
65569
65862
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
65570
|
-
* changed while the popup was closed.
|
|
65571
|
-
*
|
|
65572
|
-
*
|
|
65863
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
65864
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
65865
|
+
* under `"while-opened"`, only a press on it does, and a press that does not
|
|
65866
|
+
* open the popup throws it away.
|
|
65573
65867
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
65574
65868
|
*/
|
|
65575
65869
|
const Popover = props => {
|
|
@@ -80475,6 +80769,9 @@ const css$n = /* css */`.navi_split_button {
|
|
|
80475
80769
|
* defaultValue?: any,
|
|
80476
80770
|
* label?: import("ignore:preact").ComponentChildren,
|
|
80477
80771
|
* action?: (value: any, event: Event) => void | Promise<void>,
|
|
80772
|
+
* command?: string,
|
|
80773
|
+
* commandFor?: string,
|
|
80774
|
+
* prefetch?: boolean,
|
|
80478
80775
|
* onValueChange?: (value: any, event: Event) => void,
|
|
80479
80776
|
* chooseEffect?: "select" | "run",
|
|
80480
80777
|
* menuLabel?: string,
|
|
@@ -80528,6 +80825,12 @@ const css$n = /* css */`.navi_split_button {
|
|
|
80528
80825
|
* entry given. Pressed on the button it is the entry the button stands for;
|
|
80529
80826
|
* with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
|
|
80530
80827
|
* whole split button is busy until it settles.
|
|
80828
|
+
* @param {string} [command] What a press on the button does once its `action`
|
|
80829
|
+
* is done, as on a `<Button>` (with `commandFor`, and `command-value` when
|
|
80830
|
+
* what the command is about is not the entry the button stands for). A
|
|
80831
|
+
* split button whose button opens a popup says it here rather than from its
|
|
80832
|
+
* `action`: the press is then read as an opening before it runs, and asks
|
|
80833
|
+
* ahead for what the popup's opening reads (see `prefetch` on Button).
|
|
80531
80834
|
* @param {"select"|"run"} [chooseEffect="select"] What choosing an entry does.
|
|
80532
80835
|
* "select" hands the button that entry, to be run by a press on it — for
|
|
80533
80836
|
* something one does again and again, where the last choice is the likely
|
|
@@ -80569,6 +80872,10 @@ const SplitButton = props => {
|
|
|
80569
80872
|
defaultValue,
|
|
80570
80873
|
label,
|
|
80571
80874
|
action,
|
|
80875
|
+
command,
|
|
80876
|
+
commandFor,
|
|
80877
|
+
"command-value": commandValue,
|
|
80878
|
+
prefetch,
|
|
80572
80879
|
onValueChange,
|
|
80573
80880
|
chooseEffect = "select",
|
|
80574
80881
|
menuLabel = naviI18n("button.more_actions"),
|
|
@@ -80662,6 +80969,10 @@ const SplitButton = props => {
|
|
|
80662
80969
|
id: idResolved,
|
|
80663
80970
|
value: valueResolved,
|
|
80664
80971
|
action: actionResolved,
|
|
80972
|
+
command: command,
|
|
80973
|
+
commandFor: commandFor,
|
|
80974
|
+
"command-value": commandValue,
|
|
80975
|
+
prefetch: prefetch,
|
|
80665
80976
|
...halfProps,
|
|
80666
80977
|
children: label === undefined ? optionShown?.label : label
|
|
80667
80978
|
}), jsxs("div", {
|