@jsenv/navi 0.29.398 → 0.29.400
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 +1105 -273
- package/dist/dev/jsenv_navi.js.map +27 -26
- package/dist/jsenv_navi.js +1105 -273
- package/dist/jsenv_navi.js.map +27 -26
- package/docs/AI_INSTRUCTIONS.md +10 -4
- package/docs/control_value.md +52 -6
- package/docs/drag_to_travel.md +32 -0
- package/docs/dynamic_import.md +3 -2
- package/docs/navigation.md +81 -1
- package/docs/popup_open.md +44 -9
- package/docs/scroll.md +3 -3
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature,
|
|
7
|
+
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, trapScrollInside, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
|
-
import { isValidElement, createContext, render, h, toChildArray,
|
|
10
|
+
import { options, isValidElement, createContext, render, h, toChildArray, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
11
11
|
import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
|
|
12
12
|
import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
13
13
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
@@ -95,6 +95,29 @@ const css$17 = /* css */ `@layer navi {
|
|
|
95
95
|
`;
|
|
96
96
|
import.meta.css = [css$17, "@jsenv/navi/src/navi_z_indexes.js"];
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A navigation is ABOUT to be applied — said before its very first write.
|
|
100
|
+
*
|
|
101
|
+
* Everything else a router says arrives once the change is made: a route
|
|
102
|
+
* announces that it matches, an action that it is running. That is too late for
|
|
103
|
+
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
104
|
+
* transitions are exactly that kind of reader — the picture they keep of the
|
|
105
|
+
* page being left is taken at the next frame, and a render answering a signal
|
|
106
|
+
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
107
|
+
*
|
|
108
|
+
* So this is the one moment where nothing has moved yet. It is published
|
|
109
|
+
* synchronously, from the top of the navigation, and whoever listens runs
|
|
110
|
+
* before the URL, the visited set, or any route has changed.
|
|
111
|
+
*
|
|
112
|
+
* The other end is published too, and for the same kind of reader: whoever
|
|
113
|
+
* held something across the change and has nobody to hand it to gets a moment
|
|
114
|
+
* to let go of it that does not depend on guessing how long the change takes.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
119
|
+
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
120
|
+
|
|
98
121
|
const addIntoArray = (array, ...valuesToAdd) => {
|
|
99
122
|
if (valuesToAdd.length === 1) {
|
|
100
123
|
const [valueToAdd] = valuesToAdd;
|
|
@@ -298,6 +321,112 @@ const urlToScheme = (url) => {
|
|
|
298
321
|
return scheme;
|
|
299
322
|
};
|
|
300
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The document's rendering, held for the one frame a view transition needs.
|
|
326
|
+
*
|
|
327
|
+
* The browser does not take the picture of the page being left when a
|
|
328
|
+
* transition is ASKED for — it takes it at the next frame, just before running
|
|
329
|
+
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
330
|
+
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
331
|
+
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
332
|
+
* Both sides of the animation then show it, and one watches a page slide onto
|
|
333
|
+
* itself.
|
|
334
|
+
*
|
|
335
|
+
* So what Preact has queued waits until the update callback, which is the
|
|
336
|
+
* moment the API is built around — the change belongs inside it. The whole
|
|
337
|
+
* document is held: it is about to be frozen under a picture anyway.
|
|
338
|
+
*
|
|
339
|
+
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
340
|
+
* around Preact's `options.debounceRendering`, and two of them installed
|
|
341
|
+
* independently restore each other in the wrong order when they let go — every
|
|
342
|
+
* render queued in between is then handed to a wrapper nobody will ever
|
|
343
|
+
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
344
|
+
* route transition) must therefore hold through this module, never through a
|
|
345
|
+
* wrapper of its own.
|
|
346
|
+
*/
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
let renderingHold = null;
|
|
350
|
+
const holdRendering = () => {
|
|
351
|
+
if (renderingHold) {
|
|
352
|
+
return renderingHold.release;
|
|
353
|
+
}
|
|
354
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
355
|
+
const hold = {
|
|
356
|
+
render: null,
|
|
357
|
+
waiting: [],
|
|
358
|
+
release: () => {
|
|
359
|
+
// Only the hold that is still standing may be given back: a holder
|
|
360
|
+
// releasing after another has taken over must not let go of what it
|
|
361
|
+
// does not hold.
|
|
362
|
+
if (renderingHold !== hold) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
renderingHold = null;
|
|
366
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
367
|
+
const { render, waiting } = hold;
|
|
368
|
+
hold.render = null;
|
|
369
|
+
hold.waiting = [];
|
|
370
|
+
if (render) {
|
|
371
|
+
render();
|
|
372
|
+
}
|
|
373
|
+
for (const wait of waiting) {
|
|
374
|
+
wait();
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
renderingHold = hold;
|
|
379
|
+
options.debounceRendering = (render) => {
|
|
380
|
+
hold.render = render;
|
|
381
|
+
};
|
|
382
|
+
return hold.release;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Anything else that must not happen before the picture is taken, and the
|
|
386
|
+
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
387
|
+
// document put back to its top while the page being left is still on screen is
|
|
388
|
+
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
389
|
+
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
390
|
+
// the band it had already painted — the page being left is then seen in
|
|
391
|
+
// fragments, whatever the movement does afterwards.
|
|
392
|
+
//
|
|
393
|
+
// Run at once when nobody is photographing anything, which is the common case
|
|
394
|
+
// and must stay free.
|
|
395
|
+
const whenRenderingResumes = (callback) => {
|
|
396
|
+
if (!renderingHold) {
|
|
397
|
+
callback();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
renderingHold.waiting.push(callback);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// The hold a navigation takes on its way in — from before its first write,
|
|
404
|
+
// because by the time a route announces that it matches, Preact has already
|
|
405
|
+
// been told and the render is queued; a hold taken then is a hold taken too
|
|
406
|
+
// late. Kept here until whoever animates the change takes it over, or the
|
|
407
|
+
// navigation turns out to be one nobody animates.
|
|
408
|
+
let routingRenderingHold = null;
|
|
409
|
+
const holdRenderingForRouting = () => {
|
|
410
|
+
routingRenderingHold = holdRendering();
|
|
411
|
+
};
|
|
412
|
+
// Nobody had a picture to take: a page held for a change it does not animate
|
|
413
|
+
// is a page that stutters for nothing.
|
|
414
|
+
const releaseRoutingRenderingHold = () => {
|
|
415
|
+
const release = routingRenderingHold;
|
|
416
|
+
routingRenderingHold = null;
|
|
417
|
+
if (release) {
|
|
418
|
+
release();
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// An animator takes the navigation's hold as its own — taking another would be
|
|
422
|
+
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
423
|
+
// the change it animates is not a navigation.
|
|
424
|
+
const takeoverRoutingRenderingHold = () => {
|
|
425
|
+
const release = routingRenderingHold || holdRendering();
|
|
426
|
+
routingRenderingHold = null;
|
|
427
|
+
return release;
|
|
428
|
+
};
|
|
429
|
+
|
|
301
430
|
installImportMetaCssBuild(import.meta);
|
|
302
431
|
|
|
303
432
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
@@ -347,7 +476,8 @@ let urlTargetOptions = {
|
|
|
347
476
|
* How long, in ms, to keep waiting for a target that has not arrived, counted
|
|
348
477
|
* from the moment the document stops working.
|
|
349
478
|
* @param {number} [options.maxWait=10000]
|
|
350
|
-
* Longest wait, in ms, for a document that never stops working.
|
|
479
|
+
* Longest wait, in ms, for a document that never stops working. The element
|
|
480
|
+
* is then answered as it stands, when it is there.
|
|
351
481
|
*/
|
|
352
482
|
const setUrlTargetOptions = (options) => {
|
|
353
483
|
urlTargetOptions = { ...urlTargetOptions, ...options };
|
|
@@ -374,6 +504,23 @@ const useUrlTargetId = () => {
|
|
|
374
504
|
let stopWaitingForCurrentTarget = null;
|
|
375
505
|
let currentTargetKey;
|
|
376
506
|
|
|
507
|
+
const documentLoadIsReturn = () => {
|
|
508
|
+
const [navigationEntry] = performance.getEntriesByType("navigation");
|
|
509
|
+
if (!navigationEntry) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return (
|
|
513
|
+
navigationEntry.type === "reload" || navigationEntry.type === "back_forward"
|
|
514
|
+
);
|
|
515
|
+
};
|
|
516
|
+
// The url the document is on its way back to — a back, a forward, a reload —
|
|
517
|
+
// where the reader was, as opposed to a place just sent to. Said by the
|
|
518
|
+
// navigation before it writes anything, and by the load for the first url.
|
|
519
|
+
let returningToUrl = documentLoadIsReturn() ? window.location.href : null;
|
|
520
|
+
observeBeforeRouting(({ url, navigationType }) => {
|
|
521
|
+
returningToUrl = navigationType === "traverse" ? url : null;
|
|
522
|
+
});
|
|
523
|
+
|
|
377
524
|
/**
|
|
378
525
|
* Answers the URL's target again, as if it had just been designated.
|
|
379
526
|
*
|
|
@@ -383,10 +530,10 @@ let currentTargetKey;
|
|
|
383
530
|
*/
|
|
384
531
|
const rearmUrlTarget = () => {
|
|
385
532
|
currentTargetKey = undefined;
|
|
386
|
-
armUrlTarget(documentUrlSignal.peek());
|
|
533
|
+
armUrlTarget(documentUrlSignal.peek(), { isReturn: false });
|
|
387
534
|
};
|
|
388
535
|
|
|
389
|
-
const armUrlTarget = (documentUrl) => {
|
|
536
|
+
const armUrlTarget = (documentUrl, { isReturn }) => {
|
|
390
537
|
const targetKey = urlToTargetKey(documentUrl);
|
|
391
538
|
if (targetKey === currentTargetKey) {
|
|
392
539
|
return;
|
|
@@ -397,11 +544,25 @@ const armUrlTarget = (documentUrl) => {
|
|
|
397
544
|
stopWaitingForCurrentTarget = null;
|
|
398
545
|
}
|
|
399
546
|
const targetId = urlToTargetId(documentUrl);
|
|
547
|
+
// The browser leaves `:target` on the element a URL named once the URL has
|
|
548
|
+
// stopped naming it — a pushState never touches it.
|
|
549
|
+
const staleTarget = document.querySelector(":target");
|
|
550
|
+
if (staleTarget && staleTarget.id !== targetId) {
|
|
551
|
+
writeTarget(null);
|
|
552
|
+
}
|
|
400
553
|
if (!targetId) {
|
|
401
554
|
return;
|
|
402
555
|
}
|
|
403
|
-
stopWaitingForCurrentTarget =
|
|
556
|
+
stopWaitingForCurrentTarget = waitForUrlTarget(targetId, (element) => {
|
|
404
557
|
stopWaitingForCurrentTarget = null;
|
|
558
|
+
if (!element.matches(":target")) {
|
|
559
|
+
writeTarget(element);
|
|
560
|
+
}
|
|
561
|
+
if (isReturn) {
|
|
562
|
+
// The offset put back by scroll_restoration.js is the reader's own: the
|
|
563
|
+
// element is named by the URL, not arrived at.
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
405
566
|
revealUrlTarget(element);
|
|
406
567
|
});
|
|
407
568
|
};
|
|
@@ -418,14 +579,27 @@ const urlToTargetId = (url) => {
|
|
|
418
579
|
return hash ? decodeURIComponent(hash.slice(1)) : "";
|
|
419
580
|
};
|
|
420
581
|
|
|
421
|
-
|
|
582
|
+
// Calls `onSettled` with the element once it can be answered: it exists, it
|
|
583
|
+
// shows something, and the document is done working. Found is not enough while
|
|
584
|
+
// the document works — what sits above it may still be drawn and push it down,
|
|
585
|
+
// and a page reusing a node of the page being left holds it before its own
|
|
586
|
+
// data has come.
|
|
587
|
+
//
|
|
588
|
+
// Confirmed at the next frame, once rendering has resumed: the render the last
|
|
589
|
+
// answer asked for has run by then, and a component starting its own run from
|
|
590
|
+
// that render has made the document busy again.
|
|
591
|
+
const waitForUrlTarget = (id, onSettled) => {
|
|
592
|
+
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
593
|
+
let stopped = false;
|
|
594
|
+
let confirmationPending = false;
|
|
595
|
+
let frame = null;
|
|
422
596
|
let mutationObserver = null;
|
|
423
597
|
let stopWatchingBusy = null;
|
|
424
598
|
let idleTimeout = null;
|
|
425
599
|
let maxWaitTimeout = null;
|
|
426
|
-
let found = false;
|
|
427
600
|
|
|
428
601
|
const stopWaiting = () => {
|
|
602
|
+
stopped = true;
|
|
429
603
|
if (mutationObserver) {
|
|
430
604
|
mutationObserver.disconnect();
|
|
431
605
|
mutationObserver = null;
|
|
@@ -436,76 +610,214 @@ const waitForElementWithId = (id, onFound) => {
|
|
|
436
610
|
}
|
|
437
611
|
clearTimeout(idleTimeout);
|
|
438
612
|
clearTimeout(maxWaitTimeout);
|
|
613
|
+
if (frame !== null) {
|
|
614
|
+
cancelAnimationFrame(frame);
|
|
615
|
+
frame = null;
|
|
616
|
+
}
|
|
439
617
|
};
|
|
440
618
|
|
|
441
|
-
const
|
|
619
|
+
const findElement = () => {
|
|
442
620
|
const element = document.getElementById(id);
|
|
443
621
|
if (!element) {
|
|
444
|
-
return;
|
|
622
|
+
return null;
|
|
445
623
|
}
|
|
446
624
|
// Rendered inside a closed tab, a folded details, a view that is not the
|
|
447
|
-
// one on screen: the element exists but would show nothing.
|
|
448
|
-
// it is the same wait, for the same reason.
|
|
625
|
+
// one on screen: the element exists but would show nothing.
|
|
449
626
|
if (element.checkVisibility && !element.checkVisibility()) {
|
|
450
|
-
return;
|
|
627
|
+
return null;
|
|
451
628
|
}
|
|
452
|
-
|
|
629
|
+
return element;
|
|
630
|
+
};
|
|
631
|
+
const settle = (element) => {
|
|
453
632
|
stopWaiting();
|
|
454
|
-
|
|
633
|
+
onSettled(element);
|
|
634
|
+
};
|
|
635
|
+
const check = () => {
|
|
636
|
+
if (confirmationPending || documentIsBusySignal.peek() || !findElement()) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
confirmationPending = true;
|
|
640
|
+
whenRenderingResumes(() => {
|
|
641
|
+
if (stopped) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
frame = requestAnimationFrame(() => {
|
|
645
|
+
frame = null;
|
|
646
|
+
confirmationPending = false;
|
|
647
|
+
if (documentIsBusySignal.peek()) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const element = findElement();
|
|
651
|
+
if (element) {
|
|
652
|
+
settle(element);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
});
|
|
455
656
|
};
|
|
456
657
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
mutationObserver = new MutationObserver(checkForElement);
|
|
658
|
+
mutationObserver = new MutationObserver(check);
|
|
659
|
+
// Every attribute, not only `id`: what shows an element that is already
|
|
660
|
+
// there — a <details> opening, a tab panel losing its display: none — is an
|
|
661
|
+
// attribute written somewhere above it, and nothing else says so.
|
|
463
662
|
mutationObserver.observe(document.documentElement, {
|
|
464
663
|
childList: true,
|
|
465
664
|
subtree: true,
|
|
466
665
|
attributes: true,
|
|
467
|
-
attributeFilter: ["id"],
|
|
468
666
|
});
|
|
469
|
-
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
470
667
|
stopWatchingBusy = effect(() => {
|
|
471
668
|
const documentIsBusy = documentIsBusySignal.value;
|
|
472
669
|
clearTimeout(idleTimeout);
|
|
473
|
-
if (
|
|
474
|
-
|
|
670
|
+
if (documentIsBusy) {
|
|
671
|
+
return;
|
|
475
672
|
}
|
|
476
|
-
|
|
477
|
-
|
|
673
|
+
check();
|
|
674
|
+
// The grace is for an element that has not come; one already there and
|
|
675
|
+
// waiting for rendering to resume is not given up on.
|
|
676
|
+
idleTimeout = setTimeout(() => {
|
|
677
|
+
if (!confirmationPending) {
|
|
678
|
+
stopWaiting();
|
|
679
|
+
}
|
|
680
|
+
}, graceAfterIdle);
|
|
681
|
+
});
|
|
682
|
+
// A document that never stops working: the element as it stands, when it is
|
|
683
|
+
// there at all.
|
|
684
|
+
maxWaitTimeout = setTimeout(() => {
|
|
685
|
+
const element = findElement();
|
|
686
|
+
if (element) {
|
|
687
|
+
settle(element);
|
|
688
|
+
} else {
|
|
689
|
+
stopWaiting();
|
|
690
|
+
}
|
|
691
|
+
}, maxWait);
|
|
478
692
|
|
|
479
693
|
return stopWaiting;
|
|
480
694
|
};
|
|
481
695
|
|
|
482
696
|
const revealUrlTarget = (element) => {
|
|
483
697
|
const { block, behavior, markDuration } = urlTargetOptions;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
698
|
+
const prefersReducedMotion = window.matchMedia(
|
|
699
|
+
"(prefers-reduced-motion: reduce)",
|
|
700
|
+
).matches;
|
|
701
|
+
element.scrollIntoView({
|
|
702
|
+
block,
|
|
703
|
+
behavior: prefersReducedMotion ? "instant" : behavior,
|
|
704
|
+
});
|
|
705
|
+
// What the browser does when it handles a fragment itself: keyboard
|
|
706
|
+
// navigation resumes from the target, not from the top of the document.
|
|
707
|
+
if (elementIsFocusable(element)) {
|
|
708
|
+
element.focus({ preventScroll: true });
|
|
709
|
+
}
|
|
710
|
+
element.setAttribute(URL_TARGET_ATTRIBUTE, "");
|
|
711
|
+
setTimeout(() => {
|
|
712
|
+
element.removeAttribute(URL_TARGET_ATTRIBUTE);
|
|
713
|
+
}, markDuration);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
let targetWriteInProgress = false;
|
|
717
|
+
/**
|
|
718
|
+
* True while navi writes the browser's `:target`. The fragment navigation that
|
|
719
|
+
* takes is announced like any other — a popstate, a navigate event, an entry
|
|
720
|
+
* change — and names the address and the state the document is already at, so
|
|
721
|
+
* whoever routes on those events leaves it alone.
|
|
722
|
+
*/
|
|
723
|
+
const isTargetWriteInProgress = () => {
|
|
724
|
+
return targetWriteInProgress;
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// `:target` onto `element`, or off whatever holds it when `element` is null.
|
|
728
|
+
//
|
|
729
|
+
// A replace towards the address the browser is at: the browser answers it as a
|
|
730
|
+
// fragment navigation, and that is the only moment it sets `:target`. It also
|
|
731
|
+
// scrolls to what the fragment designates, resets the entry's states
|
|
732
|
+
// (history.state in Firefox, the Navigation API state everywhere) and fires a
|
|
733
|
+
// popstate (Chrome, Safari) and a navigate event. The address, both states and
|
|
734
|
+
// the scroll are put back in the same task, so nothing of it is painted; a
|
|
735
|
+
// running view transition is not skipped by it (measured in Chrome, Firefox
|
|
736
|
+
// and Safari).
|
|
737
|
+
const writeTarget = (element) => {
|
|
738
|
+
const address = window.location.href;
|
|
739
|
+
const historyState = window.history.state;
|
|
740
|
+
const { navigation } = window;
|
|
741
|
+
const entry = navigation ? navigation.currentEntry : null;
|
|
742
|
+
const entryState = entry ? entry.getState() : undefined;
|
|
743
|
+
const putScrollBack = captureScroll(element);
|
|
744
|
+
const addressFragment = new URL(address).hash.slice(1);
|
|
745
|
+
const addressTargetId = decodeURIComponent(addressFragment);
|
|
746
|
+
|
|
747
|
+
let fragmentUrl;
|
|
748
|
+
let silencedElement = null;
|
|
749
|
+
if (element) {
|
|
750
|
+
fragmentUrl =
|
|
751
|
+
element.id === addressTargetId
|
|
752
|
+
? address
|
|
753
|
+
: `${urlWithoutFragment(address)}#${encodeURIComponent(element.id)}`;
|
|
754
|
+
} else if (addressFragment) {
|
|
755
|
+
// The address itself, so the fragment does not change and no hashchange
|
|
756
|
+
// follows; the element it names briefly without its id, so it designates
|
|
757
|
+
// nothing.
|
|
758
|
+
fragmentUrl = address;
|
|
759
|
+
silencedElement = document.getElementById(addressTargetId);
|
|
760
|
+
} else {
|
|
761
|
+
// An empty fragment designates the top of the document; the scroll it
|
|
762
|
+
// makes is put back with the others.
|
|
763
|
+
fragmentUrl = `${address}#`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
targetWriteInProgress = true;
|
|
767
|
+
try {
|
|
768
|
+
if (silencedElement) {
|
|
769
|
+
silencedElement.removeAttribute("id");
|
|
498
770
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
771
|
+
window.location.replace(fragmentUrl);
|
|
772
|
+
} finally {
|
|
773
|
+
if (silencedElement) {
|
|
774
|
+
silencedElement.id = addressTargetId;
|
|
775
|
+
}
|
|
776
|
+
window.history.replaceState(historyState, null, address);
|
|
777
|
+
if (entry && navigation.currentEntry) {
|
|
778
|
+
navigation.updateCurrentEntry({ state: entryState });
|
|
779
|
+
}
|
|
780
|
+
targetWriteInProgress = false;
|
|
781
|
+
}
|
|
782
|
+
putScrollBack();
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// Every box a fragment navigation towards `element` may scroll: its ancestors,
|
|
786
|
+
// and the document.
|
|
787
|
+
const captureScroll = (element) => {
|
|
788
|
+
const boxes = [];
|
|
789
|
+
let box = element ? element.parentElement : null;
|
|
790
|
+
while (box) {
|
|
791
|
+
boxes.push(box);
|
|
792
|
+
box = box.parentElement;
|
|
793
|
+
}
|
|
794
|
+
const { scrollingElement } = document;
|
|
795
|
+
if (!boxes.includes(scrollingElement)) {
|
|
796
|
+
boxes.push(scrollingElement);
|
|
797
|
+
}
|
|
798
|
+
const positions = boxes.map((box) => {
|
|
799
|
+
return { box, left: box.scrollLeft, top: box.scrollTop };
|
|
503
800
|
});
|
|
801
|
+
return () => {
|
|
802
|
+
for (const { box, left, top } of positions) {
|
|
803
|
+
if (box.scrollLeft !== left || box.scrollTop !== top) {
|
|
804
|
+
box.scrollTo({ left, top, behavior: "instant" });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const urlWithoutFragment = (url) => {
|
|
811
|
+
const urlObject = new URL(url);
|
|
812
|
+
urlObject.hash = "";
|
|
813
|
+
return urlObject.href;
|
|
504
814
|
};
|
|
505
815
|
|
|
506
816
|
effect(() => {
|
|
507
817
|
const documentUrl = documentUrlSignal.value;
|
|
508
|
-
|
|
818
|
+
const isReturn = returningToUrl === documentUrl;
|
|
819
|
+
returningToUrl = null;
|
|
820
|
+
armUrlTarget(documentUrl, { isReturn });
|
|
509
821
|
});
|
|
510
822
|
|
|
511
823
|
/**
|
|
@@ -10103,6 +10415,26 @@ const dispatchRequestInteraction = (
|
|
|
10103
10415
|
});
|
|
10104
10416
|
};
|
|
10105
10417
|
|
|
10418
|
+
/**
|
|
10419
|
+
* Whether the control `element` belongs to lets an interaction of this intent
|
|
10420
|
+
* through, asked ahead of the event the gate will judge, and silently: nothing
|
|
10421
|
+
* is reported, nothing runs. For what a press is about to lead to (see
|
|
10422
|
+
* announceOpeningPress in commands.js) — the press itself is not that event: a
|
|
10423
|
+
* drag source cancels a `pointerdown` whose click still comes and opens.
|
|
10424
|
+
*
|
|
10425
|
+
* @param {Element} element
|
|
10426
|
+
* @param {{ intent: "read" | "write" }} options
|
|
10427
|
+
* @returns {boolean}
|
|
10428
|
+
*/
|
|
10429
|
+
const allowsInteraction = (element, { intent }) => {
|
|
10430
|
+
const controlHost = findControlHost(element) || element;
|
|
10431
|
+
const ci = controlHost.__uiStateController__?.rules.interaction;
|
|
10432
|
+
if (!ci) {
|
|
10433
|
+
return true;
|
|
10434
|
+
}
|
|
10435
|
+
return ci.checkInteractivity({ intent });
|
|
10436
|
+
};
|
|
10437
|
+
|
|
10106
10438
|
const onRequestInteraction = (
|
|
10107
10439
|
requestInteractionCustomEvent,
|
|
10108
10440
|
{ debugInteraction },
|
|
@@ -17751,6 +18083,33 @@ const syncOwnedResourceToSignals = (
|
|
|
17751
18083
|
|
|
17752
18084
|
// Global signal registry for route template detection
|
|
17753
18085
|
const globalSignalRegistry = new Map();
|
|
18086
|
+
|
|
18087
|
+
// The values some states are about to hold, answered in place of what they
|
|
18088
|
+
// hold for the length of one readStateAsIf.
|
|
18089
|
+
let valuesAsIf = null;
|
|
18090
|
+
/**
|
|
18091
|
+
* Runs `read` as if each state in `values` held the value given for it: the
|
|
18092
|
+
* address a press is about to write, read before it is written. Nothing is
|
|
18093
|
+
* written, so whoever watches those states hears nothing, and `read`
|
|
18094
|
+
* subscribes to nothing.
|
|
18095
|
+
*
|
|
18096
|
+
* Only a read of the state itself is answered this way. A `computed` derived
|
|
18097
|
+
* from one answers what it last computed — or, evaluated for the first time
|
|
18098
|
+
* inside `read`, caches a value derived from this answer: `read` reads the
|
|
18099
|
+
* states it depends on directly (a route action's params do).
|
|
18100
|
+
*
|
|
18101
|
+
* @param {Map<import("@preact/signals").Signal, any>} values
|
|
18102
|
+
* @param {() => any} read
|
|
18103
|
+
*/
|
|
18104
|
+
const readStateAsIf = (values, read) => {
|
|
18105
|
+
const previous = valuesAsIf;
|
|
18106
|
+
valuesAsIf = values;
|
|
18107
|
+
try {
|
|
18108
|
+
return untracked(read);
|
|
18109
|
+
} finally {
|
|
18110
|
+
valuesAsIf = previous;
|
|
18111
|
+
}
|
|
18112
|
+
};
|
|
17754
18113
|
let signalIdCounter = 0;
|
|
17755
18114
|
const generateSignalId = () => {
|
|
17756
18115
|
const id = signalIdCounter++;
|
|
@@ -18074,6 +18433,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18074
18433
|
const valueDescriptor = Object.getOwnPropertyDescriptor(signalProto, "value");
|
|
18075
18434
|
Object.defineProperty(preactSignal, "value", {
|
|
18076
18435
|
get() {
|
|
18436
|
+
if (valuesAsIf !== null && valuesAsIf.has(preactSignal)) {
|
|
18437
|
+
return valuesAsIf.get(preactSignal);
|
|
18438
|
+
}
|
|
18077
18439
|
return valueDescriptor.get.call(preactSignal);
|
|
18078
18440
|
},
|
|
18079
18441
|
set(newValue) {
|
|
@@ -26332,7 +26694,8 @@ const route = (
|
|
|
26332
26694
|
};
|
|
26333
26695
|
// Asks ahead of the arrival for what this route's actions ask without the
|
|
26334
26696
|
// 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
|
|
26697
|
+
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
26698
|
+
// (preloadState is the one that has one).
|
|
26336
26699
|
route.preload = () => {
|
|
26337
26700
|
const preloadSet = routePreloadMap.get(route);
|
|
26338
26701
|
if (!preloadSet) {
|
|
@@ -26899,6 +27262,40 @@ const preloadUrl = (url) => {
|
|
|
26899
27262
|
}
|
|
26900
27263
|
}
|
|
26901
27264
|
};
|
|
27265
|
+
|
|
27266
|
+
const routeStatePreloadMap = new WeakMap();
|
|
27267
|
+
const registerRouteStatePreload = (route, preload) => {
|
|
27268
|
+
let preloadSet = routeStatePreloadMap.get(route);
|
|
27269
|
+
if (!preloadSet) {
|
|
27270
|
+
preloadSet = new Set();
|
|
27271
|
+
routeStatePreloadMap.set(route, preloadSet);
|
|
27272
|
+
}
|
|
27273
|
+
preloadSet.add(preload);
|
|
27274
|
+
};
|
|
27275
|
+
/**
|
|
27276
|
+
* Preloads what the routes one is on will ask for once `signal` holds
|
|
27277
|
+
* `value`: the address a popup's opening is about to write (see
|
|
27278
|
+
* open_controller.js), known at the press that opens it. Only the routes
|
|
27279
|
+
* matching now are asked — writing a state keeps one on the same page.
|
|
27280
|
+
*/
|
|
27281
|
+
const preloadState = (signal, value) => {
|
|
27282
|
+
if (!activeRouteSet) {
|
|
27283
|
+
return;
|
|
27284
|
+
}
|
|
27285
|
+
const values = new Map([[signal, value]]);
|
|
27286
|
+
for (const route of activeRouteSet) {
|
|
27287
|
+
if (!route.matchingSignal.peek()) {
|
|
27288
|
+
continue;
|
|
27289
|
+
}
|
|
27290
|
+
const preloadSet = routeStatePreloadMap.get(route);
|
|
27291
|
+
if (!preloadSet) {
|
|
27292
|
+
continue;
|
|
27293
|
+
}
|
|
27294
|
+
for (const preload of preloadSet) {
|
|
27295
|
+
preload(values);
|
|
27296
|
+
}
|
|
27297
|
+
}
|
|
27298
|
+
};
|
|
26902
27299
|
const setupRoutes = (routes) => {
|
|
26903
27300
|
if (setupRoutesCalled) {
|
|
26904
27301
|
throw new Error(
|
|
@@ -27473,112 +27870,6 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
27473
27870
|
return stateWithoutGeneratedIdKeys;
|
|
27474
27871
|
};
|
|
27475
27872
|
|
|
27476
|
-
/**
|
|
27477
|
-
* The document's rendering, held for the one frame a view transition needs.
|
|
27478
|
-
*
|
|
27479
|
-
* The browser does not take the picture of the page being left when a
|
|
27480
|
-
* transition is ASKED for — it takes it at the next frame, just before running
|
|
27481
|
-
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
27482
|
-
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
27483
|
-
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
27484
|
-
* Both sides of the animation then show it, and one watches a page slide onto
|
|
27485
|
-
* itself.
|
|
27486
|
-
*
|
|
27487
|
-
* So what Preact has queued waits until the update callback, which is the
|
|
27488
|
-
* moment the API is built around — the change belongs inside it. The whole
|
|
27489
|
-
* document is held: it is about to be frozen under a picture anyway.
|
|
27490
|
-
*
|
|
27491
|
-
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
27492
|
-
* around Preact's `options.debounceRendering`, and two of them installed
|
|
27493
|
-
* independently restore each other in the wrong order when they let go — every
|
|
27494
|
-
* render queued in between is then handed to a wrapper nobody will ever
|
|
27495
|
-
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
27496
|
-
* route transition) must therefore hold through this module, never through a
|
|
27497
|
-
* wrapper of its own.
|
|
27498
|
-
*/
|
|
27499
|
-
|
|
27500
|
-
|
|
27501
|
-
let renderingHold = null;
|
|
27502
|
-
const holdRendering = () => {
|
|
27503
|
-
if (renderingHold) {
|
|
27504
|
-
return renderingHold.release;
|
|
27505
|
-
}
|
|
27506
|
-
const debounceRenderingBefore = options.debounceRendering;
|
|
27507
|
-
const hold = {
|
|
27508
|
-
render: null,
|
|
27509
|
-
waiting: [],
|
|
27510
|
-
release: () => {
|
|
27511
|
-
// Only the hold that is still standing may be given back: a holder
|
|
27512
|
-
// releasing after another has taken over must not let go of what it
|
|
27513
|
-
// does not hold.
|
|
27514
|
-
if (renderingHold !== hold) {
|
|
27515
|
-
return;
|
|
27516
|
-
}
|
|
27517
|
-
renderingHold = null;
|
|
27518
|
-
options.debounceRendering = debounceRenderingBefore;
|
|
27519
|
-
const { render, waiting } = hold;
|
|
27520
|
-
hold.render = null;
|
|
27521
|
-
hold.waiting = [];
|
|
27522
|
-
if (render) {
|
|
27523
|
-
render();
|
|
27524
|
-
}
|
|
27525
|
-
for (const wait of waiting) {
|
|
27526
|
-
wait();
|
|
27527
|
-
}
|
|
27528
|
-
},
|
|
27529
|
-
};
|
|
27530
|
-
renderingHold = hold;
|
|
27531
|
-
options.debounceRendering = (render) => {
|
|
27532
|
-
hold.render = render;
|
|
27533
|
-
};
|
|
27534
|
-
return hold.release;
|
|
27535
|
-
};
|
|
27536
|
-
|
|
27537
|
-
// Anything else that must not happen before the picture is taken, and the
|
|
27538
|
-
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
27539
|
-
// document put back to its top while the page being left is still on screen is
|
|
27540
|
-
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
27541
|
-
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
27542
|
-
// the band it had already painted — the page being left is then seen in
|
|
27543
|
-
// fragments, whatever the movement does afterwards.
|
|
27544
|
-
//
|
|
27545
|
-
// Run at once when nobody is photographing anything, which is the common case
|
|
27546
|
-
// and must stay free.
|
|
27547
|
-
const whenRenderingResumes = (callback) => {
|
|
27548
|
-
if (!renderingHold) {
|
|
27549
|
-
callback();
|
|
27550
|
-
return;
|
|
27551
|
-
}
|
|
27552
|
-
renderingHold.waiting.push(callback);
|
|
27553
|
-
};
|
|
27554
|
-
|
|
27555
|
-
// The hold a navigation takes on its way in — from before its first write,
|
|
27556
|
-
// because by the time a route announces that it matches, Preact has already
|
|
27557
|
-
// been told and the render is queued; a hold taken then is a hold taken too
|
|
27558
|
-
// late. Kept here until whoever animates the change takes it over, or the
|
|
27559
|
-
// navigation turns out to be one nobody animates.
|
|
27560
|
-
let routingRenderingHold = null;
|
|
27561
|
-
const holdRenderingForRouting = () => {
|
|
27562
|
-
routingRenderingHold = holdRendering();
|
|
27563
|
-
};
|
|
27564
|
-
// Nobody had a picture to take: a page held for a change it does not animate
|
|
27565
|
-
// is a page that stutters for nothing.
|
|
27566
|
-
const releaseRoutingRenderingHold = () => {
|
|
27567
|
-
const release = routingRenderingHold;
|
|
27568
|
-
routingRenderingHold = null;
|
|
27569
|
-
if (release) {
|
|
27570
|
-
release();
|
|
27571
|
-
}
|
|
27572
|
-
};
|
|
27573
|
-
// An animator takes the navigation's hold as its own — taking another would be
|
|
27574
|
-
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
27575
|
-
// the change it animates is not a navigation.
|
|
27576
|
-
const takeoverRoutingRenderingHold = () => {
|
|
27577
|
-
const release = routingRenderingHold || holdRendering();
|
|
27578
|
-
routingRenderingHold = null;
|
|
27579
|
-
return release;
|
|
27580
|
-
};
|
|
27581
|
-
|
|
27582
27873
|
/**
|
|
27583
27874
|
* Where a page was left, given back when one comes back to it.
|
|
27584
27875
|
*
|
|
@@ -27779,6 +28070,11 @@ const restoreScrollPosition = (url) => {
|
|
|
27779
28070
|
// So `from`, the url being left, tells an arrival from the same place said
|
|
27780
28071
|
// differently, and only the first is moved.
|
|
27781
28072
|
//
|
|
28073
|
+
// A fragment does not make it less of an arrival. The browser finds nothing to
|
|
28074
|
+
// scroll to after a pushState; url_target.js brings the element in once it
|
|
28075
|
+
// renders, from this top — and when it never comes, the page is read from its
|
|
28076
|
+
// top like any other.
|
|
28077
|
+
//
|
|
27782
28078
|
// The document, because the document is the scrollport in the common case. An
|
|
27783
28079
|
// app that scrolls an element of its own scrolls it itself.
|
|
27784
28080
|
const startAtTop = (url, { from } = {}) => {
|
|
@@ -27788,14 +28084,10 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
27788
28084
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27789
28085
|
};
|
|
27790
28086
|
const isArrival = (url, { from }) => {
|
|
27791
|
-
const urlObject = new URL(url, window.location.href);
|
|
27792
|
-
// A fragment names where to land, and the browser is the one that finds it.
|
|
27793
|
-
if (urlObject.hash) {
|
|
27794
|
-
return false;
|
|
27795
|
-
}
|
|
27796
28087
|
if (
|
|
27797
28088
|
from !== undefined &&
|
|
27798
|
-
new URL(from, window.location.href).pathname ===
|
|
28089
|
+
new URL(from, window.location.href).pathname ===
|
|
28090
|
+
new URL(url, window.location.href).pathname
|
|
27799
28091
|
) {
|
|
27800
28092
|
return false;
|
|
27801
28093
|
}
|
|
@@ -27872,29 +28164,6 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
27872
28164
|
}
|
|
27873
28165
|
};
|
|
27874
28166
|
|
|
27875
|
-
/**
|
|
27876
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27877
|
-
*
|
|
27878
|
-
* Everything else a router says arrives once the change is made: a route
|
|
27879
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
27880
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
27881
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
27882
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
27883
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
27884
|
-
*
|
|
27885
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
27886
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
27887
|
-
* before the URL, the visited set, or any route has changed.
|
|
27888
|
-
*
|
|
27889
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
27890
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
27891
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
27892
|
-
*/
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
27896
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
27897
|
-
|
|
27898
28167
|
const getHrefTargetInfo = (href) => {
|
|
27899
28168
|
href = String(href);
|
|
27900
28169
|
|
|
@@ -28402,6 +28671,11 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28402
28671
|
});
|
|
28403
28672
|
|
|
28404
28673
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
28674
|
+
if (isTargetWriteInProgress()) {
|
|
28675
|
+
// The fragment navigation navi makes to set `:target` (see
|
|
28676
|
+
// url_target.js): the entry keeps its address and its state.
|
|
28677
|
+
return;
|
|
28678
|
+
}
|
|
28405
28679
|
// The entry a pending replace was for is no longer the current one, and
|
|
28406
28680
|
// the History API has no way to write it (see replaceAddressWhenSettled).
|
|
28407
28681
|
dropPendingReplace();
|
|
@@ -28477,9 +28751,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28477
28751
|
// one), so whoever awaits this reads a document url and state that
|
|
28478
28752
|
// already say where it landed.
|
|
28479
28753
|
const landedPromise = new Promise((resolve) => {
|
|
28480
|
-
|
|
28481
|
-
|
|
28482
|
-
|
|
28754
|
+
const onPopstate = () => {
|
|
28755
|
+
if (isTargetWriteInProgress()) {
|
|
28756
|
+
return;
|
|
28757
|
+
}
|
|
28758
|
+
window.removeEventListener("popstate", onPopstate);
|
|
28759
|
+
resolve(true);
|
|
28760
|
+
};
|
|
28761
|
+
window.addEventListener("popstate", onPopstate);
|
|
28483
28762
|
});
|
|
28484
28763
|
landOnPending = landOn || null;
|
|
28485
28764
|
window.history.back();
|
|
@@ -33300,10 +33579,15 @@ const RouteTravel = ({
|
|
|
33300
33579
|
// stopping. Which end decides how it is answered, and the two are not the
|
|
33301
33580
|
// same amount of work.
|
|
33302
33581
|
onEdge: ({
|
|
33303
|
-
sign
|
|
33582
|
+
sign,
|
|
33583
|
+
thrown
|
|
33304
33584
|
}) => {
|
|
33305
33585
|
const travel = travelRef.current;
|
|
33306
|
-
if (
|
|
33586
|
+
if (
|
|
33587
|
+
// Thrown on before its end: handing over from there would mean jumping
|
|
33588
|
+
// the pictures the rest of the way first (see scrubTravel below), a
|
|
33589
|
+
// page seen leaping. The travel caught simply arrives.
|
|
33590
|
+
thrown || !travel || travel.noPicture || travel.ended || travel.reverting || !CAN_KEEP_PICTURE) {
|
|
33307
33591
|
return false;
|
|
33308
33592
|
}
|
|
33309
33593
|
// Where the page in hand is coming from, said the way the gesture says
|
|
@@ -33880,10 +34164,14 @@ const writeCover = (edge, element, size) => {
|
|
|
33880
34164
|
* the address, which is also what makes it worth fetching AHEAD of the
|
|
33881
34165
|
* arrival: a link to the route preruns it when the pointer or the focus
|
|
33882
34166
|
* reaches the link (see docs/dynamic_import.md). An action whose params
|
|
33883
|
-
* come from the address is
|
|
34167
|
+
* come from the address is not prefetched by a link — a link's prefetch has
|
|
34168
|
+
* no address. It is by the press that opens a popup bound to a state these
|
|
34169
|
+
* params read: that address is the current one with the state set to the
|
|
34170
|
+
* popup's value (see `prefetch` on Button).
|
|
33884
34171
|
* @param {object} [options]
|
|
33885
|
-
* @param {boolean} [options.prefetch=true] - `false` keeps
|
|
33886
|
-
*
|
|
34172
|
+
* @param {boolean} [options.prefetch=true] - `false` keeps the action from
|
|
34173
|
+
* being prerun ahead of what asks for it, when the read is not worth a
|
|
34174
|
+
* hover or a press that may not end in an opening.
|
|
33887
34175
|
*/
|
|
33888
34176
|
const routeAction = (
|
|
33889
34177
|
routeOrRoutes,
|
|
@@ -33912,27 +34200,47 @@ const routeAction = (
|
|
|
33912
34200
|
},
|
|
33913
34201
|
options,
|
|
33914
34202
|
);
|
|
33915
|
-
if (
|
|
34203
|
+
if (prefetch) {
|
|
33916
34204
|
for (const route of routes) {
|
|
33917
|
-
|
|
33918
|
-
|
|
33919
|
-
|
|
34205
|
+
if (paramsEffect) {
|
|
34206
|
+
registerRouteStatePreload(route, (values) => {
|
|
34207
|
+
prefetchAsIf(action, paramsEffect, values);
|
|
34208
|
+
});
|
|
34209
|
+
} else {
|
|
34210
|
+
registerRoutePreload(route, () => {
|
|
34211
|
+
prerunAhead(action.bindParams(true));
|
|
34212
|
+
});
|
|
34213
|
+
}
|
|
33920
34214
|
}
|
|
33921
34215
|
}
|
|
33922
34216
|
return actionBoundToRoute;
|
|
33923
34217
|
};
|
|
33924
34218
|
|
|
33925
|
-
// The
|
|
33926
|
-
// (
|
|
33927
|
-
//
|
|
33928
|
-
// and
|
|
33929
|
-
|
|
33930
|
-
|
|
33931
|
-
|
|
33932
|
-
|
|
33933
|
-
|
|
33934
|
-
|
|
33935
|
-
|
|
34219
|
+
// The params as they will read once the states hold what they are about to
|
|
34220
|
+
// (see readStateAsIf), prerun only when that changes them: params the state
|
|
34221
|
+
// leaves alone are the ones the action already runs on — or is meant not to,
|
|
34222
|
+
// and a prerun would retry a FAILED one behind the screen's back.
|
|
34223
|
+
const prefetchAsIf = (action, paramsEffect, values) => {
|
|
34224
|
+
const params = readStateAsIf(values, paramsEffect);
|
|
34225
|
+
if (!params) {
|
|
34226
|
+
return;
|
|
34227
|
+
}
|
|
34228
|
+
if (compareTwoJsValues(params, untracked(paramsEffect))) {
|
|
34229
|
+
return;
|
|
34230
|
+
}
|
|
34231
|
+
prerunAhead(action.bindParams(params));
|
|
34232
|
+
};
|
|
34233
|
+
|
|
34234
|
+
// The instance prerun is the one the arrival runs on — the one bound to the
|
|
34235
|
+
// params it reads (`true` for a param-less action) — so the arrival promotes
|
|
34236
|
+
// it to a run. A prefetch that fails is not reported: nothing on screen asked
|
|
34237
|
+
// for it, and the failure stays on the instance, where a run asks again — a
|
|
34238
|
+
// FAILED action is run, only RUNNING and COMPLETED are left alone — and the
|
|
34239
|
+
// arrival shows its own. It is left FAILED on purpose, not reset: a prerun is
|
|
34240
|
+
// often still in flight when the arrival promotes it (the press that brings
|
|
34241
|
+
// the arrival focuses the link, or is the one that asked), and a reset
|
|
34242
|
+
// landing after that would pull the answer from under the page reading it.
|
|
34243
|
+
const prerunAhead = (instance) => {
|
|
33936
34244
|
let result;
|
|
33937
34245
|
try {
|
|
33938
34246
|
result = instance.prerun({ reason: "intent" });
|
|
@@ -34930,6 +35238,30 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
34930
35238
|
},
|
|
34931
35239
|
};
|
|
34932
35240
|
});
|
|
35241
|
+
/**
|
|
35242
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
35243
|
+
* release that runs the command: what the opening will read can be asked for
|
|
35244
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
35245
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
35246
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
35247
|
+
* press is the first thing the finger says anyway.
|
|
35248
|
+
*
|
|
35249
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
35250
|
+
* runs here, and a source with no target says so at the release.
|
|
35251
|
+
*/
|
|
35252
|
+
const announceOpeningPress = (source, command) => {
|
|
35253
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
35254
|
+
return;
|
|
35255
|
+
}
|
|
35256
|
+
const commandFor = source.getAttribute("commandfor");
|
|
35257
|
+
const target = commandFor
|
|
35258
|
+
? document.getElementById(commandFor)
|
|
35259
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
35260
|
+
if (!target) {
|
|
35261
|
+
return;
|
|
35262
|
+
}
|
|
35263
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
35264
|
+
};
|
|
34933
35265
|
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
34934
35266
|
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
34935
35267
|
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
@@ -36383,7 +36715,7 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
36383
36715
|
return;
|
|
36384
36716
|
}
|
|
36385
36717
|
if (opened) {
|
|
36386
|
-
signal.value = popupValue
|
|
36718
|
+
signal.value = readOpenValue(popupValue);
|
|
36387
36719
|
return;
|
|
36388
36720
|
}
|
|
36389
36721
|
const closedValue = readClosedValue(signalValue);
|
|
@@ -36415,6 +36747,9 @@ const readOpened = (signalValue, popupValue) => {
|
|
|
36415
36747
|
}
|
|
36416
36748
|
return signalValue === popupValue;
|
|
36417
36749
|
};
|
|
36750
|
+
const readOpenValue = (popupValue) => {
|
|
36751
|
+
return popupValue === undefined ? true : popupValue;
|
|
36752
|
+
};
|
|
36418
36753
|
const readClosedValue = (openSignalValue) => {
|
|
36419
36754
|
return openSignalValue === true ? false : undefined;
|
|
36420
36755
|
};
|
|
@@ -36488,6 +36823,29 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
36488
36823
|
}
|
|
36489
36824
|
}
|
|
36490
36825
|
: null;
|
|
36826
|
+
// A press that will open this popup has started (see announceOpeningPress
|
|
36827
|
+
// in commands.js). What the opening writes — the popup's value, in its
|
|
36828
|
+
// signal — is known before the release, so what the screen reads off it is
|
|
36829
|
+
// asked for now (see preloadState).
|
|
36830
|
+
useEffect(() => {
|
|
36831
|
+
if (!signal) {
|
|
36832
|
+
return undefined;
|
|
36833
|
+
}
|
|
36834
|
+
const element = openController.getElement?.();
|
|
36835
|
+
if (!element) {
|
|
36836
|
+
return undefined;
|
|
36837
|
+
}
|
|
36838
|
+
const onOpenPress = () => {
|
|
36839
|
+
if (openController.opened) {
|
|
36840
|
+
return;
|
|
36841
|
+
}
|
|
36842
|
+
preloadState(signal, readOpenValue(value));
|
|
36843
|
+
};
|
|
36844
|
+
element.addEventListener("navi_open_press", onOpenPress);
|
|
36845
|
+
return () => {
|
|
36846
|
+
element.removeEventListener("navi_open_press", onOpenPress);
|
|
36847
|
+
};
|
|
36848
|
+
}, [signal, value]);
|
|
36491
36849
|
// Tracks whether the effect below has ever run before — only the very
|
|
36492
36850
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
36493
36851
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -37182,17 +37540,36 @@ const armOutsidePressClose = (
|
|
|
37182
37540
|
* content ahead of the click that will open it. Deferring the build to the
|
|
37183
37541
|
* opening puts its whole cost in the frame right after the click — the frame
|
|
37184
37542
|
* 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
|
-
*
|
|
37543
|
+
* trigger and pressing it are free. The warming render starts at the frame
|
|
37544
|
+
* after the one that follows the intent: nothing here needs the content in the
|
|
37545
|
+
* DOM before the click, only before the open that follows it, and on a touch
|
|
37546
|
+
* screen `pointerenter` arrives together with the `pointerdown` — a build
|
|
37547
|
+
* started there would hold back the frame that shows the press. Two animation
|
|
37548
|
+
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
37549
|
+
* and a timeout started by the press fires at its release, on top of the click.
|
|
37550
|
+
*
|
|
37551
|
+
* "while-opened" content is warmed by an opening press, never by a hover or a
|
|
37552
|
+
* focus. That mode promises content built fresh for the gesture that opens
|
|
37553
|
+
* it, and mounted only while that gesture and its opening last: callers lean
|
|
37554
|
+
* on it (several pickers sharing one set of content ids, because only one
|
|
37555
|
+
* content exists at a time). A pointer crossing four triggers would build four
|
|
37556
|
+
* contents; a press is the start of one opening.
|
|
37557
|
+
*
|
|
37558
|
+
* Only what opens the popup can say its press is one, and it says so as the
|
|
37559
|
+
* press starts, with a `navi_open_press` on the popup (see
|
|
37560
|
+
* announceOpeningPress in commands.js): a `--navi-open` button, a picker's
|
|
37561
|
+
* trigger when a press is what opens it, an expandable's UI part. Not the
|
|
37562
|
+
* anchor: it says where the popup is placed, not what opens it — a card
|
|
37563
|
+
* opened by a hold is pressed all day by taps meant for what it holds.
|
|
37564
|
+
*
|
|
37565
|
+
* The press builds the content, and the press ending without an open throws
|
|
37566
|
+
* it away — its click reaching the document with the popup still closed, no
|
|
37567
|
+
* click coming after the release, the browser taking the gesture
|
|
37568
|
+
* (`pointercancel`), or the next press starting, before any of its own
|
|
37569
|
+
* handlers run: one press at a time holds a content. A keyboard opening builds
|
|
37570
|
+
* at open time. A popup with an `onOpen` is never warmed: that callback runs
|
|
37571
|
+
* before the content is built (see open_controller.js), and a content seeding
|
|
37572
|
+
* itself from what it writes must be built after it.
|
|
37196
37573
|
*/
|
|
37197
37574
|
|
|
37198
37575
|
|
|
@@ -37234,26 +37611,35 @@ const usePopupContentMount = (
|
|
|
37234
37611
|
{ mount = MOUNT_DEFAULT, anchor },
|
|
37235
37612
|
) => {
|
|
37236
37613
|
const mountedAlways = mount === "always";
|
|
37237
|
-
const [contentMounted,
|
|
37614
|
+
const [contentMounted, setContentMountedState] = useState(
|
|
37238
37615
|
() => mountedAlways || openController.opened,
|
|
37239
37616
|
);
|
|
37240
|
-
|
|
37241
|
-
|
|
37242
|
-
|
|
37243
|
-
|
|
37244
|
-
|
|
37245
|
-
|
|
37246
|
-
|
|
37247
|
-
|
|
37248
|
-
|
|
37249
|
-
|
|
37250
|
-
|
|
37251
|
-
|
|
37252
|
-
|
|
37253
|
-
|
|
37254
|
-
|
|
37255
|
-
|
|
37256
|
-
|
|
37617
|
+
// What was last asked for, ahead of the render that performs it: an open
|
|
37618
|
+
// landing in between (a press-warmed content given up just before the open
|
|
37619
|
+
// that wanted it) must still find it has to build.
|
|
37620
|
+
const contentMountedRef = useRef(contentMounted);
|
|
37621
|
+
const setContentMounted = (value) => {
|
|
37622
|
+
contentMountedRef.current = value;
|
|
37623
|
+
setContentMountedState(value);
|
|
37624
|
+
};
|
|
37625
|
+
openController.mountContent = () => {
|
|
37626
|
+
if (contentMountedRef.current) {
|
|
37627
|
+
return;
|
|
37628
|
+
}
|
|
37629
|
+
const popupElement = ref?.current;
|
|
37630
|
+
if (popupElement) {
|
|
37631
|
+
popupsMountingContentForOpen.add(popupElement);
|
|
37632
|
+
}
|
|
37633
|
+
try {
|
|
37634
|
+
flushSyncRendering(() => {
|
|
37635
|
+
setContentMounted(true);
|
|
37636
|
+
});
|
|
37637
|
+
} finally {
|
|
37638
|
+
if (popupElement) {
|
|
37639
|
+
popupsMountingContentForOpen.delete(popupElement);
|
|
37640
|
+
}
|
|
37641
|
+
}
|
|
37642
|
+
};
|
|
37257
37643
|
openController.unmountContent =
|
|
37258
37644
|
mount === "while-opened"
|
|
37259
37645
|
? () => {
|
|
@@ -37291,41 +37677,140 @@ const usePopupContentMount = (
|
|
|
37291
37677
|
cancelIdle(idleId);
|
|
37292
37678
|
};
|
|
37293
37679
|
}, [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.
|
|
37680
|
+
// Warm on intent (see the top comment).
|
|
37299
37681
|
useEffect(() => {
|
|
37300
37682
|
if (contentMounted || !anchor || mount === "while-opened") {
|
|
37301
37683
|
return undefined;
|
|
37302
37684
|
}
|
|
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;
|
|
37685
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
37312
37686
|
if (!anchorElement) {
|
|
37313
37687
|
return undefined;
|
|
37314
37688
|
}
|
|
37689
|
+
let cancelWarm = null;
|
|
37315
37690
|
const warm = () => {
|
|
37316
|
-
|
|
37691
|
+
if (cancelWarm) {
|
|
37692
|
+
return;
|
|
37693
|
+
}
|
|
37694
|
+
cancelWarm = requestFrameAfterNext(() => {
|
|
37695
|
+
setContentMounted(true);
|
|
37696
|
+
});
|
|
37317
37697
|
};
|
|
37318
37698
|
anchorElement.addEventListener("pointerenter", warm);
|
|
37319
37699
|
anchorElement.addEventListener("focusin", warm);
|
|
37320
37700
|
return () => {
|
|
37701
|
+
cancelWarm?.();
|
|
37321
37702
|
anchorElement.removeEventListener("pointerenter", warm);
|
|
37322
37703
|
anchorElement.removeEventListener("focusin", warm);
|
|
37323
37704
|
};
|
|
37324
37705
|
}, [contentMounted, anchor, mount]);
|
|
37706
|
+
// Warm on an opening press, for "while-opened" (see the top comment).
|
|
37707
|
+
// Listening for as long as the popup is there, not only while the content is
|
|
37708
|
+
// unmounted: the press that throws a warmed content away can be a new
|
|
37709
|
+
// opening press of this same popup, and it must warm again.
|
|
37710
|
+
useEffect(() => {
|
|
37711
|
+
if (mount !== "while-opened") {
|
|
37712
|
+
return undefined;
|
|
37713
|
+
}
|
|
37714
|
+
const element = openController.getElement?.();
|
|
37715
|
+
if (!element) {
|
|
37716
|
+
return undefined;
|
|
37717
|
+
}
|
|
37718
|
+
let stopPressWarm = null;
|
|
37719
|
+
const onOpeningPress = () => {
|
|
37720
|
+
if (contentMountedRef.current || openController.onOpen) {
|
|
37721
|
+
return;
|
|
37722
|
+
}
|
|
37723
|
+
const cancelBuild = requestFrameAfterNext(() => {
|
|
37724
|
+
setContentMounted(true);
|
|
37725
|
+
});
|
|
37726
|
+
const stopWatching = watchPressEnd(() => {
|
|
37727
|
+
stopPressWarm = null;
|
|
37728
|
+
cancelBuild();
|
|
37729
|
+
if (!openController.opened) {
|
|
37730
|
+
setContentMounted(false);
|
|
37731
|
+
}
|
|
37732
|
+
});
|
|
37733
|
+
stopPressWarm = () => {
|
|
37734
|
+
cancelBuild();
|
|
37735
|
+
stopWatching();
|
|
37736
|
+
};
|
|
37737
|
+
};
|
|
37738
|
+
element.addEventListener("navi_open_press", onOpeningPress);
|
|
37739
|
+
return () => {
|
|
37740
|
+
stopPressWarm?.();
|
|
37741
|
+
element.removeEventListener("navi_open_press", onOpeningPress);
|
|
37742
|
+
};
|
|
37743
|
+
}, [mount]);
|
|
37325
37744
|
|
|
37326
37745
|
return contentMounted;
|
|
37327
37746
|
};
|
|
37328
37747
|
|
|
37748
|
+
// The anchor accepts the same shapes Popover resolves at open time — a string
|
|
37749
|
+
// id, a ref, an element — but is resolved at effect time: an id that matches
|
|
37750
|
+
// nothing yet simply doesn't warm, the open still mounts the content.
|
|
37751
|
+
const resolveAnchorElement = (anchor) => {
|
|
37752
|
+
if (typeof anchor === "string") {
|
|
37753
|
+
return document.getElementById(anchor);
|
|
37754
|
+
}
|
|
37755
|
+
// A ref is unwrapped even when it holds nothing: an expandable with no UI
|
|
37756
|
+
// part hands an empty ref over, and the ref object itself is truthy — it
|
|
37757
|
+
// would reach addEventListener and throw.
|
|
37758
|
+
if ("current" in anchor) {
|
|
37759
|
+
return anchor.current;
|
|
37760
|
+
}
|
|
37761
|
+
return anchor;
|
|
37762
|
+
};
|
|
37763
|
+
|
|
37764
|
+
// The next frame paints what the input just changed (the pressed trigger); the
|
|
37765
|
+
// callback runs at the start of the one after.
|
|
37766
|
+
const requestFrameAfterNext = (callback) => {
|
|
37767
|
+
let frame = requestAnimationFrame(() => {
|
|
37768
|
+
frame = requestAnimationFrame(callback);
|
|
37769
|
+
});
|
|
37770
|
+
return () => {
|
|
37771
|
+
cancelAnimationFrame(frame);
|
|
37772
|
+
};
|
|
37773
|
+
};
|
|
37774
|
+
|
|
37775
|
+
// How long the click of a released press may take to arrive. Nothing tells a
|
|
37776
|
+
// page that a release brings no click — a press that became a drag has its
|
|
37777
|
+
// click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
|
|
37778
|
+
// press is taken to have ended without one. Longer than the 300ms a browser
|
|
37779
|
+
// still waits before the click of a tap on a page not sized for phones.
|
|
37780
|
+
const PRESS_CLICK_WAIT_MS = 500;
|
|
37781
|
+
|
|
37782
|
+
// Installed during the press's own pointerdown: the capture listeners below
|
|
37783
|
+
// only hear the presses after it.
|
|
37784
|
+
const watchPressEnd = (onEnd) => {
|
|
37785
|
+
let timeout;
|
|
37786
|
+
const stop = () => {
|
|
37787
|
+
clearTimeout(timeout);
|
|
37788
|
+
document.removeEventListener("click", onClick);
|
|
37789
|
+
document.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
37790
|
+
document.removeEventListener("pointercancel", end, { capture: true });
|
|
37791
|
+
document.removeEventListener("pointerdown", end, { capture: true });
|
|
37792
|
+
};
|
|
37793
|
+
const end = () => {
|
|
37794
|
+
stop();
|
|
37795
|
+
onEnd();
|
|
37796
|
+
};
|
|
37797
|
+
// Bubble phase, on the document: every click handler that could open the
|
|
37798
|
+
// popup has run by then.
|
|
37799
|
+
const onClick = (clickEvent) => {
|
|
37800
|
+
if (isPressDrivenClick(clickEvent)) {
|
|
37801
|
+
end();
|
|
37802
|
+
}
|
|
37803
|
+
};
|
|
37804
|
+
const onPointerUp = () => {
|
|
37805
|
+
timeout = setTimeout(end, PRESS_CLICK_WAIT_MS);
|
|
37806
|
+
};
|
|
37807
|
+
document.addEventListener("click", onClick);
|
|
37808
|
+
document.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
37809
|
+
document.addEventListener("pointercancel", end, { capture: true });
|
|
37810
|
+
document.addEventListener("pointerdown", end, { capture: true });
|
|
37811
|
+
return stop;
|
|
37812
|
+
};
|
|
37813
|
+
|
|
37329
37814
|
/**
|
|
37330
37815
|
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
37331
37816
|
* or popover visibility.
|
|
@@ -38381,6 +38866,9 @@ const useUIStateController = (
|
|
|
38381
38866
|
// The suggestion this control started on — what tells a field showing
|
|
38382
38867
|
// its default from one carrying an answer (see isUIStateHeld).
|
|
38383
38868
|
defaultValue: controlInfo.defaultValue,
|
|
38869
|
+
// The default last put on screen, which lags `defaultValue` while an
|
|
38870
|
+
// edit holds a newer one back (see followDefaultValue).
|
|
38871
|
+
defaultValueTaken: controlInfo.defaultValue,
|
|
38384
38872
|
|
|
38385
38873
|
facadeChild: null,
|
|
38386
38874
|
// Set for the duration of one interaction by whatever wants the
|
|
@@ -38482,6 +38970,18 @@ const useUIStateController = (
|
|
|
38482
38970
|
e.currentTarget || controller.ref.current,
|
|
38483
38971
|
);
|
|
38484
38972
|
const currentUIState = controller.uiState;
|
|
38973
|
+
// What a control is STARTED on is accepted, as its own defaultValue
|
|
38974
|
+
// would be: a value placed on it as it registers, or what a picker
|
|
38975
|
+
// takes up from its popup while it holds nothing of its own. A
|
|
38976
|
+
// rollback goes back there, and a control left on it holds no edit.
|
|
38977
|
+
if (!controller.hasStateProp) {
|
|
38978
|
+
if (
|
|
38979
|
+
e.type === "initial_state_push" ||
|
|
38980
|
+
(e.type === "facade_child_mount_sync" && !controller.holdsEdit())
|
|
38981
|
+
) {
|
|
38982
|
+
controller.state = newUIState;
|
|
38983
|
+
}
|
|
38984
|
+
}
|
|
38485
38985
|
const stateIsTheSame = compareTwoJsValues(newUIState, currentUIState);
|
|
38486
38986
|
if (stateIsTheSame) {
|
|
38487
38987
|
if (controlType === "button" || controlType === "link") {
|
|
@@ -38647,7 +39147,12 @@ const useUIStateController = (
|
|
|
38647
39147
|
}
|
|
38648
39148
|
// initial_state_push is pure initialization (equivalent to defaultValue on the
|
|
38649
39149
|
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
38650
|
-
|
|
39150
|
+
// A value the outside moved since (see outside_value_follow) is the
|
|
39151
|
+
// same initialization, later.
|
|
39152
|
+
if (
|
|
39153
|
+
e.type !== "initial_state_push" &&
|
|
39154
|
+
e.type !== "outside_value_follow"
|
|
39155
|
+
) {
|
|
38651
39156
|
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
38652
39157
|
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
38653
39158
|
// both would cause an infinite loop when a parent cascades state
|
|
@@ -38701,6 +39206,19 @@ const useUIStateController = (
|
|
|
38701
39206
|
stateChanged: true,
|
|
38702
39207
|
});
|
|
38703
39208
|
}
|
|
39209
|
+
if (
|
|
39210
|
+
e.type === "outside_value_follow" ||
|
|
39211
|
+
(e.type === "facade_child_mount_sync" && isOutsideValueFollow(e))
|
|
39212
|
+
) {
|
|
39213
|
+
// Heard the way a control mounting on that value would be:
|
|
39214
|
+
// silently, nobody acted. But heard, all the way up — a picker
|
|
39215
|
+
// whose popup followed tells its own group in turn — or what
|
|
39216
|
+
// the groups send is not what is on screen.
|
|
39217
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
39218
|
+
stateChanged: true,
|
|
39219
|
+
silent: true,
|
|
39220
|
+
});
|
|
39221
|
+
}
|
|
38704
39222
|
return true;
|
|
38705
39223
|
}
|
|
38706
39224
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -38796,6 +39314,41 @@ const useUIStateController = (
|
|
|
38796
39314
|
// pick — the signal would hand it straight back on the next render,
|
|
38797
39315
|
// and the refused value would win over the rollback.
|
|
38798
39316
|
writeBoundSignal(controller.state);
|
|
39317
|
+
controller.followDefaultValue();
|
|
39318
|
+
},
|
|
39319
|
+
// Something typed, picked or toggled since the value the outside last
|
|
39320
|
+
// gave or accepted.
|
|
39321
|
+
holdsEdit: () => {
|
|
39322
|
+
return !compareTwoJsValues(controller.uiState, controller.state);
|
|
39323
|
+
},
|
|
39324
|
+
// An uncontrolled control starts on its defaultValue, and a new one is
|
|
39325
|
+
// what it would start on if it were mounted again — a record saved
|
|
39326
|
+
// and handed back normalized, refreshed from elsewhere. It takes it
|
|
39327
|
+
// while it holds no edit. An edit is never undone; the default waits
|
|
39328
|
+
// for it to be accepted or rolled back.
|
|
39329
|
+
followDefaultValue: () => {
|
|
39330
|
+
// A bound signal carries its own default, followed through
|
|
39331
|
+
// stateFromSignal; a proxy shows the control it stands for.
|
|
39332
|
+
if (
|
|
39333
|
+
controller.hasStateProp ||
|
|
39334
|
+
controller.isProxy ||
|
|
39335
|
+
controller.props.signal
|
|
39336
|
+
) {
|
|
39337
|
+
return;
|
|
39338
|
+
}
|
|
39339
|
+
const { defaultValue } = controller;
|
|
39340
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
39341
|
+
return;
|
|
39342
|
+
}
|
|
39343
|
+
if (controller.holdsEdit()) {
|
|
39344
|
+
return;
|
|
39345
|
+
}
|
|
39346
|
+
controller.defaultValueTaken = defaultValue;
|
|
39347
|
+
controller.state = defaultValue;
|
|
39348
|
+
controller.setUIState(
|
|
39349
|
+
defaultValue,
|
|
39350
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
39351
|
+
);
|
|
38799
39352
|
},
|
|
38800
39353
|
// Read by the callout manager when it has nowhere else to point.
|
|
38801
39354
|
getCalloutAnchorElement: (event) =>
|
|
@@ -38809,7 +39362,12 @@ const useUIStateController = (
|
|
|
38809
39362
|
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
38810
39363
|
return;
|
|
38811
39364
|
}
|
|
39365
|
+
// A picker told yes is told for what its popup shows too, the way a
|
|
39366
|
+
// group tells its children. First, so that a default one of them
|
|
39367
|
+
// takes on the way is part of what this control accepts below.
|
|
39368
|
+
controller.facadeChild?.acknowledgeUIState();
|
|
38812
39369
|
controller.state = controller.uiState;
|
|
39370
|
+
controller.followDefaultValue();
|
|
38813
39371
|
},
|
|
38814
39372
|
onActionEnd: (e) => {
|
|
38815
39373
|
acknowledgeOwnAction(controller);
|
|
@@ -38964,6 +39522,7 @@ const useUIStateController = (
|
|
|
38964
39522
|
}
|
|
38965
39523
|
}
|
|
38966
39524
|
}
|
|
39525
|
+
controller.followDefaultValue();
|
|
38967
39526
|
}
|
|
38968
39527
|
return liveValues();
|
|
38969
39528
|
},
|
|
@@ -39544,7 +40103,12 @@ const useUIGroupStateController = (
|
|
|
39544
40103
|
// deferred alongside a mount sync stays a real change.
|
|
39545
40104
|
const pendingChange = pendingChangeRef.current;
|
|
39546
40105
|
pendingChangeRef.current = {
|
|
39547
|
-
|
|
40106
|
+
// A default followed is not replaced by a mount deferred beside
|
|
40107
|
+
// it: the event is what lets the sync below read the children.
|
|
40108
|
+
e:
|
|
40109
|
+
pendingChange && isOutsideValueFollow(pendingChange.e)
|
|
40110
|
+
? pendingChange.e
|
|
40111
|
+
: e,
|
|
39548
40112
|
notifyExternal:
|
|
39549
40113
|
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
39550
40114
|
actingChild,
|
|
@@ -39552,6 +40116,13 @@ const useUIGroupStateController = (
|
|
|
39552
40116
|
return;
|
|
39553
40117
|
}
|
|
39554
40118
|
const { controller } = s;
|
|
40119
|
+
if (isOutsideValueFollow(e)) {
|
|
40120
|
+
// Not a partial reading: the child was there already and its value
|
|
40121
|
+
// moved, told by the outside. The group is worth what its children
|
|
40122
|
+
// show, whatever it was handed before — kept, the value it sends
|
|
40123
|
+
// would be one the screen no longer shows.
|
|
40124
|
+
controller.stateGivenFromAbove = false;
|
|
40125
|
+
}
|
|
39555
40126
|
// A child mounting or unmounting is not somebody answering: while the
|
|
39556
40127
|
// children of a group are still arriving, their aggregate is a partial
|
|
39557
40128
|
// reading, and taking it for the truth is how the value the group was
|
|
@@ -39730,6 +40301,9 @@ const useUIGroupStateController = (
|
|
|
39730
40301
|
name,
|
|
39731
40302
|
value,
|
|
39732
40303
|
defaultValue,
|
|
40304
|
+
// Same as the leaf's (see useUIStateController): the default last
|
|
40305
|
+
// placed on the children, behind `defaultValue` while an edit waits.
|
|
40306
|
+
defaultValueTaken: defaultValue,
|
|
39733
40307
|
hasValueProp,
|
|
39734
40308
|
hasDefaultValueProp,
|
|
39735
40309
|
props,
|
|
@@ -40010,6 +40584,7 @@ const useUIGroupStateController = (
|
|
|
40010
40584
|
}
|
|
40011
40585
|
}
|
|
40012
40586
|
onChange(e, { notifyExternal: true });
|
|
40587
|
+
controller.followDefaultValue();
|
|
40013
40588
|
},
|
|
40014
40589
|
clearUIState: (e) => {
|
|
40015
40590
|
const ev = new CustomEvent("propagate_down_clear_ui_state", {
|
|
@@ -40038,6 +40613,46 @@ const useUIGroupStateController = (
|
|
|
40038
40613
|
c.acknowledgeUIState();
|
|
40039
40614
|
}
|
|
40040
40615
|
}
|
|
40616
|
+
controller.followDefaultValue();
|
|
40617
|
+
},
|
|
40618
|
+
// A group holds no value of its own to have edited: it holds an edit
|
|
40619
|
+
// when one of its children does.
|
|
40620
|
+
holdsEdit: () => {
|
|
40621
|
+
return childUIStateControllerArray.some(
|
|
40622
|
+
(c) => shouldPropagateStateToChild(c) && c.holdsEdit(),
|
|
40623
|
+
);
|
|
40624
|
+
},
|
|
40625
|
+
// The leaf's rule (see useUIStateController), for a group given a
|
|
40626
|
+
// plain `defaultValue`: a new one is placed on the children the way
|
|
40627
|
+
// the first one was as they registered, while none of them holds an
|
|
40628
|
+
// edit. A signal's own default is followed by the update below.
|
|
40629
|
+
followDefaultValue: () => {
|
|
40630
|
+
if (controller.hasValueProp || controller.props.signal) {
|
|
40631
|
+
return;
|
|
40632
|
+
}
|
|
40633
|
+
const { defaultValue } = controller;
|
|
40634
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
40635
|
+
return;
|
|
40636
|
+
}
|
|
40637
|
+
if (controller.holdsEdit()) {
|
|
40638
|
+
return;
|
|
40639
|
+
}
|
|
40640
|
+
controller.defaultValueTaken = defaultValue;
|
|
40641
|
+
const followEvent = new CustomEvent("outside_value_follow", {
|
|
40642
|
+
detail: {},
|
|
40643
|
+
});
|
|
40644
|
+
const placeEvent = new CustomEvent("initial_state_push", {
|
|
40645
|
+
detail: {},
|
|
40646
|
+
});
|
|
40647
|
+
chainEvent(placeEvent, followEvent);
|
|
40648
|
+
controller.setUIState(
|
|
40649
|
+
defaultValue === undefined ? fallbackState : defaultValue,
|
|
40650
|
+
placeEvent,
|
|
40651
|
+
);
|
|
40652
|
+
s.parentUIStateController?.onChildUIAction(controller, followEvent, {
|
|
40653
|
+
stateChanged: true,
|
|
40654
|
+
silent: true,
|
|
40655
|
+
});
|
|
40041
40656
|
},
|
|
40042
40657
|
onActionEnd: (e) => {
|
|
40043
40658
|
acknowledgeOwnAction(controller);
|
|
@@ -40160,6 +40775,7 @@ const useUIGroupStateController = (
|
|
|
40160
40775
|
// former value back over it on the next child interaction.
|
|
40161
40776
|
placeChildrenFrom(defaultValue);
|
|
40162
40777
|
}
|
|
40778
|
+
controller.followDefaultValue();
|
|
40163
40779
|
|
|
40164
40780
|
return liveValues();
|
|
40165
40781
|
},
|
|
@@ -40384,7 +41000,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40384
41000
|
if (child !== firstChildControllerRef.current) {
|
|
40385
41001
|
return;
|
|
40386
41002
|
}
|
|
40387
|
-
if (
|
|
41003
|
+
if (
|
|
41004
|
+
silent &&
|
|
41005
|
+
!isOutsideValueFollow(e) &&
|
|
41006
|
+
uiStateHoldsNothing(child.uiState)
|
|
41007
|
+
) {
|
|
40388
41008
|
// A silent sync means the child's own structure changed (children
|
|
40389
41009
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
40390
41010
|
// with no value there is one that currently *cannot* express one —
|
|
@@ -40395,7 +41015,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40395
41015
|
// still nothing to adopt: the sync would only respell one nothing
|
|
40396
41016
|
// as another (an array picker's [] becoming undefined, say) and
|
|
40397
41017
|
// hand that to uiAction — an empty array picker opened its popup
|
|
40398
|
-
// and told its owner the value changed.
|
|
41018
|
+
// and told its owner the value changed. A default followed down to
|
|
41019
|
+
// nothing is not that: the outside emptied a value the child held.
|
|
40399
41020
|
return;
|
|
40400
41021
|
}
|
|
40401
41022
|
updatingRef.current = true;
|
|
@@ -40459,6 +41080,69 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40459
41080
|
return scope.controller;
|
|
40460
41081
|
};
|
|
40461
41082
|
|
|
41083
|
+
/**
|
|
41084
|
+
* What every control of a picker — the picker and the controls in its popup —
|
|
41085
|
+
* has been accepted on as the popup opens. Read back by followOutsideSinceOpen
|
|
41086
|
+
* when the popup is cancelled.
|
|
41087
|
+
*/
|
|
41088
|
+
const readStatesAtOpen = (pickerController) => {
|
|
41089
|
+
const statesAtOpen = new Map();
|
|
41090
|
+
visitControllers(pickerController, (controller) => {
|
|
41091
|
+
if (Object.hasOwn(controller, "state")) {
|
|
41092
|
+
statesAtOpen.set(controller, controller.state);
|
|
41093
|
+
}
|
|
41094
|
+
});
|
|
41095
|
+
return statesAtOpen;
|
|
41096
|
+
};
|
|
41097
|
+
|
|
41098
|
+
/**
|
|
41099
|
+
* A cancel puts back the value the picker held at open, which takes back
|
|
41100
|
+
* everything that moved since — and not only what the user moved: a record
|
|
41101
|
+
* refreshed while the popup was open, a default that arrived under a field
|
|
41102
|
+
* holding an edit. Those were not the user's to take back. Once the value at
|
|
41103
|
+
* open is back, each control the outside moved since shows what the outside
|
|
41104
|
+
* holds again, and a default left waiting on an edit is taken there.
|
|
41105
|
+
*
|
|
41106
|
+
* Children before the control around them: a picker given its value by its
|
|
41107
|
+
* owner has the last word over the controls in its popup.
|
|
41108
|
+
*/
|
|
41109
|
+
const followOutsideSinceOpen = (pickerController, statesAtOpen) => {
|
|
41110
|
+
visitControllers(
|
|
41111
|
+
pickerController,
|
|
41112
|
+
(controller) => {
|
|
41113
|
+
if (
|
|
41114
|
+
statesAtOpen.has(controller) &&
|
|
41115
|
+
!compareTwoJsValues(controller.state, statesAtOpen.get(controller)) &&
|
|
41116
|
+
controller.holdsEdit()
|
|
41117
|
+
) {
|
|
41118
|
+
controller.setUIState(
|
|
41119
|
+
controller.state,
|
|
41120
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
41121
|
+
);
|
|
41122
|
+
}
|
|
41123
|
+
controller.followDefaultValue();
|
|
41124
|
+
},
|
|
41125
|
+
{ childrenFirst: true },
|
|
41126
|
+
);
|
|
41127
|
+
};
|
|
41128
|
+
|
|
41129
|
+
// A picker reaches the controls of its popup through its facade child, a group
|
|
41130
|
+
// through the children it registered.
|
|
41131
|
+
const visitControllers = (controller, visit, { childrenFirst } = {}) => {
|
|
41132
|
+
if (!childrenFirst) {
|
|
41133
|
+
visit(controller);
|
|
41134
|
+
}
|
|
41135
|
+
const children = controller.facadeChild
|
|
41136
|
+
? [controller.facadeChild]
|
|
41137
|
+
: controller.getChildControllers?.() || [];
|
|
41138
|
+
for (const child of children) {
|
|
41139
|
+
visitControllers(child, visit, { childrenFirst });
|
|
41140
|
+
}
|
|
41141
|
+
if (childrenFirst) {
|
|
41142
|
+
visit(controller);
|
|
41143
|
+
}
|
|
41144
|
+
};
|
|
41145
|
+
|
|
40462
41146
|
const describePicker = (props) =>
|
|
40463
41147
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
40464
41148
|
|
|
@@ -40523,10 +41207,18 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
40523
41207
|
// no action of the control's own — but what it holds really did move, so
|
|
40524
41208
|
// uiAction, the bound signal and the parent notification below all happen.
|
|
40525
41209
|
"auto_fix",
|
|
41210
|
+
// A control put on a value the outside moved while nobody was editing it: a
|
|
41211
|
+
// new defaultValue (see followDefaultValue), or what a cancel puts back
|
|
41212
|
+
// (see followOutsideSinceOpen). Initialization arriving after mount, so
|
|
41213
|
+
// nothing fires, and the groups above hear it silently.
|
|
41214
|
+
"outside_value_follow",
|
|
40526
41215
|
]);
|
|
40527
41216
|
const isInternalEvent = (e) => {
|
|
40528
41217
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
40529
41218
|
};
|
|
41219
|
+
const isOutsideValueFollow = (e) => {
|
|
41220
|
+
return Boolean(findEvent(e, "outside_value_follow"));
|
|
41221
|
+
};
|
|
40530
41222
|
|
|
40531
41223
|
/**
|
|
40532
41224
|
* A value handed DOWN to a control by whoever owns it: a picker filling its
|
|
@@ -45965,6 +46657,35 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
|
|
|
45965
46657
|
}, [ref, href, prefetch]);
|
|
45966
46658
|
};
|
|
45967
46659
|
|
|
46660
|
+
/**
|
|
46661
|
+
* What a popup's opening will read, asked for when the press on the element
|
|
46662
|
+
* whose command opens it starts (see announceOpeningPress): the popup, the
|
|
46663
|
+
* value it opens on and so the address it will write are all known before
|
|
46664
|
+
* the release.
|
|
46665
|
+
*/
|
|
46666
|
+
const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
46667
|
+
useEffect(() => {
|
|
46668
|
+
if (!prefetch || !command) {
|
|
46669
|
+
return undefined;
|
|
46670
|
+
}
|
|
46671
|
+
const element = ref.current;
|
|
46672
|
+
if (!element) {
|
|
46673
|
+
return undefined;
|
|
46674
|
+
}
|
|
46675
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
46676
|
+
// A right click opens a menu, never the popup.
|
|
46677
|
+
if (pointerdownEvent.button !== 0) {
|
|
46678
|
+
return;
|
|
46679
|
+
}
|
|
46680
|
+
announceOpeningPress(element, command);
|
|
46681
|
+
};
|
|
46682
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
46683
|
+
return () => {
|
|
46684
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
46685
|
+
};
|
|
46686
|
+
}, [ref, command, prefetch]);
|
|
46687
|
+
};
|
|
46688
|
+
|
|
45968
46689
|
/**
|
|
45969
46690
|
* Hook that reactively checks if a URL is visited.
|
|
45970
46691
|
* Re-renders when the visited URL set changes.
|
|
@@ -49965,7 +50686,8 @@ const useExpandableContext = partName => {
|
|
|
49965
50686
|
* content — and rebuilds it from scratch on every expansion. Under the
|
|
49966
50687
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
49967
50688
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
49968
|
-
*
|
|
50689
|
+
* built by a press on it only, and thrown away if that press does not
|
|
50690
|
+
* expand it.
|
|
49969
50691
|
*/
|
|
49970
50692
|
const Expandable = props => {
|
|
49971
50693
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -50044,8 +50766,14 @@ const Expandable = props => {
|
|
|
50044
50766
|
// before the click ever fires — so what held the focus has to be remembered
|
|
50045
50767
|
// at pointerdown time.
|
|
50046
50768
|
const focusedAtPointerDownRef = useRef(null);
|
|
50047
|
-
const onUIPointerDown =
|
|
50769
|
+
const onUIPointerDown = pointerdownEvent => {
|
|
50048
50770
|
focusedAtPointerDownRef.current = document.activeElement;
|
|
50771
|
+
// The start of an expansion, announced as a --navi-open button announces
|
|
50772
|
+
// its own (see announceOpeningPress in commands.js) — unless aimed at a
|
|
50773
|
+
// control inside the UI part, whose click does not toggle (see onUIClick).
|
|
50774
|
+
if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
|
|
50775
|
+
dispatchCustomEvent(rootRef.current, "navi_open_press");
|
|
50776
|
+
}
|
|
50049
50777
|
};
|
|
50050
50778
|
|
|
50051
50779
|
// The content keeps its final size while the track animates (see the top
|
|
@@ -50343,17 +51071,18 @@ const Expandable = props => {
|
|
|
50343
51071
|
if (clickEvent.defaultPrevented) {
|
|
50344
51072
|
return;
|
|
50345
51073
|
}
|
|
50346
|
-
|
|
50347
|
-
|
|
50348
|
-
} = clickEvent;
|
|
50349
|
-
if (target.nodeType === 1) {
|
|
50350
|
-
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
50351
|
-
if (interactiveElement && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement)) {
|
|
50352
|
-
return;
|
|
50353
|
-
}
|
|
51074
|
+
if (isAimedAtControlInsideUI(clickEvent.target)) {
|
|
51075
|
+
return;
|
|
50354
51076
|
}
|
|
50355
51077
|
toggle(clickEvent);
|
|
50356
51078
|
};
|
|
51079
|
+
const isAimedAtControlInsideUI = target => {
|
|
51080
|
+
if (target.nodeType !== 1) {
|
|
51081
|
+
return false;
|
|
51082
|
+
}
|
|
51083
|
+
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
51084
|
+
return Boolean(interactiveElement) && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement);
|
|
51085
|
+
};
|
|
50357
51086
|
|
|
50358
51087
|
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
50359
51088
|
// control inside it belongs to that control.
|
|
@@ -51277,6 +52006,9 @@ const ButtonUI = props => {
|
|
|
51277
52006
|
// Nothing to prefetch on the way to another document: the routes do not
|
|
51278
52007
|
// lead there.
|
|
51279
52008
|
usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
|
|
52009
|
+
// A press the button refuses opens nothing: nothing to ask ahead for.
|
|
52010
|
+
const refusesPress = basePseudoState[":read-only"] || basePseudoState[":disabled"] || loading;
|
|
52011
|
+
usePreloadOpeningOnPress(ref, props.command, prefetch && !refusesPress);
|
|
51280
52012
|
useAccentColorAttributes(ref, null, {
|
|
51281
52013
|
elementSelector: visualSelector
|
|
51282
52014
|
});
|
|
@@ -51768,8 +52500,11 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
51768
52500
|
* pointer or the focus arrives, ahead of the press (see
|
|
51769
52501
|
* docs/dynamic_import.md): the route actions that ask nothing of the
|
|
51770
52502
|
* address — a page's code, a read without params — are prerun; one keyed by
|
|
51771
|
-
* a param waits for the arrival.
|
|
51772
|
-
*
|
|
52503
|
+
* a param waits for the arrival. With a `--navi-open`/`--navi-toggle`
|
|
52504
|
+
* command instead, the press — not the hover: what it asks is keyed by the
|
|
52505
|
+
* popup — preruns the route actions whose params the popup's opening
|
|
52506
|
+
* changes, read as if its `signal` held its `value`. `false` for a
|
|
52507
|
+
* destination not worth fetching ahead.
|
|
51773
52508
|
* @param {boolean} [pressableDuringRouteTransition] Keep answering presses
|
|
51774
52509
|
* while a route transition plays: what a movement photographs goes deaf to
|
|
51775
52510
|
* the pointer for its whole length, and the door that opened the page — a
|
|
@@ -58649,6 +59384,52 @@ const SlideContainer = ({
|
|
|
58649
59384
|
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
58650
59385
|
paintDrag();
|
|
58651
59386
|
},
|
|
59387
|
+
// The slide being brought in walked whole and the hand still going, or a
|
|
59388
|
+
// travel caught on its way thrown on: either way the hand asks for the
|
|
59389
|
+
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
59390
|
+
// for that — the slide reached becomes the one dragged, the one the hand
|
|
59391
|
+
// came from stays next to it for a hand that turns around, and the slide
|
|
59392
|
+
// past it is put on the other side.
|
|
59393
|
+
onEdge: ({
|
|
59394
|
+
axis,
|
|
59395
|
+
sign
|
|
59396
|
+
}) => {
|
|
59397
|
+
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
59398
|
+
const {
|
|
59399
|
+
slideElements,
|
|
59400
|
+
placeOf
|
|
59401
|
+
} = readMap();
|
|
59402
|
+
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
59403
|
+
if (!reachedElement) {
|
|
59404
|
+
return false;
|
|
59405
|
+
}
|
|
59406
|
+
// The gate goToArea reads at the release, asked of the slide the hand
|
|
59407
|
+
// is now in: a slide holding on to the user is not walked through.
|
|
59408
|
+
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
59409
|
+
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
59410
|
+
if (!past) {
|
|
59411
|
+
return false;
|
|
59412
|
+
}
|
|
59413
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
59414
|
+
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
59415
|
+
x: 0,
|
|
59416
|
+
y: 0
|
|
59417
|
+
};
|
|
59418
|
+
drag.areaBack = sign > 0 ? past : drag.area;
|
|
59419
|
+
drag.areaOn = sign > 0 ? drag.area : past;
|
|
59420
|
+
drag.area = reached;
|
|
59421
|
+
drag.basePlace = basePlace;
|
|
59422
|
+
drag.baseOffset = {
|
|
59423
|
+
x: -basePlace.x * drag.box.width,
|
|
59424
|
+
y: -basePlace.y * drag.box.height
|
|
59425
|
+
};
|
|
59426
|
+
stageDrag(drag, readArea(currentElement));
|
|
59427
|
+
return {
|
|
59428
|
+
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
59429
|
+
travelBack: Boolean(drag.areaBack),
|
|
59430
|
+
travelOn: Boolean(drag.areaOn)
|
|
59431
|
+
};
|
|
59432
|
+
},
|
|
58652
59433
|
onEnd: ({
|
|
58653
59434
|
axis,
|
|
58654
59435
|
sign,
|
|
@@ -62537,9 +63318,11 @@ const css$E = /* css */`
|
|
|
62537
63318
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
62538
63319
|
* the popup has finished closing, for content whose fresh state is its
|
|
62539
63320
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
62540
|
-
* changed while the popup was closed.
|
|
62541
|
-
*
|
|
62542
|
-
*
|
|
63321
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
63322
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
63323
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
63324
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
63325
|
+
* press that does not open the popup throws it away.
|
|
62543
63326
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
62544
63327
|
*/
|
|
62545
63328
|
const Dialog = props => {
|
|
@@ -64217,9 +65000,11 @@ const css$D = /* css */`
|
|
|
64217
65000
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
64218
65001
|
* the popup has finished closing, for content whose fresh state is its
|
|
64219
65002
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
64220
|
-
* changed while the popup was closed.
|
|
64221
|
-
*
|
|
64222
|
-
*
|
|
65003
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
65004
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
65005
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
65006
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
65007
|
+
* press that does not open the popup throws it away.
|
|
64223
65008
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
64224
65009
|
*/
|
|
64225
65010
|
const Popover = props => {
|
|
@@ -65882,7 +66667,10 @@ const PickerCustom = props => {
|
|
|
65882
66667
|
// a defaultValue holds nothing, so closing on it untouched IS the answer
|
|
65883
66668
|
// ("yes, 2h15") — the same rule Form applies to an untouched field (see
|
|
65884
66669
|
// isUIStateHeld). Read at open, before anything inside can change it.
|
|
65885
|
-
const
|
|
66670
|
+
const pickerController = getPickerInput(ref.current)?.__uiStateController__;
|
|
66671
|
+
const heldAtOpen = isUIStateHeld(pickerController);
|
|
66672
|
+
// What a cancel must not take back: see followOutsideSinceOpen.
|
|
66673
|
+
const statesAtOpen = pickerController ? readStatesAtOpen(pickerController) : null;
|
|
65886
66674
|
onOpen?.(openEvent);
|
|
65887
66675
|
return {
|
|
65888
66676
|
onRequestClose: requestCloseEvent => {
|
|
@@ -65950,6 +66738,9 @@ const PickerCustom = props => {
|
|
|
65950
66738
|
});
|
|
65951
66739
|
chainEvent(rollbackEvent, closeEvent);
|
|
65952
66740
|
inputEl.__uiStateController__.setUIState(valueAtOpen, rollbackEvent);
|
|
66741
|
+
if (statesAtOpen) {
|
|
66742
|
+
followOutsideSinceOpen(inputEl.__uiStateController__, statesAtOpen);
|
|
66743
|
+
}
|
|
65953
66744
|
} else if (!heldAtOpen) {
|
|
65954
66745
|
// Confirmed a suggestion: nothing changed, so nothing has told the
|
|
65955
66746
|
// control's own bound signal / uiAction that this is now the
|
|
@@ -66030,6 +66821,11 @@ const PickerCustom = props => {
|
|
|
66030
66821
|
"onnavi_request_open": e => {
|
|
66031
66822
|
dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
|
|
66032
66823
|
},
|
|
66824
|
+
// And the press announcing that request (see announceOpeningPress in
|
|
66825
|
+
// commands.js), for the popup's content to be built on it.
|
|
66826
|
+
"onnavi_open_press": () => {
|
|
66827
|
+
dispatchCustomEvent(popupRef.current, "navi_open_press");
|
|
66828
|
+
},
|
|
66033
66829
|
"onnavi_request_close": e => {
|
|
66034
66830
|
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
66035
66831
|
if (!closing) {
|
|
@@ -66224,6 +67020,25 @@ const PickerCustom = props => {
|
|
|
66224
67020
|
// below), and the keyboard keeps its own ways in (the shortcuts above).
|
|
66225
67021
|
const openOnList = Array.isArray(openOn) ? openOn : [openOn];
|
|
66226
67022
|
const opensOnPress = openOnList.includes("press");
|
|
67023
|
+
// A press on the trigger of a picker a press opens is announced as it
|
|
67024
|
+
// starts, as a --navi-open button announces its own (see
|
|
67025
|
+
// announceOpeningPress in commands.js). Only a press that can open it: a
|
|
67026
|
+
// picker opened by a hold says nothing on a tap, nor does one refusing
|
|
67027
|
+
// to open (read-only with openWhileReadOnly={false}, busy). The
|
|
67028
|
+
// interactivity is asked alone, not the whole gate: a drag source
|
|
67029
|
+
// cancels the pointerdown, and the click that opens still comes.
|
|
67030
|
+
if (opensOnPress) {
|
|
67031
|
+
pickerProps.onPointerDown = pointerdownEvent => {
|
|
67032
|
+
props.onPointerDown?.(pointerdownEvent);
|
|
67033
|
+
const pickerEl = ref.current;
|
|
67034
|
+
if (pointerdownEvent.button !== 0 || openController.opened || isWithinPickerContent(pointerdownEvent.target) || !allowsInteraction(pickerEl, {
|
|
67035
|
+
intent: "read"
|
|
67036
|
+
})) {
|
|
67037
|
+
return;
|
|
67038
|
+
}
|
|
67039
|
+
dispatchCustomEvent(pickerEl, "navi_open_press");
|
|
67040
|
+
};
|
|
67041
|
+
}
|
|
66227
67042
|
let interactions = props.interactions;
|
|
66228
67043
|
if (!opensOnPress) {
|
|
66229
67044
|
interactions = {
|
|
@@ -78354,6 +79169,9 @@ const css$n = /* css */`.navi_split_button {
|
|
|
78354
79169
|
* defaultValue?: any,
|
|
78355
79170
|
* label?: import("ignore:preact").ComponentChildren,
|
|
78356
79171
|
* action?: (value: any, event: Event) => void | Promise<void>,
|
|
79172
|
+
* command?: string,
|
|
79173
|
+
* commandFor?: string,
|
|
79174
|
+
* prefetch?: boolean,
|
|
78357
79175
|
* onValueChange?: (value: any, event: Event) => void,
|
|
78358
79176
|
* chooseEffect?: "select" | "run",
|
|
78359
79177
|
* menuLabel?: string,
|
|
@@ -78407,6 +79225,12 @@ const css$n = /* css */`.navi_split_button {
|
|
|
78407
79225
|
* entry given. Pressed on the button it is the entry the button stands for;
|
|
78408
79226
|
* with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
|
|
78409
79227
|
* whole split button is busy until it settles.
|
|
79228
|
+
* @param {string} [command] What a press on the button does once its `action`
|
|
79229
|
+
* is done, as on a `<Button>` (with `commandFor`, and `command-value` when
|
|
79230
|
+
* what the command is about is not the entry the button stands for). A
|
|
79231
|
+
* split button whose button opens a popup says it here rather than from its
|
|
79232
|
+
* `action`: the press is then read as an opening before it runs, and asks
|
|
79233
|
+
* ahead for what the popup's opening reads (see `prefetch` on Button).
|
|
78410
79234
|
* @param {"select"|"run"} [chooseEffect="select"] What choosing an entry does.
|
|
78411
79235
|
* "select" hands the button that entry, to be run by a press on it — for
|
|
78412
79236
|
* something one does again and again, where the last choice is the likely
|
|
@@ -78448,6 +79272,10 @@ const SplitButton = props => {
|
|
|
78448
79272
|
defaultValue,
|
|
78449
79273
|
label,
|
|
78450
79274
|
action,
|
|
79275
|
+
command,
|
|
79276
|
+
commandFor,
|
|
79277
|
+
"command-value": commandValue,
|
|
79278
|
+
prefetch,
|
|
78451
79279
|
onValueChange,
|
|
78452
79280
|
chooseEffect = "select",
|
|
78453
79281
|
menuLabel = naviI18n("button.more_actions"),
|
|
@@ -78541,6 +79369,10 @@ const SplitButton = props => {
|
|
|
78541
79369
|
id: idResolved,
|
|
78542
79370
|
value: valueResolved,
|
|
78543
79371
|
action: actionResolved,
|
|
79372
|
+
command: command,
|
|
79373
|
+
commandFor: commandFor,
|
|
79374
|
+
"command-value": commandValue,
|
|
79375
|
+
prefetch: prefetch,
|
|
78544
79376
|
...halfProps,
|
|
78545
79377
|
children: label === undefined ? optionShown?.label : label
|
|
78546
79378
|
}), jsxs("div", {
|