@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/dev/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, createEventGroupLogger, 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, getVirtualKeyboardOverlayHeight, 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, prefixFirstAndIndentRemainingLines, 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;
|
|
@@ -325,6 +348,112 @@ const urlToScheme = (url) => {
|
|
|
325
348
|
return scheme;
|
|
326
349
|
};
|
|
327
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The document's rendering, held for the one frame a view transition needs.
|
|
353
|
+
*
|
|
354
|
+
* The browser does not take the picture of the page being left when a
|
|
355
|
+
* transition is ASKED for — it takes it at the next frame, just before running
|
|
356
|
+
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
357
|
+
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
358
|
+
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
359
|
+
* Both sides of the animation then show it, and one watches a page slide onto
|
|
360
|
+
* itself.
|
|
361
|
+
*
|
|
362
|
+
* So what Preact has queued waits until the update callback, which is the
|
|
363
|
+
* moment the API is built around — the change belongs inside it. The whole
|
|
364
|
+
* document is held: it is about to be frozen under a picture anyway.
|
|
365
|
+
*
|
|
366
|
+
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
367
|
+
* around Preact's `options.debounceRendering`, and two of them installed
|
|
368
|
+
* independently restore each other in the wrong order when they let go — every
|
|
369
|
+
* render queued in between is then handed to a wrapper nobody will ever
|
|
370
|
+
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
371
|
+
* route transition) must therefore hold through this module, never through a
|
|
372
|
+
* wrapper of its own.
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
let renderingHold = null;
|
|
377
|
+
const holdRendering = () => {
|
|
378
|
+
if (renderingHold) {
|
|
379
|
+
return renderingHold.release;
|
|
380
|
+
}
|
|
381
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
382
|
+
const hold = {
|
|
383
|
+
render: null,
|
|
384
|
+
waiting: [],
|
|
385
|
+
release: () => {
|
|
386
|
+
// Only the hold that is still standing may be given back: a holder
|
|
387
|
+
// releasing after another has taken over must not let go of what it
|
|
388
|
+
// does not hold.
|
|
389
|
+
if (renderingHold !== hold) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
renderingHold = null;
|
|
393
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
394
|
+
const { render, waiting } = hold;
|
|
395
|
+
hold.render = null;
|
|
396
|
+
hold.waiting = [];
|
|
397
|
+
if (render) {
|
|
398
|
+
render();
|
|
399
|
+
}
|
|
400
|
+
for (const wait of waiting) {
|
|
401
|
+
wait();
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
renderingHold = hold;
|
|
406
|
+
options.debounceRendering = (render) => {
|
|
407
|
+
hold.render = render;
|
|
408
|
+
};
|
|
409
|
+
return hold.release;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Anything else that must not happen before the picture is taken, and the
|
|
413
|
+
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
414
|
+
// document put back to its top while the page being left is still on screen is
|
|
415
|
+
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
416
|
+
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
417
|
+
// the band it had already painted — the page being left is then seen in
|
|
418
|
+
// fragments, whatever the movement does afterwards.
|
|
419
|
+
//
|
|
420
|
+
// Run at once when nobody is photographing anything, which is the common case
|
|
421
|
+
// and must stay free.
|
|
422
|
+
const whenRenderingResumes = (callback) => {
|
|
423
|
+
if (!renderingHold) {
|
|
424
|
+
callback();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
renderingHold.waiting.push(callback);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// The hold a navigation takes on its way in — from before its first write,
|
|
431
|
+
// because by the time a route announces that it matches, Preact has already
|
|
432
|
+
// been told and the render is queued; a hold taken then is a hold taken too
|
|
433
|
+
// late. Kept here until whoever animates the change takes it over, or the
|
|
434
|
+
// navigation turns out to be one nobody animates.
|
|
435
|
+
let routingRenderingHold = null;
|
|
436
|
+
const holdRenderingForRouting = () => {
|
|
437
|
+
routingRenderingHold = holdRendering();
|
|
438
|
+
};
|
|
439
|
+
// Nobody had a picture to take: a page held for a change it does not animate
|
|
440
|
+
// is a page that stutters for nothing.
|
|
441
|
+
const releaseRoutingRenderingHold = () => {
|
|
442
|
+
const release = routingRenderingHold;
|
|
443
|
+
routingRenderingHold = null;
|
|
444
|
+
if (release) {
|
|
445
|
+
release();
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
// An animator takes the navigation's hold as its own — taking another would be
|
|
449
|
+
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
450
|
+
// the change it animates is not a navigation.
|
|
451
|
+
const takeoverRoutingRenderingHold = () => {
|
|
452
|
+
const release = routingRenderingHold || holdRendering();
|
|
453
|
+
routingRenderingHold = null;
|
|
454
|
+
return release;
|
|
455
|
+
};
|
|
456
|
+
|
|
328
457
|
installImportMetaCssBuild(import.meta);
|
|
329
458
|
|
|
330
459
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
@@ -374,7 +503,8 @@ let urlTargetOptions = {
|
|
|
374
503
|
* How long, in ms, to keep waiting for a target that has not arrived, counted
|
|
375
504
|
* from the moment the document stops working.
|
|
376
505
|
* @param {number} [options.maxWait=10000]
|
|
377
|
-
* Longest wait, in ms, for a document that never stops working.
|
|
506
|
+
* Longest wait, in ms, for a document that never stops working. The element
|
|
507
|
+
* is then answered as it stands, when it is there.
|
|
378
508
|
*/
|
|
379
509
|
const setUrlTargetOptions = (options) => {
|
|
380
510
|
urlTargetOptions = { ...urlTargetOptions, ...options };
|
|
@@ -401,6 +531,23 @@ const useUrlTargetId = () => {
|
|
|
401
531
|
let stopWaitingForCurrentTarget = null;
|
|
402
532
|
let currentTargetKey;
|
|
403
533
|
|
|
534
|
+
const documentLoadIsReturn = () => {
|
|
535
|
+
const [navigationEntry] = performance.getEntriesByType("navigation");
|
|
536
|
+
if (!navigationEntry) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
return (
|
|
540
|
+
navigationEntry.type === "reload" || navigationEntry.type === "back_forward"
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
// The url the document is on its way back to — a back, a forward, a reload —
|
|
544
|
+
// where the reader was, as opposed to a place just sent to. Said by the
|
|
545
|
+
// navigation before it writes anything, and by the load for the first url.
|
|
546
|
+
let returningToUrl = documentLoadIsReturn() ? window.location.href : null;
|
|
547
|
+
observeBeforeRouting(({ url, navigationType }) => {
|
|
548
|
+
returningToUrl = navigationType === "traverse" ? url : null;
|
|
549
|
+
});
|
|
550
|
+
|
|
404
551
|
/**
|
|
405
552
|
* Answers the URL's target again, as if it had just been designated.
|
|
406
553
|
*
|
|
@@ -410,10 +557,10 @@ let currentTargetKey;
|
|
|
410
557
|
*/
|
|
411
558
|
const rearmUrlTarget = () => {
|
|
412
559
|
currentTargetKey = undefined;
|
|
413
|
-
armUrlTarget(documentUrlSignal.peek());
|
|
560
|
+
armUrlTarget(documentUrlSignal.peek(), { isReturn: false });
|
|
414
561
|
};
|
|
415
562
|
|
|
416
|
-
const armUrlTarget = (documentUrl) => {
|
|
563
|
+
const armUrlTarget = (documentUrl, { isReturn }) => {
|
|
417
564
|
const targetKey = urlToTargetKey(documentUrl);
|
|
418
565
|
if (targetKey === currentTargetKey) {
|
|
419
566
|
return;
|
|
@@ -424,11 +571,25 @@ const armUrlTarget = (documentUrl) => {
|
|
|
424
571
|
stopWaitingForCurrentTarget = null;
|
|
425
572
|
}
|
|
426
573
|
const targetId = urlToTargetId(documentUrl);
|
|
574
|
+
// The browser leaves `:target` on the element a URL named once the URL has
|
|
575
|
+
// stopped naming it — a pushState never touches it.
|
|
576
|
+
const staleTarget = document.querySelector(":target");
|
|
577
|
+
if (staleTarget && staleTarget.id !== targetId) {
|
|
578
|
+
writeTarget(null);
|
|
579
|
+
}
|
|
427
580
|
if (!targetId) {
|
|
428
581
|
return;
|
|
429
582
|
}
|
|
430
|
-
stopWaitingForCurrentTarget =
|
|
583
|
+
stopWaitingForCurrentTarget = waitForUrlTarget(targetId, (element) => {
|
|
431
584
|
stopWaitingForCurrentTarget = null;
|
|
585
|
+
if (!element.matches(":target")) {
|
|
586
|
+
writeTarget(element);
|
|
587
|
+
}
|
|
588
|
+
if (isReturn) {
|
|
589
|
+
// The offset put back by scroll_restoration.js is the reader's own: the
|
|
590
|
+
// element is named by the URL, not arrived at.
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
432
593
|
revealUrlTarget(element);
|
|
433
594
|
});
|
|
434
595
|
};
|
|
@@ -445,14 +606,27 @@ const urlToTargetId = (url) => {
|
|
|
445
606
|
return hash ? decodeURIComponent(hash.slice(1)) : "";
|
|
446
607
|
};
|
|
447
608
|
|
|
448
|
-
|
|
609
|
+
// Calls `onSettled` with the element once it can be answered: it exists, it
|
|
610
|
+
// shows something, and the document is done working. Found is not enough while
|
|
611
|
+
// the document works — what sits above it may still be drawn and push it down,
|
|
612
|
+
// and a page reusing a node of the page being left holds it before its own
|
|
613
|
+
// data has come.
|
|
614
|
+
//
|
|
615
|
+
// Confirmed at the next frame, once rendering has resumed: the render the last
|
|
616
|
+
// answer asked for has run by then, and a component starting its own run from
|
|
617
|
+
// that render has made the document busy again.
|
|
618
|
+
const waitForUrlTarget = (id, onSettled) => {
|
|
619
|
+
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
620
|
+
let stopped = false;
|
|
621
|
+
let confirmationPending = false;
|
|
622
|
+
let frame = null;
|
|
449
623
|
let mutationObserver = null;
|
|
450
624
|
let stopWatchingBusy = null;
|
|
451
625
|
let idleTimeout = null;
|
|
452
626
|
let maxWaitTimeout = null;
|
|
453
|
-
let found = false;
|
|
454
627
|
|
|
455
628
|
const stopWaiting = () => {
|
|
629
|
+
stopped = true;
|
|
456
630
|
if (mutationObserver) {
|
|
457
631
|
mutationObserver.disconnect();
|
|
458
632
|
mutationObserver = null;
|
|
@@ -463,76 +637,214 @@ const waitForElementWithId = (id, onFound) => {
|
|
|
463
637
|
}
|
|
464
638
|
clearTimeout(idleTimeout);
|
|
465
639
|
clearTimeout(maxWaitTimeout);
|
|
640
|
+
if (frame !== null) {
|
|
641
|
+
cancelAnimationFrame(frame);
|
|
642
|
+
frame = null;
|
|
643
|
+
}
|
|
466
644
|
};
|
|
467
645
|
|
|
468
|
-
const
|
|
646
|
+
const findElement = () => {
|
|
469
647
|
const element = document.getElementById(id);
|
|
470
648
|
if (!element) {
|
|
471
|
-
return;
|
|
649
|
+
return null;
|
|
472
650
|
}
|
|
473
651
|
// Rendered inside a closed tab, a folded details, a view that is not the
|
|
474
|
-
// one on screen: the element exists but would show nothing.
|
|
475
|
-
// it is the same wait, for the same reason.
|
|
652
|
+
// one on screen: the element exists but would show nothing.
|
|
476
653
|
if (element.checkVisibility && !element.checkVisibility()) {
|
|
477
|
-
return;
|
|
654
|
+
return null;
|
|
478
655
|
}
|
|
479
|
-
|
|
656
|
+
return element;
|
|
657
|
+
};
|
|
658
|
+
const settle = (element) => {
|
|
480
659
|
stopWaiting();
|
|
481
|
-
|
|
660
|
+
onSettled(element);
|
|
661
|
+
};
|
|
662
|
+
const check = () => {
|
|
663
|
+
if (confirmationPending || documentIsBusySignal.peek() || !findElement()) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
confirmationPending = true;
|
|
667
|
+
whenRenderingResumes(() => {
|
|
668
|
+
if (stopped) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
frame = requestAnimationFrame(() => {
|
|
672
|
+
frame = null;
|
|
673
|
+
confirmationPending = false;
|
|
674
|
+
if (documentIsBusySignal.peek()) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const element = findElement();
|
|
678
|
+
if (element) {
|
|
679
|
+
settle(element);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
});
|
|
482
683
|
};
|
|
483
684
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
mutationObserver = new MutationObserver(checkForElement);
|
|
685
|
+
mutationObserver = new MutationObserver(check);
|
|
686
|
+
// Every attribute, not only `id`: what shows an element that is already
|
|
687
|
+
// there — a <details> opening, a tab panel losing its display: none — is an
|
|
688
|
+
// attribute written somewhere above it, and nothing else says so.
|
|
490
689
|
mutationObserver.observe(document.documentElement, {
|
|
491
690
|
childList: true,
|
|
492
691
|
subtree: true,
|
|
493
692
|
attributes: true,
|
|
494
|
-
attributeFilter: ["id"],
|
|
495
693
|
});
|
|
496
|
-
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
497
694
|
stopWatchingBusy = effect(() => {
|
|
498
695
|
const documentIsBusy = documentIsBusySignal.value;
|
|
499
696
|
clearTimeout(idleTimeout);
|
|
500
|
-
if (
|
|
501
|
-
|
|
697
|
+
if (documentIsBusy) {
|
|
698
|
+
return;
|
|
502
699
|
}
|
|
503
|
-
|
|
504
|
-
|
|
700
|
+
check();
|
|
701
|
+
// The grace is for an element that has not come; one already there and
|
|
702
|
+
// waiting for rendering to resume is not given up on.
|
|
703
|
+
idleTimeout = setTimeout(() => {
|
|
704
|
+
if (!confirmationPending) {
|
|
705
|
+
stopWaiting();
|
|
706
|
+
}
|
|
707
|
+
}, graceAfterIdle);
|
|
708
|
+
});
|
|
709
|
+
// A document that never stops working: the element as it stands, when it is
|
|
710
|
+
// there at all.
|
|
711
|
+
maxWaitTimeout = setTimeout(() => {
|
|
712
|
+
const element = findElement();
|
|
713
|
+
if (element) {
|
|
714
|
+
settle(element);
|
|
715
|
+
} else {
|
|
716
|
+
stopWaiting();
|
|
717
|
+
}
|
|
718
|
+
}, maxWait);
|
|
505
719
|
|
|
506
720
|
return stopWaiting;
|
|
507
721
|
};
|
|
508
722
|
|
|
509
723
|
const revealUrlTarget = (element) => {
|
|
510
724
|
const { block, behavior, markDuration } = urlTargetOptions;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
725
|
+
const prefersReducedMotion = window.matchMedia(
|
|
726
|
+
"(prefers-reduced-motion: reduce)",
|
|
727
|
+
).matches;
|
|
728
|
+
element.scrollIntoView({
|
|
729
|
+
block,
|
|
730
|
+
behavior: prefersReducedMotion ? "instant" : behavior,
|
|
731
|
+
});
|
|
732
|
+
// What the browser does when it handles a fragment itself: keyboard
|
|
733
|
+
// navigation resumes from the target, not from the top of the document.
|
|
734
|
+
if (elementIsFocusable(element)) {
|
|
735
|
+
element.focus({ preventScroll: true });
|
|
736
|
+
}
|
|
737
|
+
element.setAttribute(URL_TARGET_ATTRIBUTE, "");
|
|
738
|
+
setTimeout(() => {
|
|
739
|
+
element.removeAttribute(URL_TARGET_ATTRIBUTE);
|
|
740
|
+
}, markDuration);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
let targetWriteInProgress = false;
|
|
744
|
+
/**
|
|
745
|
+
* True while navi writes the browser's `:target`. The fragment navigation that
|
|
746
|
+
* takes is announced like any other — a popstate, a navigate event, an entry
|
|
747
|
+
* change — and names the address and the state the document is already at, so
|
|
748
|
+
* whoever routes on those events leaves it alone.
|
|
749
|
+
*/
|
|
750
|
+
const isTargetWriteInProgress = () => {
|
|
751
|
+
return targetWriteInProgress;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// `:target` onto `element`, or off whatever holds it when `element` is null.
|
|
755
|
+
//
|
|
756
|
+
// A replace towards the address the browser is at: the browser answers it as a
|
|
757
|
+
// fragment navigation, and that is the only moment it sets `:target`. It also
|
|
758
|
+
// scrolls to what the fragment designates, resets the entry's states
|
|
759
|
+
// (history.state in Firefox, the Navigation API state everywhere) and fires a
|
|
760
|
+
// popstate (Chrome, Safari) and a navigate event. The address, both states and
|
|
761
|
+
// the scroll are put back in the same task, so nothing of it is painted; a
|
|
762
|
+
// running view transition is not skipped by it (measured in Chrome, Firefox
|
|
763
|
+
// and Safari).
|
|
764
|
+
const writeTarget = (element) => {
|
|
765
|
+
const address = window.location.href;
|
|
766
|
+
const historyState = window.history.state;
|
|
767
|
+
const { navigation } = window;
|
|
768
|
+
const entry = navigation ? navigation.currentEntry : null;
|
|
769
|
+
const entryState = entry ? entry.getState() : undefined;
|
|
770
|
+
const putScrollBack = captureScroll(element);
|
|
771
|
+
const addressFragment = new URL(address).hash.slice(1);
|
|
772
|
+
const addressTargetId = decodeURIComponent(addressFragment);
|
|
773
|
+
|
|
774
|
+
let fragmentUrl;
|
|
775
|
+
let silencedElement = null;
|
|
776
|
+
if (element) {
|
|
777
|
+
fragmentUrl =
|
|
778
|
+
element.id === addressTargetId
|
|
779
|
+
? address
|
|
780
|
+
: `${urlWithoutFragment(address)}#${encodeURIComponent(element.id)}`;
|
|
781
|
+
} else if (addressFragment) {
|
|
782
|
+
// The address itself, so the fragment does not change and no hashchange
|
|
783
|
+
// follows; the element it names briefly without its id, so it designates
|
|
784
|
+
// nothing.
|
|
785
|
+
fragmentUrl = address;
|
|
786
|
+
silencedElement = document.getElementById(addressTargetId);
|
|
787
|
+
} else {
|
|
788
|
+
// An empty fragment designates the top of the document; the scroll it
|
|
789
|
+
// makes is put back with the others.
|
|
790
|
+
fragmentUrl = `${address}#`;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
targetWriteInProgress = true;
|
|
794
|
+
try {
|
|
795
|
+
if (silencedElement) {
|
|
796
|
+
silencedElement.removeAttribute("id");
|
|
525
797
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
798
|
+
window.location.replace(fragmentUrl);
|
|
799
|
+
} finally {
|
|
800
|
+
if (silencedElement) {
|
|
801
|
+
silencedElement.id = addressTargetId;
|
|
802
|
+
}
|
|
803
|
+
window.history.replaceState(historyState, null, address);
|
|
804
|
+
if (entry && navigation.currentEntry) {
|
|
805
|
+
navigation.updateCurrentEntry({ state: entryState });
|
|
806
|
+
}
|
|
807
|
+
targetWriteInProgress = false;
|
|
808
|
+
}
|
|
809
|
+
putScrollBack();
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Every box a fragment navigation towards `element` may scroll: its ancestors,
|
|
813
|
+
// and the document.
|
|
814
|
+
const captureScroll = (element) => {
|
|
815
|
+
const boxes = [];
|
|
816
|
+
let box = element ? element.parentElement : null;
|
|
817
|
+
while (box) {
|
|
818
|
+
boxes.push(box);
|
|
819
|
+
box = box.parentElement;
|
|
820
|
+
}
|
|
821
|
+
const { scrollingElement } = document;
|
|
822
|
+
if (!boxes.includes(scrollingElement)) {
|
|
823
|
+
boxes.push(scrollingElement);
|
|
824
|
+
}
|
|
825
|
+
const positions = boxes.map((box) => {
|
|
826
|
+
return { box, left: box.scrollLeft, top: box.scrollTop };
|
|
530
827
|
});
|
|
828
|
+
return () => {
|
|
829
|
+
for (const { box, left, top } of positions) {
|
|
830
|
+
if (box.scrollLeft !== left || box.scrollTop !== top) {
|
|
831
|
+
box.scrollTo({ left, top, behavior: "instant" });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const urlWithoutFragment = (url) => {
|
|
838
|
+
const urlObject = new URL(url);
|
|
839
|
+
urlObject.hash = "";
|
|
840
|
+
return urlObject.href;
|
|
531
841
|
};
|
|
532
842
|
|
|
533
843
|
effect(() => {
|
|
534
844
|
const documentUrl = documentUrlSignal.value;
|
|
535
|
-
|
|
845
|
+
const isReturn = returningToUrl === documentUrl;
|
|
846
|
+
returningToUrl = null;
|
|
847
|
+
armUrlTarget(documentUrl, { isReturn });
|
|
536
848
|
});
|
|
537
849
|
|
|
538
850
|
/**
|
|
@@ -10437,6 +10749,26 @@ const dispatchRequestInteraction = (
|
|
|
10437
10749
|
});
|
|
10438
10750
|
};
|
|
10439
10751
|
|
|
10752
|
+
/**
|
|
10753
|
+
* Whether the control `element` belongs to lets an interaction of this intent
|
|
10754
|
+
* through, asked ahead of the event the gate will judge, and silently: nothing
|
|
10755
|
+
* is reported, nothing runs. For what a press is about to lead to (see
|
|
10756
|
+
* announceOpeningPress in commands.js) — the press itself is not that event: a
|
|
10757
|
+
* drag source cancels a `pointerdown` whose click still comes and opens.
|
|
10758
|
+
*
|
|
10759
|
+
* @param {Element} element
|
|
10760
|
+
* @param {{ intent: "read" | "write" }} options
|
|
10761
|
+
* @returns {boolean}
|
|
10762
|
+
*/
|
|
10763
|
+
const allowsInteraction = (element, { intent }) => {
|
|
10764
|
+
const controlHost = findControlHost(element) || element;
|
|
10765
|
+
const ci = controlHost.__uiStateController__?.rules.interaction;
|
|
10766
|
+
if (!ci) {
|
|
10767
|
+
return true;
|
|
10768
|
+
}
|
|
10769
|
+
return ci.checkInteractivity({ intent });
|
|
10770
|
+
};
|
|
10771
|
+
|
|
10440
10772
|
const onRequestInteraction = (
|
|
10441
10773
|
requestInteractionCustomEvent,
|
|
10442
10774
|
{ debugInteraction },
|
|
@@ -18348,6 +18680,33 @@ const syncOwnedResourceToSignals = (
|
|
|
18348
18680
|
|
|
18349
18681
|
// Global signal registry for route template detection
|
|
18350
18682
|
const globalSignalRegistry = new Map();
|
|
18683
|
+
|
|
18684
|
+
// The values some states are about to hold, answered in place of what they
|
|
18685
|
+
// hold for the length of one readStateAsIf.
|
|
18686
|
+
let valuesAsIf = null;
|
|
18687
|
+
/**
|
|
18688
|
+
* Runs `read` as if each state in `values` held the value given for it: the
|
|
18689
|
+
* address a press is about to write, read before it is written. Nothing is
|
|
18690
|
+
* written, so whoever watches those states hears nothing, and `read`
|
|
18691
|
+
* subscribes to nothing.
|
|
18692
|
+
*
|
|
18693
|
+
* Only a read of the state itself is answered this way. A `computed` derived
|
|
18694
|
+
* from one answers what it last computed — or, evaluated for the first time
|
|
18695
|
+
* inside `read`, caches a value derived from this answer: `read` reads the
|
|
18696
|
+
* states it depends on directly (a route action's params do).
|
|
18697
|
+
*
|
|
18698
|
+
* @param {Map<import("@preact/signals").Signal, any>} values
|
|
18699
|
+
* @param {() => any} read
|
|
18700
|
+
*/
|
|
18701
|
+
const readStateAsIf = (values, read) => {
|
|
18702
|
+
const previous = valuesAsIf;
|
|
18703
|
+
valuesAsIf = values;
|
|
18704
|
+
try {
|
|
18705
|
+
return untracked(read);
|
|
18706
|
+
} finally {
|
|
18707
|
+
valuesAsIf = previous;
|
|
18708
|
+
}
|
|
18709
|
+
};
|
|
18351
18710
|
let signalIdCounter = 0;
|
|
18352
18711
|
const generateSignalId = () => {
|
|
18353
18712
|
const id = signalIdCounter++;
|
|
@@ -18671,6 +19030,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18671
19030
|
const valueDescriptor = Object.getOwnPropertyDescriptor(signalProto, "value");
|
|
18672
19031
|
Object.defineProperty(preactSignal, "value", {
|
|
18673
19032
|
get() {
|
|
19033
|
+
if (valuesAsIf !== null && valuesAsIf.has(preactSignal)) {
|
|
19034
|
+
return valuesAsIf.get(preactSignal);
|
|
19035
|
+
}
|
|
18674
19036
|
return valueDescriptor.get.call(preactSignal);
|
|
18675
19037
|
},
|
|
18676
19038
|
set(newValue) {
|
|
@@ -27055,7 +27417,8 @@ const route = (
|
|
|
27055
27417
|
};
|
|
27056
27418
|
// Asks ahead of the arrival for what this route's actions ask without the
|
|
27057
27419
|
// address — a page's code, a read without params (see routeAction). An
|
|
27058
|
-
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
27420
|
+
// action keyed by a param waits for the arrival: a prefetch has no address
|
|
27421
|
+
// (preloadState is the one that has one).
|
|
27059
27422
|
route.preload = () => {
|
|
27060
27423
|
const preloadSet = routePreloadMap.get(route);
|
|
27061
27424
|
if (!preloadSet) {
|
|
@@ -27635,6 +27998,40 @@ const preloadUrl = (url) => {
|
|
|
27635
27998
|
}
|
|
27636
27999
|
}
|
|
27637
28000
|
};
|
|
28001
|
+
|
|
28002
|
+
const routeStatePreloadMap = new WeakMap();
|
|
28003
|
+
const registerRouteStatePreload = (route, preload) => {
|
|
28004
|
+
let preloadSet = routeStatePreloadMap.get(route);
|
|
28005
|
+
if (!preloadSet) {
|
|
28006
|
+
preloadSet = new Set();
|
|
28007
|
+
routeStatePreloadMap.set(route, preloadSet);
|
|
28008
|
+
}
|
|
28009
|
+
preloadSet.add(preload);
|
|
28010
|
+
};
|
|
28011
|
+
/**
|
|
28012
|
+
* Preloads what the routes one is on will ask for once `signal` holds
|
|
28013
|
+
* `value`: the address a popup's opening is about to write (see
|
|
28014
|
+
* open_controller.js), known at the press that opens it. Only the routes
|
|
28015
|
+
* matching now are asked — writing a state keeps one on the same page.
|
|
28016
|
+
*/
|
|
28017
|
+
const preloadState = (signal, value) => {
|
|
28018
|
+
if (!activeRouteSet) {
|
|
28019
|
+
return;
|
|
28020
|
+
}
|
|
28021
|
+
const values = new Map([[signal, value]]);
|
|
28022
|
+
for (const route of activeRouteSet) {
|
|
28023
|
+
if (!route.matchingSignal.peek()) {
|
|
28024
|
+
continue;
|
|
28025
|
+
}
|
|
28026
|
+
const preloadSet = routeStatePreloadMap.get(route);
|
|
28027
|
+
if (!preloadSet) {
|
|
28028
|
+
continue;
|
|
28029
|
+
}
|
|
28030
|
+
for (const preload of preloadSet) {
|
|
28031
|
+
preload(values);
|
|
28032
|
+
}
|
|
28033
|
+
}
|
|
28034
|
+
};
|
|
27638
28035
|
const setupRoutes = (routes) => {
|
|
27639
28036
|
if (setupRoutesCalled) {
|
|
27640
28037
|
throw new Error(
|
|
@@ -28228,112 +28625,6 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
28228
28625
|
return stateWithoutGeneratedIdKeys;
|
|
28229
28626
|
};
|
|
28230
28627
|
|
|
28231
|
-
/**
|
|
28232
|
-
* The document's rendering, held for the one frame a view transition needs.
|
|
28233
|
-
*
|
|
28234
|
-
* The browser does not take the picture of the page being left when a
|
|
28235
|
-
* transition is ASKED for — it takes it at the next frame, just before running
|
|
28236
|
-
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
28237
|
-
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
28238
|
-
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
28239
|
-
* Both sides of the animation then show it, and one watches a page slide onto
|
|
28240
|
-
* itself.
|
|
28241
|
-
*
|
|
28242
|
-
* So what Preact has queued waits until the update callback, which is the
|
|
28243
|
-
* moment the API is built around — the change belongs inside it. The whole
|
|
28244
|
-
* document is held: it is about to be frozen under a picture anyway.
|
|
28245
|
-
*
|
|
28246
|
-
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
28247
|
-
* around Preact's `options.debounceRendering`, and two of them installed
|
|
28248
|
-
* independently restore each other in the wrong order when they let go — every
|
|
28249
|
-
* render queued in between is then handed to a wrapper nobody will ever
|
|
28250
|
-
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
28251
|
-
* route transition) must therefore hold through this module, never through a
|
|
28252
|
-
* wrapper of its own.
|
|
28253
|
-
*/
|
|
28254
|
-
|
|
28255
|
-
|
|
28256
|
-
let renderingHold = null;
|
|
28257
|
-
const holdRendering = () => {
|
|
28258
|
-
if (renderingHold) {
|
|
28259
|
-
return renderingHold.release;
|
|
28260
|
-
}
|
|
28261
|
-
const debounceRenderingBefore = options.debounceRendering;
|
|
28262
|
-
const hold = {
|
|
28263
|
-
render: null,
|
|
28264
|
-
waiting: [],
|
|
28265
|
-
release: () => {
|
|
28266
|
-
// Only the hold that is still standing may be given back: a holder
|
|
28267
|
-
// releasing after another has taken over must not let go of what it
|
|
28268
|
-
// does not hold.
|
|
28269
|
-
if (renderingHold !== hold) {
|
|
28270
|
-
return;
|
|
28271
|
-
}
|
|
28272
|
-
renderingHold = null;
|
|
28273
|
-
options.debounceRendering = debounceRenderingBefore;
|
|
28274
|
-
const { render, waiting } = hold;
|
|
28275
|
-
hold.render = null;
|
|
28276
|
-
hold.waiting = [];
|
|
28277
|
-
if (render) {
|
|
28278
|
-
render();
|
|
28279
|
-
}
|
|
28280
|
-
for (const wait of waiting) {
|
|
28281
|
-
wait();
|
|
28282
|
-
}
|
|
28283
|
-
},
|
|
28284
|
-
};
|
|
28285
|
-
renderingHold = hold;
|
|
28286
|
-
options.debounceRendering = (render) => {
|
|
28287
|
-
hold.render = render;
|
|
28288
|
-
};
|
|
28289
|
-
return hold.release;
|
|
28290
|
-
};
|
|
28291
|
-
|
|
28292
|
-
// Anything else that must not happen before the picture is taken, and the
|
|
28293
|
-
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
28294
|
-
// document put back to its top while the page being left is still on screen is
|
|
28295
|
-
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
28296
|
-
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
28297
|
-
// the band it had already painted — the page being left is then seen in
|
|
28298
|
-
// fragments, whatever the movement does afterwards.
|
|
28299
|
-
//
|
|
28300
|
-
// Run at once when nobody is photographing anything, which is the common case
|
|
28301
|
-
// and must stay free.
|
|
28302
|
-
const whenRenderingResumes = (callback) => {
|
|
28303
|
-
if (!renderingHold) {
|
|
28304
|
-
callback();
|
|
28305
|
-
return;
|
|
28306
|
-
}
|
|
28307
|
-
renderingHold.waiting.push(callback);
|
|
28308
|
-
};
|
|
28309
|
-
|
|
28310
|
-
// The hold a navigation takes on its way in — from before its first write,
|
|
28311
|
-
// because by the time a route announces that it matches, Preact has already
|
|
28312
|
-
// been told and the render is queued; a hold taken then is a hold taken too
|
|
28313
|
-
// late. Kept here until whoever animates the change takes it over, or the
|
|
28314
|
-
// navigation turns out to be one nobody animates.
|
|
28315
|
-
let routingRenderingHold = null;
|
|
28316
|
-
const holdRenderingForRouting = () => {
|
|
28317
|
-
routingRenderingHold = holdRendering();
|
|
28318
|
-
};
|
|
28319
|
-
// Nobody had a picture to take: a page held for a change it does not animate
|
|
28320
|
-
// is a page that stutters for nothing.
|
|
28321
|
-
const releaseRoutingRenderingHold = () => {
|
|
28322
|
-
const release = routingRenderingHold;
|
|
28323
|
-
routingRenderingHold = null;
|
|
28324
|
-
if (release) {
|
|
28325
|
-
release();
|
|
28326
|
-
}
|
|
28327
|
-
};
|
|
28328
|
-
// An animator takes the navigation's hold as its own — taking another would be
|
|
28329
|
-
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
28330
|
-
// the change it animates is not a navigation.
|
|
28331
|
-
const takeoverRoutingRenderingHold = () => {
|
|
28332
|
-
const release = routingRenderingHold || holdRendering();
|
|
28333
|
-
routingRenderingHold = null;
|
|
28334
|
-
return release;
|
|
28335
|
-
};
|
|
28336
|
-
|
|
28337
28628
|
/**
|
|
28338
28629
|
* Where a page was left, given back when one comes back to it.
|
|
28339
28630
|
*
|
|
@@ -28534,6 +28825,11 @@ const restoreScrollPosition = (url) => {
|
|
|
28534
28825
|
// So `from`, the url being left, tells an arrival from the same place said
|
|
28535
28826
|
// differently, and only the first is moved.
|
|
28536
28827
|
//
|
|
28828
|
+
// A fragment does not make it less of an arrival. The browser finds nothing to
|
|
28829
|
+
// scroll to after a pushState; url_target.js brings the element in once it
|
|
28830
|
+
// renders, from this top — and when it never comes, the page is read from its
|
|
28831
|
+
// top like any other.
|
|
28832
|
+
//
|
|
28537
28833
|
// The document, because the document is the scrollport in the common case. An
|
|
28538
28834
|
// app that scrolls an element of its own scrolls it itself.
|
|
28539
28835
|
const startAtTop = (url, { from } = {}) => {
|
|
@@ -28543,14 +28839,10 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
28543
28839
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
28544
28840
|
};
|
|
28545
28841
|
const isArrival = (url, { from }) => {
|
|
28546
|
-
const urlObject = new URL(url, window.location.href);
|
|
28547
|
-
// A fragment names where to land, and the browser is the one that finds it.
|
|
28548
|
-
if (urlObject.hash) {
|
|
28549
|
-
return false;
|
|
28550
|
-
}
|
|
28551
28842
|
if (
|
|
28552
28843
|
from !== undefined &&
|
|
28553
|
-
new URL(from, window.location.href).pathname ===
|
|
28844
|
+
new URL(from, window.location.href).pathname ===
|
|
28845
|
+
new URL(url, window.location.href).pathname
|
|
28554
28846
|
) {
|
|
28555
28847
|
return false;
|
|
28556
28848
|
}
|
|
@@ -28627,29 +28919,6 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28627
28919
|
}
|
|
28628
28920
|
};
|
|
28629
28921
|
|
|
28630
|
-
/**
|
|
28631
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
28632
|
-
*
|
|
28633
|
-
* Everything else a router says arrives once the change is made: a route
|
|
28634
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
28635
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
28636
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
28637
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
28638
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
28639
|
-
*
|
|
28640
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
28641
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
28642
|
-
* before the URL, the visited set, or any route has changed.
|
|
28643
|
-
*
|
|
28644
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
28645
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
28646
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
28647
|
-
*/
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
28651
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
28652
|
-
|
|
28653
28922
|
const getHrefTargetInfo = (href) => {
|
|
28654
28923
|
href = String(href);
|
|
28655
28924
|
|
|
@@ -29157,6 +29426,11 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
29157
29426
|
});
|
|
29158
29427
|
|
|
29159
29428
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
29429
|
+
if (isTargetWriteInProgress()) {
|
|
29430
|
+
// The fragment navigation navi makes to set `:target` (see
|
|
29431
|
+
// url_target.js): the entry keeps its address and its state.
|
|
29432
|
+
return;
|
|
29433
|
+
}
|
|
29160
29434
|
// The entry a pending replace was for is no longer the current one, and
|
|
29161
29435
|
// the History API has no way to write it (see replaceAddressWhenSettled).
|
|
29162
29436
|
dropPendingReplace();
|
|
@@ -29232,9 +29506,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
29232
29506
|
// one), so whoever awaits this reads a document url and state that
|
|
29233
29507
|
// already say where it landed.
|
|
29234
29508
|
const landedPromise = new Promise((resolve) => {
|
|
29235
|
-
|
|
29236
|
-
|
|
29237
|
-
|
|
29509
|
+
const onPopstate = () => {
|
|
29510
|
+
if (isTargetWriteInProgress()) {
|
|
29511
|
+
return;
|
|
29512
|
+
}
|
|
29513
|
+
window.removeEventListener("popstate", onPopstate);
|
|
29514
|
+
resolve(true);
|
|
29515
|
+
};
|
|
29516
|
+
window.addEventListener("popstate", onPopstate);
|
|
29238
29517
|
});
|
|
29239
29518
|
landOnPending = landOn || null;
|
|
29240
29519
|
window.history.back();
|
|
@@ -34226,10 +34505,15 @@ const RouteTravel = ({
|
|
|
34226
34505
|
// stopping. Which end decides how it is answered, and the two are not the
|
|
34227
34506
|
// same amount of work.
|
|
34228
34507
|
onEdge: ({
|
|
34229
|
-
sign
|
|
34508
|
+
sign,
|
|
34509
|
+
thrown
|
|
34230
34510
|
}) => {
|
|
34231
34511
|
const travel = travelRef.current;
|
|
34232
|
-
if (
|
|
34512
|
+
if (
|
|
34513
|
+
// Thrown on before its end: handing over from there would mean jumping
|
|
34514
|
+
// the pictures the rest of the way first (see scrubTravel below), a
|
|
34515
|
+
// page seen leaping. The travel caught simply arrives.
|
|
34516
|
+
thrown || !travel || travel.noPicture || travel.ended || travel.reverting || !CAN_KEEP_PICTURE) {
|
|
34233
34517
|
return false;
|
|
34234
34518
|
}
|
|
34235
34519
|
// Where the page in hand is coming from, said the way the gesture says
|
|
@@ -34806,10 +35090,14 @@ const writeCover = (edge, element, size) => {
|
|
|
34806
35090
|
* the address, which is also what makes it worth fetching AHEAD of the
|
|
34807
35091
|
* arrival: a link to the route preruns it when the pointer or the focus
|
|
34808
35092
|
* reaches the link (see docs/dynamic_import.md). An action whose params
|
|
34809
|
-
* come from the address is
|
|
35093
|
+
* come from the address is not prefetched by a link — a link's prefetch has
|
|
35094
|
+
* no address. It is by the press that opens a popup bound to a state these
|
|
35095
|
+
* params read: that address is the current one with the state set to the
|
|
35096
|
+
* popup's value (see `prefetch` on Button).
|
|
34810
35097
|
* @param {object} [options]
|
|
34811
|
-
* @param {boolean} [options.prefetch=true] - `false` keeps
|
|
34812
|
-
*
|
|
35098
|
+
* @param {boolean} [options.prefetch=true] - `false` keeps the action from
|
|
35099
|
+
* being prerun ahead of what asks for it, when the read is not worth a
|
|
35100
|
+
* hover or a press that may not end in an opening.
|
|
34813
35101
|
*/
|
|
34814
35102
|
const routeAction = (
|
|
34815
35103
|
routeOrRoutes,
|
|
@@ -34838,27 +35126,47 @@ const routeAction = (
|
|
|
34838
35126
|
},
|
|
34839
35127
|
options,
|
|
34840
35128
|
);
|
|
34841
|
-
if (
|
|
35129
|
+
if (prefetch) {
|
|
34842
35130
|
for (const route of routes) {
|
|
34843
|
-
|
|
34844
|
-
|
|
34845
|
-
|
|
35131
|
+
if (paramsEffect) {
|
|
35132
|
+
registerRouteStatePreload(route, (values) => {
|
|
35133
|
+
prefetchAsIf(action, paramsEffect, values);
|
|
35134
|
+
});
|
|
35135
|
+
} else {
|
|
35136
|
+
registerRoutePreload(route, () => {
|
|
35137
|
+
prerunAhead(action.bindParams(true));
|
|
35138
|
+
});
|
|
35139
|
+
}
|
|
34846
35140
|
}
|
|
34847
35141
|
}
|
|
34848
35142
|
return actionBoundToRoute;
|
|
34849
35143
|
};
|
|
34850
35144
|
|
|
34851
|
-
// The
|
|
34852
|
-
// (
|
|
34853
|
-
//
|
|
34854
|
-
// and
|
|
34855
|
-
|
|
34856
|
-
|
|
34857
|
-
|
|
34858
|
-
|
|
34859
|
-
|
|
34860
|
-
|
|
34861
|
-
|
|
35145
|
+
// The params as they will read once the states hold what they are about to
|
|
35146
|
+
// (see readStateAsIf), prerun only when that changes them: params the state
|
|
35147
|
+
// leaves alone are the ones the action already runs on — or is meant not to,
|
|
35148
|
+
// and a prerun would retry a FAILED one behind the screen's back.
|
|
35149
|
+
const prefetchAsIf = (action, paramsEffect, values) => {
|
|
35150
|
+
const params = readStateAsIf(values, paramsEffect);
|
|
35151
|
+
if (!params) {
|
|
35152
|
+
return;
|
|
35153
|
+
}
|
|
35154
|
+
if (compareTwoJsValues(params, untracked(paramsEffect))) {
|
|
35155
|
+
return;
|
|
35156
|
+
}
|
|
35157
|
+
prerunAhead(action.bindParams(params));
|
|
35158
|
+
};
|
|
35159
|
+
|
|
35160
|
+
// The instance prerun is the one the arrival runs on — the one bound to the
|
|
35161
|
+
// params it reads (`true` for a param-less action) — so the arrival promotes
|
|
35162
|
+
// it to a run. A prefetch that fails is not reported: nothing on screen asked
|
|
35163
|
+
// for it, and the failure stays on the instance, where a run asks again — a
|
|
35164
|
+
// FAILED action is run, only RUNNING and COMPLETED are left alone — and the
|
|
35165
|
+
// arrival shows its own. It is left FAILED on purpose, not reset: a prerun is
|
|
35166
|
+
// often still in flight when the arrival promotes it (the press that brings
|
|
35167
|
+
// the arrival focuses the link, or is the one that asked), and a reset
|
|
35168
|
+
// landing after that would pull the answer from under the page reading it.
|
|
35169
|
+
const prerunAhead = (instance) => {
|
|
34862
35170
|
let result;
|
|
34863
35171
|
try {
|
|
34864
35172
|
result = instance.prerun({ reason: "intent" });
|
|
@@ -35872,6 +36180,30 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
35872
36180
|
},
|
|
35873
36181
|
};
|
|
35874
36182
|
});
|
|
36183
|
+
/**
|
|
36184
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
36185
|
+
* release that runs the command: what the opening will read can be asked for
|
|
36186
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
36187
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
36188
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
36189
|
+
* press is the first thing the finger says anyway.
|
|
36190
|
+
*
|
|
36191
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
36192
|
+
* runs here, and a source with no target says so at the release.
|
|
36193
|
+
*/
|
|
36194
|
+
const announceOpeningPress = (source, command) => {
|
|
36195
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
36196
|
+
return;
|
|
36197
|
+
}
|
|
36198
|
+
const commandFor = source.getAttribute("commandfor");
|
|
36199
|
+
const target = commandFor
|
|
36200
|
+
? document.getElementById(commandFor)
|
|
36201
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
36202
|
+
if (!target) {
|
|
36203
|
+
return;
|
|
36204
|
+
}
|
|
36205
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
36206
|
+
};
|
|
35875
36207
|
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
35876
36208
|
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
35877
36209
|
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
@@ -37330,7 +37662,7 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
37330
37662
|
return;
|
|
37331
37663
|
}
|
|
37332
37664
|
if (opened) {
|
|
37333
|
-
signal.value = popupValue
|
|
37665
|
+
signal.value = readOpenValue(popupValue);
|
|
37334
37666
|
return;
|
|
37335
37667
|
}
|
|
37336
37668
|
const closedValue = readClosedValue(signalValue);
|
|
@@ -37362,6 +37694,9 @@ const readOpened = (signalValue, popupValue) => {
|
|
|
37362
37694
|
}
|
|
37363
37695
|
return signalValue === popupValue;
|
|
37364
37696
|
};
|
|
37697
|
+
const readOpenValue = (popupValue) => {
|
|
37698
|
+
return popupValue === undefined ? true : popupValue;
|
|
37699
|
+
};
|
|
37365
37700
|
const readClosedValue = (openSignalValue) => {
|
|
37366
37701
|
return openSignalValue === true ? false : undefined;
|
|
37367
37702
|
};
|
|
@@ -37448,6 +37783,29 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
37448
37783
|
}
|
|
37449
37784
|
}
|
|
37450
37785
|
: null;
|
|
37786
|
+
// A press that will open this popup has started (see announceOpeningPress
|
|
37787
|
+
// in commands.js). What the opening writes — the popup's value, in its
|
|
37788
|
+
// signal — is known before the release, so what the screen reads off it is
|
|
37789
|
+
// asked for now (see preloadState).
|
|
37790
|
+
useEffect(() => {
|
|
37791
|
+
if (!signal) {
|
|
37792
|
+
return undefined;
|
|
37793
|
+
}
|
|
37794
|
+
const element = openController.getElement?.();
|
|
37795
|
+
if (!element) {
|
|
37796
|
+
return undefined;
|
|
37797
|
+
}
|
|
37798
|
+
const onOpenPress = () => {
|
|
37799
|
+
if (openController.opened) {
|
|
37800
|
+
return;
|
|
37801
|
+
}
|
|
37802
|
+
preloadState(signal, readOpenValue(value));
|
|
37803
|
+
};
|
|
37804
|
+
element.addEventListener("navi_open_press", onOpenPress);
|
|
37805
|
+
return () => {
|
|
37806
|
+
element.removeEventListener("navi_open_press", onOpenPress);
|
|
37807
|
+
};
|
|
37808
|
+
}, [signal, value]);
|
|
37451
37809
|
// Tracks whether the effect below has ever run before — only the very
|
|
37452
37810
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
37453
37811
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -38180,17 +38538,36 @@ const armOutsidePressClose = (
|
|
|
38180
38538
|
* content ahead of the click that will open it. Deferring the build to the
|
|
38181
38539
|
* opening puts its whole cost in the frame right after the click — the frame
|
|
38182
38540
|
* where a delay is felt hardest — while the ~100-300ms between hovering a
|
|
38183
|
-
* trigger and pressing it are free. The warming render
|
|
38184
|
-
*
|
|
38185
|
-
* the click, only before the open that follows it
|
|
38186
|
-
*
|
|
38187
|
-
*
|
|
38188
|
-
*
|
|
38189
|
-
*
|
|
38190
|
-
*
|
|
38191
|
-
*
|
|
38192
|
-
*
|
|
38193
|
-
*
|
|
38541
|
+
* trigger and pressing it are free. The warming render starts at the frame
|
|
38542
|
+
* after the one that follows the intent: nothing here needs the content in the
|
|
38543
|
+
* DOM before the click, only before the open that follows it, and on a touch
|
|
38544
|
+
* screen `pointerenter` arrives together with the `pointerdown` — a build
|
|
38545
|
+
* started there would hold back the frame that shows the press. Two animation
|
|
38546
|
+
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
38547
|
+
* and a timeout started by the press fires at its release, on top of the click.
|
|
38548
|
+
*
|
|
38549
|
+
* "while-opened" content is warmed by an opening press, never by a hover or a
|
|
38550
|
+
* focus. That mode promises content built fresh for the gesture that opens
|
|
38551
|
+
* it, and mounted only while that gesture and its opening last: callers lean
|
|
38552
|
+
* on it (several pickers sharing one set of content ids, because only one
|
|
38553
|
+
* content exists at a time). A pointer crossing four triggers would build four
|
|
38554
|
+
* contents; a press is the start of one opening.
|
|
38555
|
+
*
|
|
38556
|
+
* Only what opens the popup can say its press is one, and it says so as the
|
|
38557
|
+
* press starts, with a `navi_open_press` on the popup (see
|
|
38558
|
+
* announceOpeningPress in commands.js): a `--navi-open` button, a picker's
|
|
38559
|
+
* trigger when a press is what opens it, an expandable's UI part. Not the
|
|
38560
|
+
* anchor: it says where the popup is placed, not what opens it — a card
|
|
38561
|
+
* opened by a hold is pressed all day by taps meant for what it holds.
|
|
38562
|
+
*
|
|
38563
|
+
* The press builds the content, and the press ending without an open throws
|
|
38564
|
+
* it away — its click reaching the document with the popup still closed, no
|
|
38565
|
+
* click coming after the release, the browser taking the gesture
|
|
38566
|
+
* (`pointercancel`), or the next press starting, before any of its own
|
|
38567
|
+
* handlers run: one press at a time holds a content. A keyboard opening builds
|
|
38568
|
+
* at open time. A popup with an `onOpen` is never warmed: that callback runs
|
|
38569
|
+
* before the content is built (see open_controller.js), and a content seeding
|
|
38570
|
+
* itself from what it writes must be built after it.
|
|
38194
38571
|
*/
|
|
38195
38572
|
|
|
38196
38573
|
|
|
@@ -38232,26 +38609,35 @@ const usePopupContentMount = (
|
|
|
38232
38609
|
{ mount = MOUNT_DEFAULT, anchor },
|
|
38233
38610
|
) => {
|
|
38234
38611
|
const mountedAlways = mount === "always";
|
|
38235
|
-
const [contentMounted,
|
|
38612
|
+
const [contentMounted, setContentMountedState] = useState(
|
|
38236
38613
|
() => mountedAlways || openController.opened,
|
|
38237
38614
|
);
|
|
38238
|
-
|
|
38239
|
-
|
|
38240
|
-
|
|
38241
|
-
|
|
38242
|
-
|
|
38243
|
-
|
|
38244
|
-
|
|
38245
|
-
|
|
38246
|
-
|
|
38247
|
-
|
|
38248
|
-
|
|
38249
|
-
|
|
38250
|
-
|
|
38251
|
-
|
|
38252
|
-
|
|
38253
|
-
|
|
38254
|
-
|
|
38615
|
+
// What was last asked for, ahead of the render that performs it: an open
|
|
38616
|
+
// landing in between (a press-warmed content given up just before the open
|
|
38617
|
+
// that wanted it) must still find it has to build.
|
|
38618
|
+
const contentMountedRef = useRef(contentMounted);
|
|
38619
|
+
const setContentMounted = (value) => {
|
|
38620
|
+
contentMountedRef.current = value;
|
|
38621
|
+
setContentMountedState(value);
|
|
38622
|
+
};
|
|
38623
|
+
openController.mountContent = () => {
|
|
38624
|
+
if (contentMountedRef.current) {
|
|
38625
|
+
return;
|
|
38626
|
+
}
|
|
38627
|
+
const popupElement = ref?.current;
|
|
38628
|
+
if (popupElement) {
|
|
38629
|
+
popupsMountingContentForOpen.add(popupElement);
|
|
38630
|
+
}
|
|
38631
|
+
try {
|
|
38632
|
+
flushSyncRendering(() => {
|
|
38633
|
+
setContentMounted(true);
|
|
38634
|
+
});
|
|
38635
|
+
} finally {
|
|
38636
|
+
if (popupElement) {
|
|
38637
|
+
popupsMountingContentForOpen.delete(popupElement);
|
|
38638
|
+
}
|
|
38639
|
+
}
|
|
38640
|
+
};
|
|
38255
38641
|
openController.unmountContent =
|
|
38256
38642
|
mount === "while-opened"
|
|
38257
38643
|
? () => {
|
|
@@ -38289,41 +38675,140 @@ const usePopupContentMount = (
|
|
|
38289
38675
|
cancelIdle(idleId);
|
|
38290
38676
|
};
|
|
38291
38677
|
}, [mount, contentMounted]);
|
|
38292
|
-
// Warm on intent (see the top comment
|
|
38293
|
-
// The anchor accepts the same shapes Popover resolves at open time — a
|
|
38294
|
-
// string id, a ref, an element — but is resolved here at effect time: an id
|
|
38295
|
-
// that matches nothing yet simply doesn't warm, the open still mounts the
|
|
38296
|
-
// content like it always does.
|
|
38678
|
+
// Warm on intent (see the top comment).
|
|
38297
38679
|
useEffect(() => {
|
|
38298
38680
|
if (contentMounted || !anchor || mount === "while-opened") {
|
|
38299
38681
|
return undefined;
|
|
38300
38682
|
}
|
|
38301
|
-
const anchorElement =
|
|
38302
|
-
typeof anchor === "string"
|
|
38303
|
-
? document.getElementById(anchor)
|
|
38304
|
-
: // A ref is unwrapped even when it holds nothing: an expandable with
|
|
38305
|
-
// no UI part hands an empty ref over, and the ref object itself is
|
|
38306
|
-
// truthy — it would reach addEventListener below and throw.
|
|
38307
|
-
"current" in anchor
|
|
38308
|
-
? anchor.current
|
|
38309
|
-
: anchor;
|
|
38683
|
+
const anchorElement = resolveAnchorElement(anchor);
|
|
38310
38684
|
if (!anchorElement) {
|
|
38311
38685
|
return undefined;
|
|
38312
38686
|
}
|
|
38687
|
+
let cancelWarm = null;
|
|
38313
38688
|
const warm = () => {
|
|
38314
|
-
|
|
38689
|
+
if (cancelWarm) {
|
|
38690
|
+
return;
|
|
38691
|
+
}
|
|
38692
|
+
cancelWarm = requestFrameAfterNext(() => {
|
|
38693
|
+
setContentMounted(true);
|
|
38694
|
+
});
|
|
38315
38695
|
};
|
|
38316
38696
|
anchorElement.addEventListener("pointerenter", warm);
|
|
38317
38697
|
anchorElement.addEventListener("focusin", warm);
|
|
38318
38698
|
return () => {
|
|
38699
|
+
cancelWarm?.();
|
|
38319
38700
|
anchorElement.removeEventListener("pointerenter", warm);
|
|
38320
38701
|
anchorElement.removeEventListener("focusin", warm);
|
|
38321
38702
|
};
|
|
38322
38703
|
}, [contentMounted, anchor, mount]);
|
|
38704
|
+
// Warm on an opening press, for "while-opened" (see the top comment).
|
|
38705
|
+
// Listening for as long as the popup is there, not only while the content is
|
|
38706
|
+
// unmounted: the press that throws a warmed content away can be a new
|
|
38707
|
+
// opening press of this same popup, and it must warm again.
|
|
38708
|
+
useEffect(() => {
|
|
38709
|
+
if (mount !== "while-opened") {
|
|
38710
|
+
return undefined;
|
|
38711
|
+
}
|
|
38712
|
+
const element = openController.getElement?.();
|
|
38713
|
+
if (!element) {
|
|
38714
|
+
return undefined;
|
|
38715
|
+
}
|
|
38716
|
+
let stopPressWarm = null;
|
|
38717
|
+
const onOpeningPress = () => {
|
|
38718
|
+
if (contentMountedRef.current || openController.onOpen) {
|
|
38719
|
+
return;
|
|
38720
|
+
}
|
|
38721
|
+
const cancelBuild = requestFrameAfterNext(() => {
|
|
38722
|
+
setContentMounted(true);
|
|
38723
|
+
});
|
|
38724
|
+
const stopWatching = watchPressEnd(() => {
|
|
38725
|
+
stopPressWarm = null;
|
|
38726
|
+
cancelBuild();
|
|
38727
|
+
if (!openController.opened) {
|
|
38728
|
+
setContentMounted(false);
|
|
38729
|
+
}
|
|
38730
|
+
});
|
|
38731
|
+
stopPressWarm = () => {
|
|
38732
|
+
cancelBuild();
|
|
38733
|
+
stopWatching();
|
|
38734
|
+
};
|
|
38735
|
+
};
|
|
38736
|
+
element.addEventListener("navi_open_press", onOpeningPress);
|
|
38737
|
+
return () => {
|
|
38738
|
+
stopPressWarm?.();
|
|
38739
|
+
element.removeEventListener("navi_open_press", onOpeningPress);
|
|
38740
|
+
};
|
|
38741
|
+
}, [mount]);
|
|
38323
38742
|
|
|
38324
38743
|
return contentMounted;
|
|
38325
38744
|
};
|
|
38326
38745
|
|
|
38746
|
+
// The anchor accepts the same shapes Popover resolves at open time — a string
|
|
38747
|
+
// id, a ref, an element — but is resolved at effect time: an id that matches
|
|
38748
|
+
// nothing yet simply doesn't warm, the open still mounts the content.
|
|
38749
|
+
const resolveAnchorElement = (anchor) => {
|
|
38750
|
+
if (typeof anchor === "string") {
|
|
38751
|
+
return document.getElementById(anchor);
|
|
38752
|
+
}
|
|
38753
|
+
// A ref is unwrapped even when it holds nothing: an expandable with no UI
|
|
38754
|
+
// part hands an empty ref over, and the ref object itself is truthy — it
|
|
38755
|
+
// would reach addEventListener and throw.
|
|
38756
|
+
if ("current" in anchor) {
|
|
38757
|
+
return anchor.current;
|
|
38758
|
+
}
|
|
38759
|
+
return anchor;
|
|
38760
|
+
};
|
|
38761
|
+
|
|
38762
|
+
// The next frame paints what the input just changed (the pressed trigger); the
|
|
38763
|
+
// callback runs at the start of the one after.
|
|
38764
|
+
const requestFrameAfterNext = (callback) => {
|
|
38765
|
+
let frame = requestAnimationFrame(() => {
|
|
38766
|
+
frame = requestAnimationFrame(callback);
|
|
38767
|
+
});
|
|
38768
|
+
return () => {
|
|
38769
|
+
cancelAnimationFrame(frame);
|
|
38770
|
+
};
|
|
38771
|
+
};
|
|
38772
|
+
|
|
38773
|
+
// How long the click of a released press may take to arrive. Nothing tells a
|
|
38774
|
+
// page that a release brings no click — a press that became a drag has its
|
|
38775
|
+
// click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
|
|
38776
|
+
// press is taken to have ended without one. Longer than the 300ms a browser
|
|
38777
|
+
// still waits before the click of a tap on a page not sized for phones.
|
|
38778
|
+
const PRESS_CLICK_WAIT_MS = 500;
|
|
38779
|
+
|
|
38780
|
+
// Installed during the press's own pointerdown: the capture listeners below
|
|
38781
|
+
// only hear the presses after it.
|
|
38782
|
+
const watchPressEnd = (onEnd) => {
|
|
38783
|
+
let timeout;
|
|
38784
|
+
const stop = () => {
|
|
38785
|
+
clearTimeout(timeout);
|
|
38786
|
+
document.removeEventListener("click", onClick);
|
|
38787
|
+
document.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
38788
|
+
document.removeEventListener("pointercancel", end, { capture: true });
|
|
38789
|
+
document.removeEventListener("pointerdown", end, { capture: true });
|
|
38790
|
+
};
|
|
38791
|
+
const end = () => {
|
|
38792
|
+
stop();
|
|
38793
|
+
onEnd();
|
|
38794
|
+
};
|
|
38795
|
+
// Bubble phase, on the document: every click handler that could open the
|
|
38796
|
+
// popup has run by then.
|
|
38797
|
+
const onClick = (clickEvent) => {
|
|
38798
|
+
if (isPressDrivenClick(clickEvent)) {
|
|
38799
|
+
end();
|
|
38800
|
+
}
|
|
38801
|
+
};
|
|
38802
|
+
const onPointerUp = () => {
|
|
38803
|
+
timeout = setTimeout(end, PRESS_CLICK_WAIT_MS);
|
|
38804
|
+
};
|
|
38805
|
+
document.addEventListener("click", onClick);
|
|
38806
|
+
document.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
38807
|
+
document.addEventListener("pointercancel", end, { capture: true });
|
|
38808
|
+
document.addEventListener("pointerdown", end, { capture: true });
|
|
38809
|
+
return stop;
|
|
38810
|
+
};
|
|
38811
|
+
|
|
38327
38812
|
/**
|
|
38328
38813
|
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
38329
38814
|
* or popover visibility.
|
|
@@ -39381,6 +39866,9 @@ const useUIStateController = (
|
|
|
39381
39866
|
// The suggestion this control started on — what tells a field showing
|
|
39382
39867
|
// its default from one carrying an answer (see isUIStateHeld).
|
|
39383
39868
|
defaultValue: controlInfo.defaultValue,
|
|
39869
|
+
// The default last put on screen, which lags `defaultValue` while an
|
|
39870
|
+
// edit holds a newer one back (see followDefaultValue).
|
|
39871
|
+
defaultValueTaken: controlInfo.defaultValue,
|
|
39384
39872
|
|
|
39385
39873
|
facadeChild: null,
|
|
39386
39874
|
// Set for the duration of one interaction by whatever wants the
|
|
@@ -39490,6 +39978,18 @@ const useUIStateController = (
|
|
|
39490
39978
|
e.currentTarget || controller.ref.current,
|
|
39491
39979
|
);
|
|
39492
39980
|
const currentUIState = controller.uiState;
|
|
39981
|
+
// What a control is STARTED on is accepted, as its own defaultValue
|
|
39982
|
+
// would be: a value placed on it as it registers, or what a picker
|
|
39983
|
+
// takes up from its popup while it holds nothing of its own. A
|
|
39984
|
+
// rollback goes back there, and a control left on it holds no edit.
|
|
39985
|
+
if (!controller.hasStateProp) {
|
|
39986
|
+
if (
|
|
39987
|
+
e.type === "initial_state_push" ||
|
|
39988
|
+
(e.type === "facade_child_mount_sync" && !controller.holdsEdit())
|
|
39989
|
+
) {
|
|
39990
|
+
controller.state = newUIState;
|
|
39991
|
+
}
|
|
39992
|
+
}
|
|
39493
39993
|
const stateIsTheSame = compareTwoJsValues(newUIState, currentUIState);
|
|
39494
39994
|
if (stateIsTheSame) {
|
|
39495
39995
|
if (controlType === "button" || controlType === "link") {
|
|
@@ -39655,7 +40155,12 @@ const useUIStateController = (
|
|
|
39655
40155
|
}
|
|
39656
40156
|
// initial_state_push is pure initialization (equivalent to defaultValue on the
|
|
39657
40157
|
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
39658
|
-
|
|
40158
|
+
// A value the outside moved since (see outside_value_follow) is the
|
|
40159
|
+
// same initialization, later.
|
|
40160
|
+
if (
|
|
40161
|
+
e.type !== "initial_state_push" &&
|
|
40162
|
+
e.type !== "outside_value_follow"
|
|
40163
|
+
) {
|
|
39659
40164
|
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
39660
40165
|
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
39661
40166
|
// both would cause an infinite loop when a parent cascades state
|
|
@@ -39709,6 +40214,19 @@ const useUIStateController = (
|
|
|
39709
40214
|
stateChanged: true,
|
|
39710
40215
|
});
|
|
39711
40216
|
}
|
|
40217
|
+
if (
|
|
40218
|
+
e.type === "outside_value_follow" ||
|
|
40219
|
+
(e.type === "facade_child_mount_sync" && isOutsideValueFollow(e))
|
|
40220
|
+
) {
|
|
40221
|
+
// Heard the way a control mounting on that value would be:
|
|
40222
|
+
// silently, nobody acted. But heard, all the way up — a picker
|
|
40223
|
+
// whose popup followed tells its own group in turn — or what
|
|
40224
|
+
// the groups send is not what is on screen.
|
|
40225
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
40226
|
+
stateChanged: true,
|
|
40227
|
+
silent: true,
|
|
40228
|
+
});
|
|
40229
|
+
}
|
|
39712
40230
|
return true;
|
|
39713
40231
|
}
|
|
39714
40232
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -39816,6 +40334,41 @@ const useUIStateController = (
|
|
|
39816
40334
|
// pick — the signal would hand it straight back on the next render,
|
|
39817
40335
|
// and the refused value would win over the rollback.
|
|
39818
40336
|
writeBoundSignal(controller.state);
|
|
40337
|
+
controller.followDefaultValue();
|
|
40338
|
+
},
|
|
40339
|
+
// Something typed, picked or toggled since the value the outside last
|
|
40340
|
+
// gave or accepted.
|
|
40341
|
+
holdsEdit: () => {
|
|
40342
|
+
return !compareTwoJsValues(controller.uiState, controller.state);
|
|
40343
|
+
},
|
|
40344
|
+
// An uncontrolled control starts on its defaultValue, and a new one is
|
|
40345
|
+
// what it would start on if it were mounted again — a record saved
|
|
40346
|
+
// and handed back normalized, refreshed from elsewhere. It takes it
|
|
40347
|
+
// while it holds no edit. An edit is never undone; the default waits
|
|
40348
|
+
// for it to be accepted or rolled back.
|
|
40349
|
+
followDefaultValue: () => {
|
|
40350
|
+
// A bound signal carries its own default, followed through
|
|
40351
|
+
// stateFromSignal; a proxy shows the control it stands for.
|
|
40352
|
+
if (
|
|
40353
|
+
controller.hasStateProp ||
|
|
40354
|
+
controller.isProxy ||
|
|
40355
|
+
controller.props.signal
|
|
40356
|
+
) {
|
|
40357
|
+
return;
|
|
40358
|
+
}
|
|
40359
|
+
const { defaultValue } = controller;
|
|
40360
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
40361
|
+
return;
|
|
40362
|
+
}
|
|
40363
|
+
if (controller.holdsEdit()) {
|
|
40364
|
+
return;
|
|
40365
|
+
}
|
|
40366
|
+
controller.defaultValueTaken = defaultValue;
|
|
40367
|
+
controller.state = defaultValue;
|
|
40368
|
+
controller.setUIState(
|
|
40369
|
+
defaultValue,
|
|
40370
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
40371
|
+
);
|
|
39819
40372
|
},
|
|
39820
40373
|
// Read by the callout manager when it has nowhere else to point.
|
|
39821
40374
|
getCalloutAnchorElement: (event) =>
|
|
@@ -39829,7 +40382,12 @@ const useUIStateController = (
|
|
|
39829
40382
|
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
39830
40383
|
return;
|
|
39831
40384
|
}
|
|
40385
|
+
// A picker told yes is told for what its popup shows too, the way a
|
|
40386
|
+
// group tells its children. First, so that a default one of them
|
|
40387
|
+
// takes on the way is part of what this control accepts below.
|
|
40388
|
+
controller.facadeChild?.acknowledgeUIState();
|
|
39832
40389
|
controller.state = controller.uiState;
|
|
40390
|
+
controller.followDefaultValue();
|
|
39833
40391
|
},
|
|
39834
40392
|
onActionEnd: (e) => {
|
|
39835
40393
|
debugUIState(`"${controlType}" actionEnd called`);
|
|
@@ -39986,6 +40544,7 @@ const useUIStateController = (
|
|
|
39986
40544
|
}
|
|
39987
40545
|
}
|
|
39988
40546
|
}
|
|
40547
|
+
controller.followDefaultValue();
|
|
39989
40548
|
}
|
|
39990
40549
|
return liveValues();
|
|
39991
40550
|
},
|
|
@@ -40576,7 +41135,12 @@ const useUIGroupStateController = (
|
|
|
40576
41135
|
// deferred alongside a mount sync stays a real change.
|
|
40577
41136
|
const pendingChange = pendingChangeRef.current;
|
|
40578
41137
|
pendingChangeRef.current = {
|
|
40579
|
-
|
|
41138
|
+
// A default followed is not replaced by a mount deferred beside
|
|
41139
|
+
// it: the event is what lets the sync below read the children.
|
|
41140
|
+
e:
|
|
41141
|
+
pendingChange && isOutsideValueFollow(pendingChange.e)
|
|
41142
|
+
? pendingChange.e
|
|
41143
|
+
: e,
|
|
40580
41144
|
notifyExternal:
|
|
40581
41145
|
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
40582
41146
|
actingChild,
|
|
@@ -40584,6 +41148,13 @@ const useUIGroupStateController = (
|
|
|
40584
41148
|
return;
|
|
40585
41149
|
}
|
|
40586
41150
|
const { controller } = s;
|
|
41151
|
+
if (isOutsideValueFollow(e)) {
|
|
41152
|
+
// Not a partial reading: the child was there already and its value
|
|
41153
|
+
// moved, told by the outside. The group is worth what its children
|
|
41154
|
+
// show, whatever it was handed before — kept, the value it sends
|
|
41155
|
+
// would be one the screen no longer shows.
|
|
41156
|
+
controller.stateGivenFromAbove = false;
|
|
41157
|
+
}
|
|
40587
41158
|
// A child mounting or unmounting is not somebody answering: while the
|
|
40588
41159
|
// children of a group are still arriving, their aggregate is a partial
|
|
40589
41160
|
// reading, and taking it for the truth is how the value the group was
|
|
@@ -40762,6 +41333,9 @@ const useUIGroupStateController = (
|
|
|
40762
41333
|
name,
|
|
40763
41334
|
value,
|
|
40764
41335
|
defaultValue,
|
|
41336
|
+
// Same as the leaf's (see useUIStateController): the default last
|
|
41337
|
+
// placed on the children, behind `defaultValue` while an edit waits.
|
|
41338
|
+
defaultValueTaken: defaultValue,
|
|
40765
41339
|
hasValueProp,
|
|
40766
41340
|
hasDefaultValueProp,
|
|
40767
41341
|
props,
|
|
@@ -41045,6 +41619,7 @@ const useUIGroupStateController = (
|
|
|
41045
41619
|
}
|
|
41046
41620
|
}
|
|
41047
41621
|
onChange(e, { notifyExternal: true });
|
|
41622
|
+
controller.followDefaultValue();
|
|
41048
41623
|
},
|
|
41049
41624
|
clearUIState: (e) => {
|
|
41050
41625
|
const ev = new CustomEvent("propagate_down_clear_ui_state", {
|
|
@@ -41073,6 +41648,46 @@ const useUIGroupStateController = (
|
|
|
41073
41648
|
c.acknowledgeUIState();
|
|
41074
41649
|
}
|
|
41075
41650
|
}
|
|
41651
|
+
controller.followDefaultValue();
|
|
41652
|
+
},
|
|
41653
|
+
// A group holds no value of its own to have edited: it holds an edit
|
|
41654
|
+
// when one of its children does.
|
|
41655
|
+
holdsEdit: () => {
|
|
41656
|
+
return childUIStateControllerArray.some(
|
|
41657
|
+
(c) => shouldPropagateStateToChild(c) && c.holdsEdit(),
|
|
41658
|
+
);
|
|
41659
|
+
},
|
|
41660
|
+
// The leaf's rule (see useUIStateController), for a group given a
|
|
41661
|
+
// plain `defaultValue`: a new one is placed on the children the way
|
|
41662
|
+
// the first one was as they registered, while none of them holds an
|
|
41663
|
+
// edit. A signal's own default is followed by the update below.
|
|
41664
|
+
followDefaultValue: () => {
|
|
41665
|
+
if (controller.hasValueProp || controller.props.signal) {
|
|
41666
|
+
return;
|
|
41667
|
+
}
|
|
41668
|
+
const { defaultValue } = controller;
|
|
41669
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
41670
|
+
return;
|
|
41671
|
+
}
|
|
41672
|
+
if (controller.holdsEdit()) {
|
|
41673
|
+
return;
|
|
41674
|
+
}
|
|
41675
|
+
controller.defaultValueTaken = defaultValue;
|
|
41676
|
+
const followEvent = new CustomEvent("outside_value_follow", {
|
|
41677
|
+
detail: {},
|
|
41678
|
+
});
|
|
41679
|
+
const placeEvent = new CustomEvent("initial_state_push", {
|
|
41680
|
+
detail: {},
|
|
41681
|
+
});
|
|
41682
|
+
chainEvent(placeEvent, followEvent);
|
|
41683
|
+
controller.setUIState(
|
|
41684
|
+
defaultValue === undefined ? fallbackState : defaultValue,
|
|
41685
|
+
placeEvent,
|
|
41686
|
+
);
|
|
41687
|
+
s.parentUIStateController?.onChildUIAction(controller, followEvent, {
|
|
41688
|
+
stateChanged: true,
|
|
41689
|
+
silent: true,
|
|
41690
|
+
});
|
|
41076
41691
|
},
|
|
41077
41692
|
onActionEnd: (e) => {
|
|
41078
41693
|
acknowledgeOwnAction(controller);
|
|
@@ -41195,6 +41810,7 @@ const useUIGroupStateController = (
|
|
|
41195
41810
|
// former value back over it on the next child interaction.
|
|
41196
41811
|
placeChildrenFrom(defaultValue);
|
|
41197
41812
|
}
|
|
41813
|
+
controller.followDefaultValue();
|
|
41198
41814
|
|
|
41199
41815
|
return liveValues();
|
|
41200
41816
|
},
|
|
@@ -41421,7 +42037,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41421
42037
|
if (child !== firstChildControllerRef.current) {
|
|
41422
42038
|
return;
|
|
41423
42039
|
}
|
|
41424
|
-
if (
|
|
42040
|
+
if (
|
|
42041
|
+
silent &&
|
|
42042
|
+
!isOutsideValueFollow(e) &&
|
|
42043
|
+
uiStateHoldsNothing(child.uiState)
|
|
42044
|
+
) {
|
|
41425
42045
|
// A silent sync means the child's own structure changed (children
|
|
41426
42046
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
41427
42047
|
// with no value there is one that currently *cannot* express one —
|
|
@@ -41432,7 +42052,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41432
42052
|
// still nothing to adopt: the sync would only respell one nothing
|
|
41433
42053
|
// as another (an array picker's [] becoming undefined, say) and
|
|
41434
42054
|
// hand that to uiAction — an empty array picker opened its popup
|
|
41435
|
-
// and told its owner the value changed.
|
|
42055
|
+
// and told its owner the value changed. A default followed down to
|
|
42056
|
+
// nothing is not that: the outside emptied a value the child held.
|
|
41436
42057
|
return;
|
|
41437
42058
|
}
|
|
41438
42059
|
updatingRef.current = true;
|
|
@@ -41496,6 +42117,69 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41496
42117
|
return scope.controller;
|
|
41497
42118
|
};
|
|
41498
42119
|
|
|
42120
|
+
/**
|
|
42121
|
+
* What every control of a picker — the picker and the controls in its popup —
|
|
42122
|
+
* has been accepted on as the popup opens. Read back by followOutsideSinceOpen
|
|
42123
|
+
* when the popup is cancelled.
|
|
42124
|
+
*/
|
|
42125
|
+
const readStatesAtOpen = (pickerController) => {
|
|
42126
|
+
const statesAtOpen = new Map();
|
|
42127
|
+
visitControllers(pickerController, (controller) => {
|
|
42128
|
+
if (Object.hasOwn(controller, "state")) {
|
|
42129
|
+
statesAtOpen.set(controller, controller.state);
|
|
42130
|
+
}
|
|
42131
|
+
});
|
|
42132
|
+
return statesAtOpen;
|
|
42133
|
+
};
|
|
42134
|
+
|
|
42135
|
+
/**
|
|
42136
|
+
* A cancel puts back the value the picker held at open, which takes back
|
|
42137
|
+
* everything that moved since — and not only what the user moved: a record
|
|
42138
|
+
* refreshed while the popup was open, a default that arrived under a field
|
|
42139
|
+
* holding an edit. Those were not the user's to take back. Once the value at
|
|
42140
|
+
* open is back, each control the outside moved since shows what the outside
|
|
42141
|
+
* holds again, and a default left waiting on an edit is taken there.
|
|
42142
|
+
*
|
|
42143
|
+
* Children before the control around them: a picker given its value by its
|
|
42144
|
+
* owner has the last word over the controls in its popup.
|
|
42145
|
+
*/
|
|
42146
|
+
const followOutsideSinceOpen = (pickerController, statesAtOpen) => {
|
|
42147
|
+
visitControllers(
|
|
42148
|
+
pickerController,
|
|
42149
|
+
(controller) => {
|
|
42150
|
+
if (
|
|
42151
|
+
statesAtOpen.has(controller) &&
|
|
42152
|
+
!compareTwoJsValues(controller.state, statesAtOpen.get(controller)) &&
|
|
42153
|
+
controller.holdsEdit()
|
|
42154
|
+
) {
|
|
42155
|
+
controller.setUIState(
|
|
42156
|
+
controller.state,
|
|
42157
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
42158
|
+
);
|
|
42159
|
+
}
|
|
42160
|
+
controller.followDefaultValue();
|
|
42161
|
+
},
|
|
42162
|
+
{ childrenFirst: true },
|
|
42163
|
+
);
|
|
42164
|
+
};
|
|
42165
|
+
|
|
42166
|
+
// A picker reaches the controls of its popup through its facade child, a group
|
|
42167
|
+
// through the children it registered.
|
|
42168
|
+
const visitControllers = (controller, visit, { childrenFirst } = {}) => {
|
|
42169
|
+
if (!childrenFirst) {
|
|
42170
|
+
visit(controller);
|
|
42171
|
+
}
|
|
42172
|
+
const children = controller.facadeChild
|
|
42173
|
+
? [controller.facadeChild]
|
|
42174
|
+
: controller.getChildControllers?.() || [];
|
|
42175
|
+
for (const child of children) {
|
|
42176
|
+
visitControllers(child, visit, { childrenFirst });
|
|
42177
|
+
}
|
|
42178
|
+
if (childrenFirst) {
|
|
42179
|
+
visit(controller);
|
|
42180
|
+
}
|
|
42181
|
+
};
|
|
42182
|
+
|
|
41499
42183
|
const describePicker = (props) =>
|
|
41500
42184
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
41501
42185
|
|
|
@@ -41584,10 +42268,18 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
41584
42268
|
// no action of the control's own — but what it holds really did move, so
|
|
41585
42269
|
// uiAction, the bound signal and the parent notification below all happen.
|
|
41586
42270
|
"auto_fix",
|
|
42271
|
+
// A control put on a value the outside moved while nobody was editing it: a
|
|
42272
|
+
// new defaultValue (see followDefaultValue), or what a cancel puts back
|
|
42273
|
+
// (see followOutsideSinceOpen). Initialization arriving after mount, so
|
|
42274
|
+
// nothing fires, and the groups above hear it silently.
|
|
42275
|
+
"outside_value_follow",
|
|
41587
42276
|
]);
|
|
41588
42277
|
const isInternalEvent = (e) => {
|
|
41589
42278
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
41590
42279
|
};
|
|
42280
|
+
const isOutsideValueFollow = (e) => {
|
|
42281
|
+
return Boolean(findEvent(e, "outside_value_follow"));
|
|
42282
|
+
};
|
|
41591
42283
|
|
|
41592
42284
|
/**
|
|
41593
42285
|
* A value handed DOWN to a control by whoever owns it: a picker filling its
|
|
@@ -47106,6 +47798,35 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
|
|
|
47106
47798
|
}, [ref, href, prefetch]);
|
|
47107
47799
|
};
|
|
47108
47800
|
|
|
47801
|
+
/**
|
|
47802
|
+
* What a popup's opening will read, asked for when the press on the element
|
|
47803
|
+
* whose command opens it starts (see announceOpeningPress): the popup, the
|
|
47804
|
+
* value it opens on and so the address it will write are all known before
|
|
47805
|
+
* the release.
|
|
47806
|
+
*/
|
|
47807
|
+
const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
47808
|
+
useEffect(() => {
|
|
47809
|
+
if (!prefetch || !command) {
|
|
47810
|
+
return undefined;
|
|
47811
|
+
}
|
|
47812
|
+
const element = ref.current;
|
|
47813
|
+
if (!element) {
|
|
47814
|
+
return undefined;
|
|
47815
|
+
}
|
|
47816
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
47817
|
+
// A right click opens a menu, never the popup.
|
|
47818
|
+
if (pointerdownEvent.button !== 0) {
|
|
47819
|
+
return;
|
|
47820
|
+
}
|
|
47821
|
+
announceOpeningPress(element, command);
|
|
47822
|
+
};
|
|
47823
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
47824
|
+
return () => {
|
|
47825
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
47826
|
+
};
|
|
47827
|
+
}, [ref, command, prefetch]);
|
|
47828
|
+
};
|
|
47829
|
+
|
|
47109
47830
|
/**
|
|
47110
47831
|
* Hook that reactively checks if a URL is visited.
|
|
47111
47832
|
* Re-renders when the visited URL set changes.
|
|
@@ -51115,7 +51836,8 @@ const useExpandableContext = partName => {
|
|
|
51115
51836
|
* content — and rebuilds it from scratch on every expansion. Under the
|
|
51116
51837
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
51117
51838
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
51118
|
-
*
|
|
51839
|
+
* built by a press on it only, and thrown away if that press does not
|
|
51840
|
+
* expand it.
|
|
51119
51841
|
*/
|
|
51120
51842
|
const Expandable = props => {
|
|
51121
51843
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -51194,8 +51916,14 @@ const Expandable = props => {
|
|
|
51194
51916
|
// before the click ever fires — so what held the focus has to be remembered
|
|
51195
51917
|
// at pointerdown time.
|
|
51196
51918
|
const focusedAtPointerDownRef = useRef(null);
|
|
51197
|
-
const onUIPointerDown =
|
|
51919
|
+
const onUIPointerDown = pointerdownEvent => {
|
|
51198
51920
|
focusedAtPointerDownRef.current = document.activeElement;
|
|
51921
|
+
// The start of an expansion, announced as a --navi-open button announces
|
|
51922
|
+
// its own (see announceOpeningPress in commands.js) — unless aimed at a
|
|
51923
|
+
// control inside the UI part, whose click does not toggle (see onUIClick).
|
|
51924
|
+
if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
|
|
51925
|
+
dispatchCustomEvent(rootRef.current, "navi_open_press");
|
|
51926
|
+
}
|
|
51199
51927
|
};
|
|
51200
51928
|
|
|
51201
51929
|
// The content keeps its final size while the track animates (see the top
|
|
@@ -51493,17 +52221,18 @@ const Expandable = props => {
|
|
|
51493
52221
|
if (clickEvent.defaultPrevented) {
|
|
51494
52222
|
return;
|
|
51495
52223
|
}
|
|
51496
|
-
|
|
51497
|
-
|
|
51498
|
-
} = clickEvent;
|
|
51499
|
-
if (target.nodeType === 1) {
|
|
51500
|
-
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
51501
|
-
if (interactiveElement && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement)) {
|
|
51502
|
-
return;
|
|
51503
|
-
}
|
|
52224
|
+
if (isAimedAtControlInsideUI(clickEvent.target)) {
|
|
52225
|
+
return;
|
|
51504
52226
|
}
|
|
51505
52227
|
toggle(clickEvent);
|
|
51506
52228
|
};
|
|
52229
|
+
const isAimedAtControlInsideUI = target => {
|
|
52230
|
+
if (target.nodeType !== 1) {
|
|
52231
|
+
return false;
|
|
52232
|
+
}
|
|
52233
|
+
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
52234
|
+
return Boolean(interactiveElement) && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement);
|
|
52235
|
+
};
|
|
51507
52236
|
|
|
51508
52237
|
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
51509
52238
|
// control inside it belongs to that control.
|
|
@@ -52427,6 +53156,9 @@ const ButtonUI = props => {
|
|
|
52427
53156
|
// Nothing to prefetch on the way to another document: the routes do not
|
|
52428
53157
|
// lead there.
|
|
52429
53158
|
usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
|
|
53159
|
+
// A press the button refuses opens nothing: nothing to ask ahead for.
|
|
53160
|
+
const refusesPress = basePseudoState[":read-only"] || basePseudoState[":disabled"] || loading;
|
|
53161
|
+
usePreloadOpeningOnPress(ref, props.command, prefetch && !refusesPress);
|
|
52430
53162
|
useAccentColorAttributes(ref, null, {
|
|
52431
53163
|
elementSelector: visualSelector
|
|
52432
53164
|
});
|
|
@@ -52921,8 +53653,11 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
52921
53653
|
* pointer or the focus arrives, ahead of the press (see
|
|
52922
53654
|
* docs/dynamic_import.md): the route actions that ask nothing of the
|
|
52923
53655
|
* address — a page's code, a read without params — are prerun; one keyed by
|
|
52924
|
-
* a param waits for the arrival.
|
|
52925
|
-
*
|
|
53656
|
+
* a param waits for the arrival. With a `--navi-open`/`--navi-toggle`
|
|
53657
|
+
* command instead, the press — not the hover: what it asks is keyed by the
|
|
53658
|
+
* popup — preruns the route actions whose params the popup's opening
|
|
53659
|
+
* changes, read as if its `signal` held its `value`. `false` for a
|
|
53660
|
+
* destination not worth fetching ahead.
|
|
52926
53661
|
* @param {boolean} [pressableDuringRouteTransition] Keep answering presses
|
|
52927
53662
|
* while a route transition plays: what a movement photographs goes deaf to
|
|
52928
53663
|
* the pointer for its whole length, and the door that opened the page — a
|
|
@@ -59835,6 +60570,52 @@ const SlideContainer = ({
|
|
|
59835
60570
|
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
59836
60571
|
paintDrag();
|
|
59837
60572
|
},
|
|
60573
|
+
// The slide being brought in walked whole and the hand still going, or a
|
|
60574
|
+
// travel caught on its way thrown on: either way the hand asks for the
|
|
60575
|
+
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
60576
|
+
// for that — the slide reached becomes the one dragged, the one the hand
|
|
60577
|
+
// came from stays next to it for a hand that turns around, and the slide
|
|
60578
|
+
// past it is put on the other side.
|
|
60579
|
+
onEdge: ({
|
|
60580
|
+
axis,
|
|
60581
|
+
sign
|
|
60582
|
+
}) => {
|
|
60583
|
+
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
60584
|
+
const {
|
|
60585
|
+
slideElements,
|
|
60586
|
+
placeOf
|
|
60587
|
+
} = readMap();
|
|
60588
|
+
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
60589
|
+
if (!reachedElement) {
|
|
60590
|
+
return false;
|
|
60591
|
+
}
|
|
60592
|
+
// The gate goToArea reads at the release, asked of the slide the hand
|
|
60593
|
+
// is now in: a slide holding on to the user is not walked through.
|
|
60594
|
+
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
60595
|
+
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
60596
|
+
if (!past) {
|
|
60597
|
+
return false;
|
|
60598
|
+
}
|
|
60599
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
60600
|
+
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
60601
|
+
x: 0,
|
|
60602
|
+
y: 0
|
|
60603
|
+
};
|
|
60604
|
+
drag.areaBack = sign > 0 ? past : drag.area;
|
|
60605
|
+
drag.areaOn = sign > 0 ? drag.area : past;
|
|
60606
|
+
drag.area = reached;
|
|
60607
|
+
drag.basePlace = basePlace;
|
|
60608
|
+
drag.baseOffset = {
|
|
60609
|
+
x: -basePlace.x * drag.box.width,
|
|
60610
|
+
y: -basePlace.y * drag.box.height
|
|
60611
|
+
};
|
|
60612
|
+
stageDrag(drag, readArea(currentElement));
|
|
60613
|
+
return {
|
|
60614
|
+
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
60615
|
+
travelBack: Boolean(drag.areaBack),
|
|
60616
|
+
travelOn: Boolean(drag.areaOn)
|
|
60617
|
+
};
|
|
60618
|
+
},
|
|
59838
60619
|
onEnd: ({
|
|
59839
60620
|
axis,
|
|
59840
60621
|
sign,
|
|
@@ -63805,9 +64586,11 @@ const css$E = /* css */`
|
|
|
63805
64586
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
63806
64587
|
* the popup has finished closing, for content whose fresh state is its
|
|
63807
64588
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
63808
|
-
* changed while the popup was closed.
|
|
63809
|
-
*
|
|
63810
|
-
*
|
|
64589
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
64590
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
64591
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
64592
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
64593
|
+
* press that does not open the popup throws it away.
|
|
63811
64594
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
63812
64595
|
*/
|
|
63813
64596
|
const Dialog = props => {
|
|
@@ -65567,9 +66350,11 @@ const css$D = /* css */`
|
|
|
65567
66350
|
* the cost on the critical render. `"while-opened"` throws them away once
|
|
65568
66351
|
* the popup has finished closing, for content whose fresh state is its
|
|
65569
66352
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
65570
|
-
* changed while the popup was closed.
|
|
65571
|
-
*
|
|
65572
|
-
*
|
|
66353
|
+
* changed while the popup was closed. Intent on the anchor (pointer
|
|
66354
|
+
* entering it, focus landing in it) builds the content ahead of the click;
|
|
66355
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
66356
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
66357
|
+
* press that does not open the popup throws it away.
|
|
65573
66358
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
65574
66359
|
*/
|
|
65575
66360
|
const Popover = props => {
|
|
@@ -67242,7 +68027,10 @@ const PickerCustom = props => {
|
|
|
67242
68027
|
// a defaultValue holds nothing, so closing on it untouched IS the answer
|
|
67243
68028
|
// ("yes, 2h15") — the same rule Form applies to an untouched field (see
|
|
67244
68029
|
// isUIStateHeld). Read at open, before anything inside can change it.
|
|
67245
|
-
const
|
|
68030
|
+
const pickerController = getPickerInput(ref.current)?.__uiStateController__;
|
|
68031
|
+
const heldAtOpen = isUIStateHeld(pickerController);
|
|
68032
|
+
// What a cancel must not take back: see followOutsideSinceOpen.
|
|
68033
|
+
const statesAtOpen = pickerController ? readStatesAtOpen(pickerController) : null;
|
|
67246
68034
|
debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen, heldAtOpen ? `(held)` : `(a suggestion, not an answer yet)`);
|
|
67247
68035
|
onOpen?.(openEvent);
|
|
67248
68036
|
return {
|
|
@@ -67311,6 +68099,9 @@ const PickerCustom = props => {
|
|
|
67311
68099
|
});
|
|
67312
68100
|
chainEvent(rollbackEvent, closeEvent);
|
|
67313
68101
|
inputEl.__uiStateController__.setUIState(valueAtOpen, rollbackEvent);
|
|
68102
|
+
if (statesAtOpen) {
|
|
68103
|
+
followOutsideSinceOpen(inputEl.__uiStateController__, statesAtOpen);
|
|
68104
|
+
}
|
|
67314
68105
|
} else if (!heldAtOpen) {
|
|
67315
68106
|
// Confirmed a suggestion: nothing changed, so nothing has told the
|
|
67316
68107
|
// control's own bound signal / uiAction that this is now the
|
|
@@ -67402,6 +68193,11 @@ const PickerCustom = props => {
|
|
|
67402
68193
|
"onnavi_request_open": e => {
|
|
67403
68194
|
dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
|
|
67404
68195
|
},
|
|
68196
|
+
// And the press announcing that request (see announceOpeningPress in
|
|
68197
|
+
// commands.js), for the popup's content to be built on it.
|
|
68198
|
+
"onnavi_open_press": () => {
|
|
68199
|
+
dispatchCustomEvent(popupRef.current, "navi_open_press");
|
|
68200
|
+
},
|
|
67405
68201
|
"onnavi_request_close": e => {
|
|
67406
68202
|
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
67407
68203
|
if (!closing) {
|
|
@@ -67596,6 +68392,25 @@ const PickerCustom = props => {
|
|
|
67596
68392
|
// below), and the keyboard keeps its own ways in (the shortcuts above).
|
|
67597
68393
|
const openOnList = Array.isArray(openOn) ? openOn : [openOn];
|
|
67598
68394
|
const opensOnPress = openOnList.includes("press");
|
|
68395
|
+
// A press on the trigger of a picker a press opens is announced as it
|
|
68396
|
+
// starts, as a --navi-open button announces its own (see
|
|
68397
|
+
// announceOpeningPress in commands.js). Only a press that can open it: a
|
|
68398
|
+
// picker opened by a hold says nothing on a tap, nor does one refusing
|
|
68399
|
+
// to open (read-only with openWhileReadOnly={false}, busy). The
|
|
68400
|
+
// interactivity is asked alone, not the whole gate: a drag source
|
|
68401
|
+
// cancels the pointerdown, and the click that opens still comes.
|
|
68402
|
+
if (opensOnPress) {
|
|
68403
|
+
pickerProps.onPointerDown = pointerdownEvent => {
|
|
68404
|
+
props.onPointerDown?.(pointerdownEvent);
|
|
68405
|
+
const pickerEl = ref.current;
|
|
68406
|
+
if (pointerdownEvent.button !== 0 || openController.opened || isWithinPickerContent(pointerdownEvent.target) || !allowsInteraction(pickerEl, {
|
|
68407
|
+
intent: "read"
|
|
68408
|
+
})) {
|
|
68409
|
+
return;
|
|
68410
|
+
}
|
|
68411
|
+
dispatchCustomEvent(pickerEl, "navi_open_press");
|
|
68412
|
+
};
|
|
68413
|
+
}
|
|
67599
68414
|
let interactions = props.interactions;
|
|
67600
68415
|
if (!opensOnPress) {
|
|
67601
68416
|
interactions = {
|
|
@@ -80475,6 +81290,9 @@ const css$n = /* css */`.navi_split_button {
|
|
|
80475
81290
|
* defaultValue?: any,
|
|
80476
81291
|
* label?: import("ignore:preact").ComponentChildren,
|
|
80477
81292
|
* action?: (value: any, event: Event) => void | Promise<void>,
|
|
81293
|
+
* command?: string,
|
|
81294
|
+
* commandFor?: string,
|
|
81295
|
+
* prefetch?: boolean,
|
|
80478
81296
|
* onValueChange?: (value: any, event: Event) => void,
|
|
80479
81297
|
* chooseEffect?: "select" | "run",
|
|
80480
81298
|
* menuLabel?: string,
|
|
@@ -80528,6 +81346,12 @@ const css$n = /* css */`.navi_split_button {
|
|
|
80528
81346
|
* entry given. Pressed on the button it is the entry the button stands for;
|
|
80529
81347
|
* with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
|
|
80530
81348
|
* whole split button is busy until it settles.
|
|
81349
|
+
* @param {string} [command] What a press on the button does once its `action`
|
|
81350
|
+
* is done, as on a `<Button>` (with `commandFor`, and `command-value` when
|
|
81351
|
+
* what the command is about is not the entry the button stands for). A
|
|
81352
|
+
* split button whose button opens a popup says it here rather than from its
|
|
81353
|
+
* `action`: the press is then read as an opening before it runs, and asks
|
|
81354
|
+
* ahead for what the popup's opening reads (see `prefetch` on Button).
|
|
80531
81355
|
* @param {"select"|"run"} [chooseEffect="select"] What choosing an entry does.
|
|
80532
81356
|
* "select" hands the button that entry, to be run by a press on it — for
|
|
80533
81357
|
* something one does again and again, where the last choice is the likely
|
|
@@ -80569,6 +81393,10 @@ const SplitButton = props => {
|
|
|
80569
81393
|
defaultValue,
|
|
80570
81394
|
label,
|
|
80571
81395
|
action,
|
|
81396
|
+
command,
|
|
81397
|
+
commandFor,
|
|
81398
|
+
"command-value": commandValue,
|
|
81399
|
+
prefetch,
|
|
80572
81400
|
onValueChange,
|
|
80573
81401
|
chooseEffect = "select",
|
|
80574
81402
|
menuLabel = naviI18n("button.more_actions"),
|
|
@@ -80662,6 +81490,10 @@ const SplitButton = props => {
|
|
|
80662
81490
|
id: idResolved,
|
|
80663
81491
|
value: valueResolved,
|
|
80664
81492
|
action: actionResolved,
|
|
81493
|
+
command: command,
|
|
81494
|
+
commandFor: commandFor,
|
|
81495
|
+
"command-value": commandValue,
|
|
81496
|
+
prefetch: prefetch,
|
|
80665
81497
|
...halfProps,
|
|
80666
81498
|
children: label === undefined ? optionShown?.label : label
|
|
80667
81499
|
}), jsxs("div", {
|