@jsenv/navi 0.29.425 → 0.29.426
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 +372 -119
- package/dist/dev/jsenv_navi.js.map +15 -13
- package/dist/jsenv_navi.js +372 -119
- package/dist/jsenv_navi.js.map +15 -13
- package/docs/AI_INSTRUCTIONS.md +9 -5
- package/docs/control_object.md +76 -2
- package/docs/navigation.md +4 -1
- package/docs/popup_open.md +6 -3
- package/docs/scroll.md +53 -0
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchIntern
|
|
|
7
7
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
9
9
|
import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
10
|
-
import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback,
|
|
10
|
+
import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
|
|
11
11
|
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";
|
|
12
12
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
13
13
|
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
@@ -11240,6 +11240,179 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
11240
11240
|
return stateWithoutGeneratedIdKeys;
|
|
11241
11241
|
};
|
|
11242
11242
|
|
|
11243
|
+
const FRAME_TIMEOUT_MS = 100;
|
|
11244
|
+
|
|
11245
|
+
/**
|
|
11246
|
+
* Calls back once the browser has painted what is committed now.
|
|
11247
|
+
*
|
|
11248
|
+
* The callbacks of the next frame run before that frame paints, so the call is
|
|
11249
|
+
* made from a task queued from inside one — the same way preact schedules
|
|
11250
|
+
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
11251
|
+
* component's pending effects as soon as that component renders again, and a
|
|
11252
|
+
* re-render before the paint is exactly what a caller waiting for the paint
|
|
11253
|
+
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
11254
|
+
* late, through the timeout.
|
|
11255
|
+
*
|
|
11256
|
+
* @param {() => void} callback
|
|
11257
|
+
* @returns {() => void} cancel
|
|
11258
|
+
*/
|
|
11259
|
+
const afterPaint = (callback) => {
|
|
11260
|
+
let called = false;
|
|
11261
|
+
let timeoutId;
|
|
11262
|
+
let frameId;
|
|
11263
|
+
const onFrame = () => {
|
|
11264
|
+
if (called) {
|
|
11265
|
+
return;
|
|
11266
|
+
}
|
|
11267
|
+
called = true;
|
|
11268
|
+
clearTimeout(timeoutId);
|
|
11269
|
+
cancelAnimationFrame(frameId);
|
|
11270
|
+
timeoutId = setTimeout(callback, 0);
|
|
11271
|
+
};
|
|
11272
|
+
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
11273
|
+
frameId = requestAnimationFrame(onFrame);
|
|
11274
|
+
return () => {
|
|
11275
|
+
called = true;
|
|
11276
|
+
clearTimeout(timeoutId);
|
|
11277
|
+
cancelAnimationFrame(frameId);
|
|
11278
|
+
};
|
|
11279
|
+
};
|
|
11280
|
+
|
|
11281
|
+
/**
|
|
11282
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
11283
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
11284
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
11285
|
+
*
|
|
11286
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
11287
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
11288
|
+
* to do it, because what comes after is a browser event still in flight
|
|
11289
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
11290
|
+
*
|
|
11291
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
11292
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
11293
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
11294
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
11295
|
+
* batching that makes several state changes one commit.
|
|
11296
|
+
*/
|
|
11297
|
+
const flushSyncRendering = (fn) => {
|
|
11298
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
11299
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
11300
|
+
drainRenderQueue();
|
|
11301
|
+
};
|
|
11302
|
+
try {
|
|
11303
|
+
fn();
|
|
11304
|
+
} finally {
|
|
11305
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
11306
|
+
}
|
|
11307
|
+
};
|
|
11308
|
+
|
|
11309
|
+
/**
|
|
11310
|
+
* The content of a `<Box mount="after-paint">`: built right after the first
|
|
11311
|
+
* frame that shows the box, unless that frame would show it.
|
|
11312
|
+
*
|
|
11313
|
+
* The browser paints nothing until the render that builds a page has ended, and
|
|
11314
|
+
* what lies below the first screen weighs in that render as much as what is on
|
|
11315
|
+
* it. A box marked this way is left empty in that render, then built once the
|
|
11316
|
+
* frame is painted — the page-level twin of a list's render window (see
|
|
11317
|
+
* `renderBudget` in list.jsx), for a page made of sections.
|
|
11318
|
+
*
|
|
11319
|
+
* Which boxes lie below the screen is measured, not taken from the caller. The
|
|
11320
|
+
* caller cannot know: a taller phone, a profile with nothing in its first
|
|
11321
|
+
* section, and the box it marked is on screen. So each box looks where it
|
|
11322
|
+
* stands in the animation callbacks of its first frame — laid out, not yet
|
|
11323
|
+
* painted — and one the frame would show, or one above it, is built there:
|
|
11324
|
+
* content appearing on screen is a pop-in, and content growing ABOVE what is
|
|
11325
|
+
* read pushes it down. Marking too much costs a second render in that frame,
|
|
11326
|
+
* never a wrong picture.
|
|
11327
|
+
*
|
|
11328
|
+
* The document put back where it was read is the other frame that shows it.
|
|
11329
|
+
* That offset is written by navi (see scroll_restoration.js), and written into
|
|
11330
|
+
* a document still missing its bottom it is clamped: the page opens higher than
|
|
11331
|
+
* it was left, and the bottom arrives under a page that has already jumped. So
|
|
11332
|
+
* whoever writes an arrival's offset builds first the boxes that offset shows
|
|
11333
|
+
* (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
|
|
11334
|
+
* right after it (the offset, a route travel measuring its window) reads the
|
|
11335
|
+
* layout.
|
|
11336
|
+
*/
|
|
11337
|
+
|
|
11338
|
+
const deferredBoxes = new Set();
|
|
11339
|
+
const MountAfterPaint = ({
|
|
11340
|
+
boxRef,
|
|
11341
|
+
children
|
|
11342
|
+
}) => {
|
|
11343
|
+
const [built, setBuilt] = useState(false);
|
|
11344
|
+
useLayoutEffect(() => {
|
|
11345
|
+
let frame;
|
|
11346
|
+
let cancelAfterPaint;
|
|
11347
|
+
const stopWaiting = () => {
|
|
11348
|
+
deferredBoxes.delete(deferredBox);
|
|
11349
|
+
cancelAnimationFrame(frame);
|
|
11350
|
+
cancelAfterPaint();
|
|
11351
|
+
};
|
|
11352
|
+
// The first of the three that come — the offset of an arrival, the frame
|
|
11353
|
+
// finding the box on screen, the paint — builds it and cancels the others.
|
|
11354
|
+
const deferredBox = {
|
|
11355
|
+
getElement: () => boxRef.current,
|
|
11356
|
+
build: () => {
|
|
11357
|
+
stopWaiting();
|
|
11358
|
+
setBuilt(true);
|
|
11359
|
+
}
|
|
11360
|
+
};
|
|
11361
|
+
deferredBoxes.add(deferredBox);
|
|
11362
|
+
// Requested before afterPaint's own frame callback, so the box is looked at
|
|
11363
|
+
// before that one turns the paint into a build.
|
|
11364
|
+
frame = requestAnimationFrame(() => {
|
|
11365
|
+
const top = readTopInWindow(boxRef.current);
|
|
11366
|
+
if (top !== null && top < window.innerHeight) {
|
|
11367
|
+
deferredBox.build();
|
|
11368
|
+
}
|
|
11369
|
+
});
|
|
11370
|
+
cancelAfterPaint = afterPaint(deferredBox.build);
|
|
11371
|
+
return stopWaiting;
|
|
11372
|
+
}, []);
|
|
11373
|
+
return built ? children : null;
|
|
11374
|
+
};
|
|
11375
|
+
|
|
11376
|
+
/**
|
|
11377
|
+
* Builds, now, every box still waiting whose top is above `bottom` (in the
|
|
11378
|
+
* document's coordinates): what the document will show once scrolled so that
|
|
11379
|
+
* `bottom` is the bottom of the screen. Boxes built push those below them
|
|
11380
|
+
* further down, so the ones measured above `bottom` are all the ones that can
|
|
11381
|
+
* be shown there, and a few more at worst.
|
|
11382
|
+
*
|
|
11383
|
+
* Renders synchronously, so it must be called outside a Preact commit: a render
|
|
11384
|
+
* flushed from a layout effect runs inside the commit still walking the tree.
|
|
11385
|
+
*/
|
|
11386
|
+
const buildDeferredBoxesAbove = bottom => {
|
|
11387
|
+
if (deferredBoxes.size === 0) {
|
|
11388
|
+
return;
|
|
11389
|
+
}
|
|
11390
|
+
const toBuild = [];
|
|
11391
|
+
for (const deferredBox of deferredBoxes) {
|
|
11392
|
+
const top = readTopInWindow(deferredBox.getElement());
|
|
11393
|
+
if (top !== null && top + window.scrollY < bottom) {
|
|
11394
|
+
toBuild.push(deferredBox);
|
|
11395
|
+
}
|
|
11396
|
+
}
|
|
11397
|
+
if (toBuild.length === 0) {
|
|
11398
|
+
return;
|
|
11399
|
+
}
|
|
11400
|
+
flushSyncRendering(() => {
|
|
11401
|
+
for (const deferredBox of toBuild) {
|
|
11402
|
+
deferredBox.build();
|
|
11403
|
+
}
|
|
11404
|
+
});
|
|
11405
|
+
};
|
|
11406
|
+
|
|
11407
|
+
// null for a box nothing draws (a closed popup, a hidden tab): no frame shows
|
|
11408
|
+
// it, wherever the document is.
|
|
11409
|
+
const readTopInWindow = element => {
|
|
11410
|
+
if (!element || element.getClientRects().length === 0) {
|
|
11411
|
+
return null;
|
|
11412
|
+
}
|
|
11413
|
+
return element.getBoundingClientRect().top;
|
|
11414
|
+
};
|
|
11415
|
+
|
|
11243
11416
|
/**
|
|
11244
11417
|
* A container has put its page on screen — or as much of it as it can.
|
|
11245
11418
|
*
|
|
@@ -11295,7 +11468,9 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
|
11295
11468
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
11296
11469
|
* something still loading. Its content is not there at the moment it is put
|
|
11297
11470
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
11298
|
-
* page knows when it is whole.
|
|
11471
|
+
* page knows when it is whole. What navi itself leaves out of a first render
|
|
11472
|
+
* is the exception — a `<Box mount="after-paint">` — and is built before the
|
|
11473
|
+
* offset that shows it is written (see mount_after_paint.jsx).
|
|
11299
11474
|
*
|
|
11300
11475
|
* WHEN a page is arrived at is not decided here either. A document navigation
|
|
11301
11476
|
* lands where its kind says (see via_history.js), and one scrollport can be
|
|
@@ -11444,14 +11619,18 @@ const installScrollRestoration = () => {
|
|
|
11444
11619
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11445
11620
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11446
11621
|
// once there is a page under it.
|
|
11622
|
+
// Written once the commit that rendered it has ended: said from inside it,
|
|
11623
|
+
// and what the offset shows may still have to be built (see scrollTo).
|
|
11447
11624
|
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11448
11625
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11449
11626
|
const stopListening = observeRouteRender(() => {
|
|
11450
11627
|
stopListening();
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11628
|
+
queueMicrotask(() => {
|
|
11629
|
+
if (documentHoldCount) {
|
|
11630
|
+
return;
|
|
11631
|
+
}
|
|
11632
|
+
scrollTo(positionOnLoad);
|
|
11633
|
+
});
|
|
11455
11634
|
});
|
|
11456
11635
|
}
|
|
11457
11636
|
};
|
|
@@ -11558,14 +11737,25 @@ const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
|
11558
11737
|
// last read. Where this one was read, and its top when it never was — leaving
|
|
11559
11738
|
// the offset alone would seat the reader wherever the neighbour happened to
|
|
11560
11739
|
// be, so here "nothing recorded" and "stay" are not the same thing.
|
|
11740
|
+
//
|
|
11741
|
+
// Called outside a Preact commit only, like scrollTo: a row of tabs measures
|
|
11742
|
+
// its window right after, so what the top shows is built first here rather
|
|
11743
|
+
// than in the frame (see mount_after_paint.jsx).
|
|
11561
11744
|
const arriveAtScrollPosition = (url) => {
|
|
11562
11745
|
if (restoreScrollPosition(url)) {
|
|
11563
11746
|
return;
|
|
11564
11747
|
}
|
|
11748
|
+
buildDeferredBoxesAbove(window.innerHeight);
|
|
11565
11749
|
startAtTop(url);
|
|
11566
11750
|
};
|
|
11567
11751
|
|
|
11752
|
+
// What the offset shows is built first: written into a document still missing
|
|
11753
|
+
// a deferred box, the offset is clamped (see mount_after_paint.jsx). That build
|
|
11754
|
+
// is a render of its own, so this is never called from inside a commit — a
|
|
11755
|
+
// caller that learns the page is there from a layout effect (observeRouteRender)
|
|
11756
|
+
// writes once that commit has ended.
|
|
11568
11757
|
const scrollTo = ({ x, y }) => {
|
|
11758
|
+
buildDeferredBoxesAbove(y + window.innerHeight);
|
|
11569
11759
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
11570
11760
|
};
|
|
11571
11761
|
|
|
@@ -30928,6 +31118,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30928
31118
|
* footer?: boolean,
|
|
30929
31119
|
* body?: boolean,
|
|
30930
31120
|
* sticky?: boolean,
|
|
31121
|
+
* mount?: "always" | "after-paint",
|
|
30931
31122
|
* children?: import("ignore:preact").ComponentChildren,
|
|
30932
31123
|
* [key: string]: any,
|
|
30933
31124
|
* }>}
|
|
@@ -30976,6 +31167,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30976
31167
|
* (see docs/interactions.md). A plain box has no wiring of its own: it does
|
|
30977
31168
|
* nothing with `action` (that is a control's prop), so a click on it is
|
|
30978
31169
|
* declared here like any other interaction: `interactions={{ click: fn }}`.
|
|
31170
|
+
* @param {"always"|"after-paint"} [mount="always"] When `children` are built.
|
|
31171
|
+
* `"after-paint"` leaves the box empty in the render that first mounts it and
|
|
31172
|
+
* builds them right after the first frame that shows the page, then keeps
|
|
31173
|
+
* them: for the sections of a page that lie below its first screen. navi
|
|
31174
|
+
* measures where the box stands before that frame, and one the frame would
|
|
31175
|
+
* show (or where the document is put back, on a return) is built before the
|
|
31176
|
+
* paint all the same. Until built, what is inside is not in the document:
|
|
31177
|
+
* no id there is found, no named control there is there to hear its own
|
|
31178
|
+
* `signal` (see docs/control_object.md). Only for content nothing reaches
|
|
31179
|
+
* before the first frame (see docs/scroll.md).
|
|
30979
31180
|
*/
|
|
30980
31181
|
const Box = props => {
|
|
30981
31182
|
const {
|
|
@@ -30983,6 +31184,7 @@ const Box = props => {
|
|
|
30983
31184
|
children,
|
|
30984
31185
|
separator,
|
|
30985
31186
|
interactions,
|
|
31187
|
+
mount,
|
|
30986
31188
|
...computeProps
|
|
30987
31189
|
} = props;
|
|
30988
31190
|
const parentBoxFlow = useContext(BoxFlowContext);
|
|
@@ -31060,6 +31262,12 @@ const Box = props => {
|
|
|
31060
31262
|
children: innerChildren
|
|
31061
31263
|
});
|
|
31062
31264
|
}
|
|
31265
|
+
if (mount === "after-paint") {
|
|
31266
|
+
innerChildren = jsx(MountAfterPaint, {
|
|
31267
|
+
boxRef: finalRef,
|
|
31268
|
+
children: innerChildren
|
|
31269
|
+
});
|
|
31270
|
+
}
|
|
31063
31271
|
return jsx(TagName, {
|
|
31064
31272
|
ref: finalRef,
|
|
31065
31273
|
className: innerClassName,
|
|
@@ -34630,6 +34838,13 @@ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
|
|
|
34630
34838
|
const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
|
|
34631
34839
|
const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
|
|
34632
34840
|
const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
|
|
34841
|
+
const VIEWPORT_TOP_PROPERTY = "--navi-transition-viewport-top";
|
|
34842
|
+
const VIEWPORT_LEFT_PROPERTY = "--navi-transition-viewport-left";
|
|
34843
|
+
// Worn by the root once the group is held at the window by our numbers alone
|
|
34844
|
+
// (see placeTransitionWindow): the group rule of each movement is written on
|
|
34845
|
+
// it. Read by rules on pseudo-elements only, so wearing it restyles nothing
|
|
34846
|
+
// the document inherits.
|
|
34847
|
+
const WINDOW_PLACED_ATTRIBUTE = "data-navi-transition-window-placed";
|
|
34633
34848
|
const WINDOW_PROPERTIES = [
|
|
34634
34849
|
WINDOW_TOP_PROPERTY,
|
|
34635
34850
|
WINDOW_LEFT_PROPERTY,
|
|
@@ -34643,6 +34858,8 @@ const WINDOW_PROPERTIES = [
|
|
|
34643
34858
|
OLD_BAND_RIGHT_PROPERTY,
|
|
34644
34859
|
OLD_BAND_BOTTOM_PROPERTY,
|
|
34645
34860
|
OLD_BAND_LEFT_PROPERTY,
|
|
34861
|
+
VIEWPORT_TOP_PROPERTY,
|
|
34862
|
+
VIEWPORT_LEFT_PROPERTY,
|
|
34646
34863
|
];
|
|
34647
34864
|
|
|
34648
34865
|
// Whose numbers are currently published. The window belongs to the movement
|
|
@@ -34650,6 +34867,8 @@ const WINDOW_PROPERTIES = [
|
|
|
34650
34867
|
// after another has replaced it must not wipe numbers the new one is standing
|
|
34651
34868
|
// on.
|
|
34652
34869
|
let windowOwner = null;
|
|
34870
|
+
// The element the box is captured as, for as long as the window is held.
|
|
34871
|
+
let windowElement = null;
|
|
34653
34872
|
// Stops re-reading the arriving state's corner, when a movement is on.
|
|
34654
34873
|
let unfollowArrivingState = null;
|
|
34655
34874
|
|
|
@@ -34705,6 +34924,10 @@ const holdTransitionWindow = (
|
|
|
34705
34924
|
const right =
|
|
34706
34925
|
rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
|
|
34707
34926
|
windowOwner = owner;
|
|
34927
|
+
windowElement = element;
|
|
34928
|
+
// Pictures of a movement this one took over from are placed by the browser
|
|
34929
|
+
// until this one's own pictures exist.
|
|
34930
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
34708
34931
|
setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
|
|
34709
34932
|
setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
|
|
34710
34933
|
setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
|
|
@@ -34729,6 +34952,33 @@ const holdTransitionWindow = (
|
|
|
34729
34952
|
});
|
|
34730
34953
|
};
|
|
34731
34954
|
|
|
34955
|
+
/**
|
|
34956
|
+
* Called once the pictures exist — the view transition's `ready` — with the
|
|
34957
|
+
* name the box was captured under. Where the viewport's corner stands in the
|
|
34958
|
+
* space the pictures are drawn in is the browser's placement of the group
|
|
34959
|
+
* minus our reading of the same element, both taken in this one instant; the
|
|
34960
|
+
* group is held at the window with it from then on (see the top of the file).
|
|
34961
|
+
* A browser that does not hand the group's placement back leaves the group
|
|
34962
|
+
* where it places it, moved to the window by the arriving corner.
|
|
34963
|
+
*/
|
|
34964
|
+
const placeTransitionWindow = (owner, groupName) => {
|
|
34965
|
+
if (owner !== windowOwner) {
|
|
34966
|
+
return;
|
|
34967
|
+
}
|
|
34968
|
+
const { transform } = getComputedStyle(
|
|
34969
|
+
document.documentElement,
|
|
34970
|
+
`::view-transition-group(${groupName})`,
|
|
34971
|
+
);
|
|
34972
|
+
if (!transform || transform === "none") {
|
|
34973
|
+
return;
|
|
34974
|
+
}
|
|
34975
|
+
const placement = new DOMMatrixReadOnly(transform);
|
|
34976
|
+
const rect = windowElement.getBoundingClientRect();
|
|
34977
|
+
setTransitionValue(VIEWPORT_TOP_PROPERTY, `${placement.m42 - rect.top}px`);
|
|
34978
|
+
setTransitionValue(VIEWPORT_LEFT_PROPERTY, `${placement.m41 - rect.left}px`);
|
|
34979
|
+
document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
|
|
34980
|
+
};
|
|
34981
|
+
|
|
34732
34982
|
// The live layout takes the box back. A discontinuity by construction — the
|
|
34733
34983
|
// window stands at the held height, the box is at its own — and an invisible
|
|
34734
34984
|
// one: the page arriving is fully in place, and the strip below it that the
|
|
@@ -34738,20 +34988,22 @@ const releaseTransitionWindow = (owner) => {
|
|
|
34738
34988
|
return;
|
|
34739
34989
|
}
|
|
34740
34990
|
windowOwner = null;
|
|
34991
|
+
windowElement = null;
|
|
34741
34992
|
if (unfollowArrivingState) {
|
|
34742
34993
|
unfollowArrivingState();
|
|
34743
34994
|
}
|
|
34995
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
34744
34996
|
for (const property of WINDOW_PROPERTIES) {
|
|
34745
34997
|
removeTransitionValue(property);
|
|
34746
34998
|
}
|
|
34747
34999
|
};
|
|
34748
35000
|
|
|
34749
|
-
// The arriving state's corner, re-read every frame — as often as
|
|
34750
|
-
//
|
|
34751
|
-
//
|
|
34752
|
-
//
|
|
34753
|
-
//
|
|
34754
|
-
//
|
|
35001
|
+
// The arriving state's corner, re-read every frame — as often as anything can
|
|
35002
|
+
// move the page arriving (see the top of the file) — and with it the offset
|
|
35003
|
+
// outside the box, which carries the window and the corner of the state being
|
|
35004
|
+
// left along. Requested, never awaited: the first call runs inside the update
|
|
35005
|
+
// callback, where a frame cannot come. Written only when it moved: every write
|
|
35006
|
+
// restyles the pictures.
|
|
34755
35007
|
const followArrivingState = (element, rectAtHold, heldAt) => {
|
|
34756
35008
|
if (unfollowArrivingState) {
|
|
34757
35009
|
unfollowArrivingState();
|
|
@@ -35256,6 +35508,13 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
|
|
|
35256
35508
|
animation-name: none;
|
|
35257
35509
|
}
|
|
35258
35510
|
|
|
35511
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-transition) {
|
|
35512
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
35513
|
+
left: calc(var(--navi-transition-window-left) +
|
|
35514
|
+
var(--navi-transition-viewport-left));
|
|
35515
|
+
transform: none;
|
|
35516
|
+
}
|
|
35517
|
+
|
|
35259
35518
|
&::view-transition-old(navi-route-transition) {
|
|
35260
35519
|
top: calc(var(--navi-transition-window-old-top) - var(--navi-transition-window-top));
|
|
35261
35520
|
left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
|
|
@@ -36303,6 +36562,8 @@ const beginTransition = ({
|
|
|
36303
36562
|
// the only place the silent misconfigurations show. They are all about the
|
|
36304
36563
|
// same thing — a movement playing on pictures that are not the pages.
|
|
36305
36564
|
const viewTransitionReady = () => {
|
|
36565
|
+
// Read before anything below writes to the document.
|
|
36566
|
+
placeTransitionWindow(transition, AREA_NAME);
|
|
36306
36567
|
startTransitionFurniture(transition, {
|
|
36307
36568
|
duration: transition.duration
|
|
36308
36569
|
});
|
|
@@ -36814,6 +37075,13 @@ const css$11 = /* css */`.navi_route_travel {
|
|
|
36814
37075
|
animation-duration: var(--navi-route-travel-duration, .3s);
|
|
36815
37076
|
animation-name: none;
|
|
36816
37077
|
}
|
|
37078
|
+
|
|
37079
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-travel) {
|
|
37080
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
37081
|
+
left: calc(var(--navi-transition-window-left) +
|
|
37082
|
+
var(--navi-transition-viewport-left));
|
|
37083
|
+
transform: none;
|
|
37084
|
+
}
|
|
36817
37085
|
}
|
|
36818
37086
|
|
|
36819
37087
|
:root[data-navi-route-travel-held] {
|
|
@@ -37192,6 +37460,10 @@ const RouteTravel = ({
|
|
|
37192
37460
|
});
|
|
37193
37461
|
});
|
|
37194
37462
|
travel.viewTransition = viewTransition;
|
|
37463
|
+
// Subscribed first, so it reads before the scrub below writes anything.
|
|
37464
|
+
viewTransition.ready.then(() => {
|
|
37465
|
+
placeTransitionWindow(travel, TRAVEL_NAME);
|
|
37466
|
+
}, ignoreSkipped);
|
|
37195
37467
|
if (scrub) {
|
|
37196
37468
|
// Said only now: the release has to have something to let go of, and the
|
|
37197
37469
|
// transition did not exist a line above.
|
|
@@ -37391,16 +37663,20 @@ const RouteTravel = ({
|
|
|
37391
37663
|
// what makes the document tall enough to hold that offset again —
|
|
37392
37664
|
// and the recording stays deaf until then, over the clamp the swap
|
|
37393
37665
|
// back makes on the way. Its own deafness rather than the travel's:
|
|
37394
|
-
// the travel ends here, and this outlives it by a render.
|
|
37666
|
+
// the travel ends here, and this outlives it by a render. Written
|
|
37667
|
+
// once the commit that says so has ended, as arriveAtScrollPosition
|
|
37668
|
+
// asks.
|
|
37395
37669
|
const resumeScrollRecording = suspendScrollRecording();
|
|
37396
37670
|
const stopWatchingRender = observeRouteRender(() => {
|
|
37397
37671
|
stopWatchingRender();
|
|
37398
|
-
|
|
37399
|
-
|
|
37400
|
-
|
|
37401
|
-
|
|
37402
|
-
|
|
37403
|
-
|
|
37672
|
+
queueMicrotask(() => {
|
|
37673
|
+
const {
|
|
37674
|
+
route,
|
|
37675
|
+
params
|
|
37676
|
+
} = travel.fromPage;
|
|
37677
|
+
arriveAtScrollPosition(route.buildUrl(params));
|
|
37678
|
+
resumeScrollRecording();
|
|
37679
|
+
});
|
|
37404
37680
|
});
|
|
37405
37681
|
}
|
|
37406
37682
|
releaseRendering();
|
|
@@ -40184,34 +40460,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
40184
40460
|
};
|
|
40185
40461
|
});
|
|
40186
40462
|
|
|
40187
|
-
/**
|
|
40188
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
40189
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
40190
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
40191
|
-
*
|
|
40192
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
40193
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
40194
|
-
* to do it, because what comes after is a browser event still in flight
|
|
40195
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
40196
|
-
*
|
|
40197
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
40198
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
40199
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
40200
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
40201
|
-
* batching that makes several state changes one commit.
|
|
40202
|
-
*/
|
|
40203
|
-
const flushSyncRendering = (fn) => {
|
|
40204
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
40205
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
40206
|
-
drainRenderQueue();
|
|
40207
|
-
};
|
|
40208
|
-
try {
|
|
40209
|
-
fn();
|
|
40210
|
-
} finally {
|
|
40211
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
40212
|
-
}
|
|
40213
|
-
};
|
|
40214
|
-
|
|
40215
40463
|
/**
|
|
40216
40464
|
* When a popup builds what it holds, and when it throws it away.
|
|
40217
40465
|
*
|
|
@@ -42534,6 +42782,65 @@ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
|
42534
42782
|
}
|
|
42535
42783
|
return kept;
|
|
42536
42784
|
};
|
|
42785
|
+
// The same claim, for an object: a key whose control is not there cannot be
|
|
42786
|
+
// contradicted by it — a slide not built yet (SlideContainer mount="near"), a
|
|
42787
|
+
// row scrolled out, a page parked. A key of `kept` stays until a control of
|
|
42788
|
+
// that name, there, says otherwise; a value put ON the group replaces the
|
|
42789
|
+
// whole object, which is how a key is dropped on purpose.
|
|
42790
|
+
const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
|
|
42791
|
+
const groupValues =
|
|
42792
|
+
kept !== null && typeof kept === "object" && !Array.isArray(kept)
|
|
42793
|
+
? { ...kept }
|
|
42794
|
+
: {};
|
|
42795
|
+
return Object.assign(
|
|
42796
|
+
groupValues,
|
|
42797
|
+
readNamedChildUIStates(children, { warnNameless: true }),
|
|
42798
|
+
);
|
|
42799
|
+
};
|
|
42800
|
+
// What the named controls among `children` say, one key each. A nameless
|
|
42801
|
+
// GROUP is a grouping, not a value: it exists to hold its children together
|
|
42802
|
+
// (a WheelGroup sharing navigation, a fieldset-ish cluster) without claiming a
|
|
42803
|
+
// key of its own, so what its controls say is written as if they were here.
|
|
42804
|
+
// Naming it is what turns the same group into one key — see ControlGroup's
|
|
42805
|
+
// `name`. Only what its CONTROLS say: such a grouping is handed the whole
|
|
42806
|
+
// object around it (see distributeChildUIState below) and keeps what they do
|
|
42807
|
+
// not answer for, so merged whole it would carry its siblings' keys back up,
|
|
42808
|
+
// stale.
|
|
42809
|
+
const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
42810
|
+
const values = {};
|
|
42811
|
+
for (const child of children) {
|
|
42812
|
+
const { name, emptyUIState } = child;
|
|
42813
|
+
// A control holding nothing writes its own empty, not a hole: the key is in
|
|
42814
|
+
// the object either way, and what is read from it keeps the shape the
|
|
42815
|
+
// reader was promised (see resolveEmptyUIState).
|
|
42816
|
+
const uiState =
|
|
42817
|
+
child.uiState === undefined && emptyUIState !== undefined
|
|
42818
|
+
? emptyUIState
|
|
42819
|
+
: child.uiState;
|
|
42820
|
+
if (!name) {
|
|
42821
|
+
if (isNamelessGrouping(child, uiState)) {
|
|
42822
|
+
Object.assign(
|
|
42823
|
+
values,
|
|
42824
|
+
child.keepsUnansweredKeys
|
|
42825
|
+
? readNamedChildUIStates(child.getChildControllers(), {
|
|
42826
|
+
warnNameless: false,
|
|
42827
|
+
})
|
|
42828
|
+
: uiState,
|
|
42829
|
+
);
|
|
42830
|
+
continue;
|
|
42831
|
+
}
|
|
42832
|
+
if (warnNameless) {
|
|
42833
|
+
console.warn(
|
|
42834
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
42835
|
+
child,
|
|
42836
|
+
);
|
|
42837
|
+
}
|
|
42838
|
+
continue;
|
|
42839
|
+
}
|
|
42840
|
+
values[name] = uiState;
|
|
42841
|
+
}
|
|
42842
|
+
return values;
|
|
42843
|
+
};
|
|
42537
42844
|
|
|
42538
42845
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
42539
42846
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
@@ -42603,37 +42910,7 @@ const GROUP_DEFAULTS = {
|
|
|
42603
42910
|
childControlFilter: (child) => {
|
|
42604
42911
|
return child.controlType !== "button" && child.controlType !== "link";
|
|
42605
42912
|
},
|
|
42606
|
-
aggregateChildStates:
|
|
42607
|
-
const groupValues = {};
|
|
42608
|
-
for (const child of children) {
|
|
42609
|
-
const { name, emptyUIState } = child;
|
|
42610
|
-
// A control holding nothing writes its own empty, not a hole: the key
|
|
42611
|
-
// is in the object either way, and what is read from it keeps the shape
|
|
42612
|
-
// the reader was promised (see resolveEmptyUIState).
|
|
42613
|
-
const uiState =
|
|
42614
|
-
child.uiState === undefined && emptyUIState !== undefined
|
|
42615
|
-
? emptyUIState
|
|
42616
|
-
: child.uiState;
|
|
42617
|
-
if (!name) {
|
|
42618
|
-
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
42619
|
-
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
42620
|
-
// cluster) without claiming a key of its own, so what it holds is
|
|
42621
|
-
// merged in as if its children had been written here. Naming it is
|
|
42622
|
-
// what turns the same group into one key — see ControlGroup's `name`.
|
|
42623
|
-
if (isNamelessGrouping(child, uiState)) {
|
|
42624
|
-
Object.assign(groupValues, uiState);
|
|
42625
|
-
continue;
|
|
42626
|
-
}
|
|
42627
|
-
console.warn(
|
|
42628
|
-
"A group child is missing a name property, its state won't be included in the group state",
|
|
42629
|
-
child,
|
|
42630
|
-
);
|
|
42631
|
-
continue;
|
|
42632
|
-
}
|
|
42633
|
-
groupValues[name] = uiState;
|
|
42634
|
-
}
|
|
42635
|
-
return groupValues;
|
|
42636
|
-
},
|
|
42913
|
+
aggregateChildStates: keptAndNamedChildUIStates,
|
|
42637
42914
|
distributeChildUIState: (newUIState, child) => {
|
|
42638
42915
|
const childName = child.name;
|
|
42639
42916
|
if (
|
|
@@ -42657,7 +42934,8 @@ const GROUP_DEFAULTS = {
|
|
|
42657
42934
|
return childUIState;
|
|
42658
42935
|
}
|
|
42659
42936
|
// Merged in on the way up (see above), so on the way down it takes the
|
|
42660
|
-
// whole object
|
|
42937
|
+
// whole object: its controls pick out their own keys, and it keeps the
|
|
42938
|
+
// rest for those of its controls that are not there yet.
|
|
42661
42939
|
// Only for a child with no name of its own: the same condition the
|
|
42662
42940
|
// aggregate applies before merging. A NAMED child holding an object (a
|
|
42663
42941
|
// `type="object"` picker, say) answers for one key, and the object it is
|
|
@@ -43145,6 +43423,11 @@ const useUIGroupStateController = (
|
|
|
43145
43423
|
// a picker filling its popup, a value prop) rather than worked out from
|
|
43146
43424
|
// its children. What it protects is read in onChange.
|
|
43147
43425
|
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
43426
|
+
// Its value carries keys none of its controls answers for (see
|
|
43427
|
+
// keptAndNamedChildUIStates): a group merging it as a nameless
|
|
43428
|
+
// grouping reads its controls instead of its value.
|
|
43429
|
+
keepsUnansweredKeys:
|
|
43430
|
+
resolvedAggregateChildStates === keptAndNamedChildUIStates,
|
|
43148
43431
|
uiStateSignal,
|
|
43149
43432
|
wantRequesterButtonState,
|
|
43150
43433
|
ref,
|
|
@@ -53746,7 +54029,8 @@ const useExpandableContext = partName => {
|
|
|
53746
54029
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
53747
54030
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
53748
54031
|
* built by a press on it only, and thrown away if that press does not
|
|
53749
|
-
* expand it.
|
|
54032
|
+
* expand it. Named controls in content not built are kept by the group
|
|
54033
|
+
* around them (docs/control_object.md, "A control that is not there").
|
|
53750
54034
|
*/
|
|
53751
54035
|
const Expandable = props => {
|
|
53752
54036
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -60837,12 +61121,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
60837
61121
|
* would bring alongside (neighbours on the map, along the axes
|
|
60838
61122
|
* `travelByDrag` opens, the other end of a `loop` included) — and kept from
|
|
60839
61123
|
* then on. For slides that are costly to build (a grid of a few hundred
|
|
60840
|
-
*
|
|
61124
|
+
* items each): opening builds the slide shown and its neighbours rather
|
|
60841
61125
|
* than all of them, and a jump to a far slide builds that one and none of
|
|
60842
61126
|
* those in between, which a travel never shows. The box is then as large as
|
|
60843
61127
|
* the largest slide BUILT so far (see `sizing`), so it grows as a larger one
|
|
60844
61128
|
* is reached: give it a size of its own when the slides differ.
|
|
60845
61129
|
* `<Slide mount>` says it for one slide — a heavy one among light ones.
|
|
61130
|
+
* A slide not built has no controls, and a group around the container keeps
|
|
61131
|
+
* their keys for them: what it was handed reaches them once built. What is
|
|
61132
|
+
* shared between slides is written through that group (its `signal`, a
|
|
61133
|
+
* `--navi-update` aimed at it), never into one control's own `signal`, which
|
|
61134
|
+
* nobody reads while that control is not built; and nothing validates a
|
|
61135
|
+
* field before its slide is built. See docs/control_object.md, "A control
|
|
61136
|
+
* that is not there".
|
|
60846
61137
|
*/
|
|
60847
61138
|
const SlideContainer = ({
|
|
60848
61139
|
layout = "row",
|
|
@@ -70926,44 +71217,6 @@ document.addEventListener(
|
|
|
70926
71217
|
{ capture: true, passive: true },
|
|
70927
71218
|
);
|
|
70928
71219
|
|
|
70929
|
-
const FRAME_TIMEOUT_MS = 100;
|
|
70930
|
-
|
|
70931
|
-
/**
|
|
70932
|
-
* Calls back once the browser has painted what is committed now.
|
|
70933
|
-
*
|
|
70934
|
-
* The callbacks of the next frame run before that frame paints, so the call is
|
|
70935
|
-
* made from a task queued from inside one — the same way preact schedules
|
|
70936
|
-
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
70937
|
-
* component's pending effects as soon as that component renders again, and a
|
|
70938
|
-
* re-render before the paint is exactly what a caller waiting for the paint
|
|
70939
|
-
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
70940
|
-
* late, through the timeout.
|
|
70941
|
-
*
|
|
70942
|
-
* @param {() => void} callback
|
|
70943
|
-
* @returns {() => void} cancel
|
|
70944
|
-
*/
|
|
70945
|
-
const afterPaint = (callback) => {
|
|
70946
|
-
let called = false;
|
|
70947
|
-
let timeoutId;
|
|
70948
|
-
let frameId;
|
|
70949
|
-
const onFrame = () => {
|
|
70950
|
-
if (called) {
|
|
70951
|
-
return;
|
|
70952
|
-
}
|
|
70953
|
-
called = true;
|
|
70954
|
-
clearTimeout(timeoutId);
|
|
70955
|
-
cancelAnimationFrame(frameId);
|
|
70956
|
-
timeoutId = setTimeout(callback, 0);
|
|
70957
|
-
};
|
|
70958
|
-
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
70959
|
-
frameId = requestAnimationFrame(onFrame);
|
|
70960
|
-
return () => {
|
|
70961
|
-
called = true;
|
|
70962
|
-
clearTimeout(timeoutId);
|
|
70963
|
-
cancelAnimationFrame(frameId);
|
|
70964
|
-
};
|
|
70965
|
-
};
|
|
70966
|
-
|
|
70967
71220
|
const LoadingDotsSvg = () => {
|
|
70968
71221
|
return jsxs("svg", {
|
|
70969
71222
|
viewBox: "0 0 200 200",
|