@jsenv/navi 0.29.433 → 0.29.435
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 +224 -59
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +164 -47
- package/dist/jsenv_navi.js.map +8 -8
- package/docs/AI_INSTRUCTIONS.md +4 -3
- package/docs/route_transitions.md +4 -0
- package/docs/scroll.md +38 -5
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -11086,9 +11086,16 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
|
11086
11086
|
* (recallScrollerPosition). What a push means for them is what it means for
|
|
11087
11087
|
* the document — an arrival opens at the top (see startAtTop): the page
|
|
11088
11088
|
* arrived at has its named positions dropped before its lists render, so a
|
|
11089
|
-
* list recalls only on the way back. A scroller that
|
|
11090
|
-
* stays
|
|
11091
|
-
*
|
|
11089
|
+
* list recalls only on the way back. A scroller that outlives a navigation —
|
|
11090
|
+
* its page stays under a new address, or it lives outside the page that
|
|
11091
|
+
* changed — is shown under the new address too, and is carried to it (see
|
|
11092
|
+
* carryScrollersOnceRendered). A scroller that goes while its page stays — a
|
|
11093
|
+
* popup closing over the same address — has nothing to come back to, and says
|
|
11094
|
+
* so as it leaves (forgetScrollerUnlessPageLeft).
|
|
11095
|
+
*
|
|
11096
|
+
* Every position is kept under the address the routing shows, which can be
|
|
11097
|
+
* ahead of the browser's: a replace waiting for its state to settle has moved
|
|
11098
|
+
* the page and not yet `window.location` (see readShownUrl).
|
|
11092
11099
|
*
|
|
11093
11100
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
11094
11101
|
* something still loading. Its content is not there at the moment it is put
|
|
@@ -11117,9 +11124,20 @@ let documentOffset = null;
|
|
|
11117
11124
|
// url -> (scroller name -> position). The position is whatever the scroller
|
|
11118
11125
|
// handed in: what it can put itself back on, in its own terms.
|
|
11119
11126
|
const scrollerPositionsByUrl = new Map();
|
|
11120
|
-
//
|
|
11121
|
-
// a page that stays from one
|
|
11122
|
-
|
|
11127
|
+
// What each named scroller still mounted last said: its position and the url
|
|
11128
|
+
// it said it under. What tells a scroller leaving a page that stays from one
|
|
11129
|
+
// leaving with its page, and what one outliving a navigation is carried with.
|
|
11130
|
+
const saidByScrollerName = new Map();
|
|
11131
|
+
|
|
11132
|
+
// The address the page is shown under, as the routing knows it (see
|
|
11133
|
+
// installScrollRestoration): `window.location` lags behind a replace waiting
|
|
11134
|
+
// for its state to settle (replaceAddressWhenSettled in either integration),
|
|
11135
|
+
// and a scroll keyed by it meanwhile is written for the address being left.
|
|
11136
|
+
// Resolved, as an address handed to navTo can be kept as it was given.
|
|
11137
|
+
let readRoutingAddress = () => window.location.href;
|
|
11138
|
+
const readShownUrl = () => {
|
|
11139
|
+
return new URL(readRoutingAddress(), window.location.href).href;
|
|
11140
|
+
};
|
|
11123
11141
|
|
|
11124
11142
|
// Read once, the first time anyone needs the positions: the document's
|
|
11125
11143
|
// restoration is installed by the routing, and a list remembering itself may
|
|
@@ -11220,11 +11238,12 @@ const holdDocumentScroll = () => {
|
|
|
11220
11238
|
};
|
|
11221
11239
|
|
|
11222
11240
|
let installed = false;
|
|
11223
|
-
const installScrollRestoration = () => {
|
|
11241
|
+
const installScrollRestoration = ({ readAddress }) => {
|
|
11224
11242
|
if (installed) {
|
|
11225
11243
|
return;
|
|
11226
11244
|
}
|
|
11227
11245
|
installed = true;
|
|
11246
|
+
readRoutingAddress = readAddress;
|
|
11228
11247
|
if (!("scrollRestoration" in window.history)) {
|
|
11229
11248
|
return;
|
|
11230
11249
|
}
|
|
@@ -11241,14 +11260,16 @@ const installScrollRestoration = () => {
|
|
|
11241
11260
|
if (suspendCount) {
|
|
11242
11261
|
return;
|
|
11243
11262
|
}
|
|
11244
|
-
positionByUrl.set(
|
|
11263
|
+
positionByUrl.set(readShownUrl(), documentOffset);
|
|
11245
11264
|
},
|
|
11246
11265
|
{ passive: true },
|
|
11247
11266
|
);
|
|
11248
|
-
const
|
|
11267
|
+
const urlOnLoad = readShownUrl();
|
|
11268
|
+
const positionOnLoad = positionByUrl.get(urlOnLoad);
|
|
11249
11269
|
if (!positionOnLoad) {
|
|
11250
|
-
// The first page, where the browser landed it
|
|
11251
|
-
stayInPlace
|
|
11270
|
+
// The first page, where the browser landed it: no scroll event says so
|
|
11271
|
+
// (see stayInPlace).
|
|
11272
|
+
positionByUrl.set(urlOnLoad, documentOffset);
|
|
11252
11273
|
return;
|
|
11253
11274
|
}
|
|
11254
11275
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
@@ -11301,7 +11322,9 @@ const restoreScrollPosition = (url) => {
|
|
|
11301
11322
|
// clamp is a scroll recorded under the url already returned to.
|
|
11302
11323
|
const restoreScrollPositionOnReturn = (url) => {
|
|
11303
11324
|
const arrival = ++arrivalCount;
|
|
11304
|
-
const
|
|
11325
|
+
const href = new URL(url, window.location.href).href;
|
|
11326
|
+
carryScrollersOnceRendered(href);
|
|
11327
|
+
const position = positionByUrl.get(href);
|
|
11305
11328
|
if (!position) {
|
|
11306
11329
|
return;
|
|
11307
11330
|
}
|
|
@@ -11343,11 +11366,13 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
11343
11366
|
return;
|
|
11344
11367
|
}
|
|
11345
11368
|
arrivalCount++;
|
|
11369
|
+
const href = new URL(url, window.location.href).href;
|
|
11370
|
+
carryScrollersOnceRendered(href);
|
|
11346
11371
|
// Written down as well as done: a document already at its top fires no
|
|
11347
11372
|
// scroll event, and a page left without ever being scrolled would have no
|
|
11348
11373
|
// position to come back to — the return would keep the offset of the page
|
|
11349
11374
|
// being left.
|
|
11350
|
-
positionByUrl.set(
|
|
11375
|
+
positionByUrl.set(href, { x: 0, y: 0 });
|
|
11351
11376
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
11352
11377
|
};
|
|
11353
11378
|
const isArrival = (url, { from }) => {
|
|
@@ -11361,21 +11386,51 @@ const isArrival = (url, { from }) => {
|
|
|
11361
11386
|
return true;
|
|
11362
11387
|
};
|
|
11363
11388
|
|
|
11364
|
-
// A url the
|
|
11365
|
-
// keeps the page (see startAtTop)
|
|
11366
|
-
//
|
|
11367
|
-
// nothing and keep the offset of the page being left.
|
|
11368
|
-
// events last told, not read again: a read here would lay the page out in the
|
|
11369
|
-
// middle of a routing, once per address a settling param writes.
|
|
11389
|
+
// A url the page is shown under without changing: a replace, a push that
|
|
11390
|
+
// keeps the page (see startAtTop). Nothing scrolls, so nothing says where the
|
|
11391
|
+
// document stands under it — written down here, or a return to it would find
|
|
11392
|
+
// nothing and keep the offset of the page being left.
|
|
11370
11393
|
//
|
|
11371
|
-
//
|
|
11372
|
-
//
|
|
11373
|
-
//
|
|
11394
|
+
// The offset as its scroll events last told it, not read again: a read here
|
|
11395
|
+
// would lay the page out in the middle of a routing, once per address a
|
|
11396
|
+
// settling param writes. Deaf with the recording: a row of tabs replaces the
|
|
11397
|
+
// url as it travels, and the offset belongs to nobody until the row says where
|
|
11398
|
+
// its tab lands (see arriveAtScrollPosition).
|
|
11374
11399
|
const stayInPlace = (url) => {
|
|
11375
|
-
|
|
11376
|
-
|
|
11400
|
+
const href = new URL(url, window.location.href).href;
|
|
11401
|
+
carryScrollersOnceRendered(href);
|
|
11402
|
+
if (documentOffset !== null && !suspendCount) {
|
|
11403
|
+
positionByUrl.set(href, documentOffset);
|
|
11377
11404
|
}
|
|
11378
|
-
|
|
11405
|
+
};
|
|
11406
|
+
|
|
11407
|
+
// A scroller that outlives a navigation — its page stays under the new url, or
|
|
11408
|
+
// it lives outside the page that changed (a sidebar list kept by every page) —
|
|
11409
|
+
// is shown under that url as well, and says nothing there unless it moves: a
|
|
11410
|
+
// return to it would find no position and open the list at its top. So it is
|
|
11411
|
+
// carried, with the position it last said.
|
|
11412
|
+
//
|
|
11413
|
+
// Told apart once the navigation has rendered: until then a list about to be
|
|
11414
|
+
// swapped out is as mounted as one that stays, and carried, its position is
|
|
11415
|
+
// what a list of the same name arriving would read. By then one that left has
|
|
11416
|
+
// said so (forgetScrollerUnlessPageLeft), and one that arrived has read its
|
|
11417
|
+
// own and speaks under this url. One still mounted on its way out (a page kept
|
|
11418
|
+
// hidden while its route transition plays) is carried, and forgets the copy as
|
|
11419
|
+
// it unmounts under this url.
|
|
11420
|
+
const carryScrollersOnceRendered = (url) => {
|
|
11421
|
+
whenPageRendered(() => {
|
|
11422
|
+
if (readShownUrl() !== url) {
|
|
11423
|
+
// Another navigation came first: what is mounted is shown under its url,
|
|
11424
|
+
// and its own carry says so.
|
|
11425
|
+
return;
|
|
11426
|
+
}
|
|
11427
|
+
for (const [name, said] of saidByScrollerName) {
|
|
11428
|
+
if (said.url === url) {
|
|
11429
|
+
continue;
|
|
11430
|
+
}
|
|
11431
|
+
writeScrollerPosition(url, name, said.position);
|
|
11432
|
+
}
|
|
11433
|
+
});
|
|
11379
11434
|
};
|
|
11380
11435
|
|
|
11381
11436
|
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
@@ -11418,19 +11473,21 @@ const scrollTo = ({ x, y }) => {
|
|
|
11418
11473
|
|
|
11419
11474
|
const rememberScrollerPosition = (name, position) => {
|
|
11420
11475
|
loadStore();
|
|
11421
|
-
|
|
11476
|
+
writeScrollerPosition(readShownUrl(), name, position);
|
|
11477
|
+
};
|
|
11478
|
+
const writeScrollerPosition = (url, name, position) => {
|
|
11422
11479
|
let positionByName = scrollerPositionsByUrl.get(url);
|
|
11423
11480
|
if (!positionByName) {
|
|
11424
11481
|
positionByName = new Map();
|
|
11425
11482
|
scrollerPositionsByUrl.set(url, positionByName);
|
|
11426
11483
|
}
|
|
11427
11484
|
positionByName.set(name, position);
|
|
11428
|
-
|
|
11485
|
+
saidByScrollerName.set(name, { url, position });
|
|
11429
11486
|
};
|
|
11430
11487
|
|
|
11431
11488
|
const recallScrollerPosition = (name) => {
|
|
11432
11489
|
loadStore();
|
|
11433
|
-
const positionByName = scrollerPositionsByUrl.get(
|
|
11490
|
+
const positionByName = scrollerPositionsByUrl.get(readShownUrl());
|
|
11434
11491
|
if (!positionByName) {
|
|
11435
11492
|
return undefined;
|
|
11436
11493
|
}
|
|
@@ -11440,17 +11497,18 @@ const recallScrollerPosition = (name) => {
|
|
|
11440
11497
|
// Said by a scroller as it unmounts. Its page is being left when the url is
|
|
11441
11498
|
// already another one — the history is written before the page it names is
|
|
11442
11499
|
// taken down — and then its position is kept for the way back. The url still
|
|
11443
|
-
// being the one it
|
|
11444
|
-
// goes (a popup closing, a section folding): there is no coming back to
|
|
11445
|
-
// place that was not left, and a position kept would greet the next mount
|
|
11500
|
+
// being the one it was last kept under means the page stays and the scroller
|
|
11501
|
+
// alone goes (a popup closing, a section folding): there is no coming back to
|
|
11502
|
+
// a place that was not left, and a position kept would greet the next mount
|
|
11446
11503
|
// under this address as a return.
|
|
11447
11504
|
const forgetScrollerUnlessPageLeft = (name) => {
|
|
11448
|
-
const
|
|
11449
|
-
if (
|
|
11505
|
+
const said = saidByScrollerName.get(name);
|
|
11506
|
+
if (said === undefined) {
|
|
11450
11507
|
return;
|
|
11451
11508
|
}
|
|
11452
|
-
|
|
11453
|
-
|
|
11509
|
+
saidByScrollerName.delete(name);
|
|
11510
|
+
const { url } = said;
|
|
11511
|
+
if (url !== readShownUrl()) {
|
|
11454
11512
|
return;
|
|
11455
11513
|
}
|
|
11456
11514
|
const positionByName = scrollerPositionsByUrl.get(url);
|
|
@@ -11978,7 +12036,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
11978
12036
|
// The browser's own scroll restoration is taken over here rather than left
|
|
11979
12037
|
// to whoever navigates: it is a decision about the document, and the entry
|
|
11980
12038
|
// being left must be recorded from the first pixel scrolled.
|
|
11981
|
-
installScrollRestoration();
|
|
12039
|
+
installScrollRestoration({ readAddress });
|
|
11982
12040
|
|
|
11983
12041
|
// The document is leaving, or may be killed in the background: what the
|
|
11984
12042
|
// entry says about it has to be what the document says (see
|
|
@@ -32492,6 +32550,20 @@ const toHex = ([r, g, b]) =>
|
|
|
32492
32550
|
// sure to be understood in, whatever getComputedStyle answered with.
|
|
32493
32551
|
let colorContext = null;
|
|
32494
32552
|
const parseColor = (color) => {
|
|
32553
|
+
const [r, g, b, a] = drawColor(color);
|
|
32554
|
+
if (a === 0 || a === 255) {
|
|
32555
|
+
return [r, g, b, a / 255];
|
|
32556
|
+
}
|
|
32557
|
+
// The canvas keeps a pixel premultiplied by its alpha, in bytes: the
|
|
32558
|
+
// channels of a translucent colour come back rounded at that alpha —
|
|
32559
|
+
// rgba(12, 23, 44, 0.45) reads 11, 22, 44. Made opaque, they read whole.
|
|
32560
|
+
const [rOpaque, gOpaque, bOpaque] = drawColor(
|
|
32561
|
+
`color(from ${color} srgb r g b / 1)`,
|
|
32562
|
+
);
|
|
32563
|
+
return [rOpaque, gOpaque, bOpaque, a / 255];
|
|
32564
|
+
};
|
|
32565
|
+
|
|
32566
|
+
const drawColor = (color) => {
|
|
32495
32567
|
if (!colorContext) {
|
|
32496
32568
|
const canvas = document.createElement("canvas");
|
|
32497
32569
|
canvas.width = 1;
|
|
@@ -32504,8 +32576,7 @@ const parseColor = (color) => {
|
|
|
32504
32576
|
colorContext.fillStyle = "transparent";
|
|
32505
32577
|
colorContext.fillStyle = color;
|
|
32506
32578
|
colorContext.fillRect(0, 0, 1, 1);
|
|
32507
|
-
|
|
32508
|
-
return [r, g, b, a / 255];
|
|
32579
|
+
return colorContext.getImageData(0, 0, 1, 1).data;
|
|
32509
32580
|
};
|
|
32510
32581
|
|
|
32511
32582
|
// The title belongs to the last Head to arrive, whatever order they leave in:
|
|
@@ -48480,6 +48551,15 @@ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
|
|
|
48480
48551
|
}
|
|
48481
48552
|
return childrenWithGap;
|
|
48482
48553
|
};
|
|
48554
|
+
const isSpacingWidth = spacing => {
|
|
48555
|
+
if (typeof spacing === "number") {
|
|
48556
|
+
return true;
|
|
48557
|
+
}
|
|
48558
|
+
if (typeof spacing !== "string") {
|
|
48559
|
+
return false;
|
|
48560
|
+
}
|
|
48561
|
+
return isSizeSpacingKey(spacing) || hasCSSSizeUnit(spacing) || spacing.startsWith("var(");
|
|
48562
|
+
};
|
|
48483
48563
|
const outsideTextFlowSet = new Set();
|
|
48484
48564
|
const markAsOutsideTextFlow = jsxElement => {
|
|
48485
48565
|
outsideTextFlowSet.add(jsxElement);
|
|
@@ -48747,7 +48827,11 @@ const TextUI = props => {
|
|
|
48747
48827
|
}
|
|
48748
48828
|
const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
|
|
48749
48829
|
if (shouldPreserveSpacing) {
|
|
48750
|
-
|
|
48830
|
+
// Between flex/grid items spacing is a gap, so only a width carries over: a
|
|
48831
|
+
// separator (the default space, a text, an element) or "pre" has no gap.
|
|
48832
|
+
if (isSpacingWidth(spacing)) {
|
|
48833
|
+
boxProps.spacing = spacing;
|
|
48834
|
+
}
|
|
48751
48835
|
} else {
|
|
48752
48836
|
children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
|
|
48753
48837
|
}
|
|
@@ -53383,6 +53467,26 @@ const Expandable = props => {
|
|
|
53383
53467
|
return stopRide;
|
|
53384
53468
|
};
|
|
53385
53469
|
|
|
53470
|
+
// A silent change is a state the page was already in (see openEffect): it
|
|
53471
|
+
// lands at once. Not playing a reveal is not enough — the stylesheet's own
|
|
53472
|
+
// transition plays it whenever the previous state's style was resolved
|
|
53473
|
+
// before the render: an expandable mounted closed and measured in the same
|
|
53474
|
+
// commit (a List sizing its items) before its deferred mount-time open, or
|
|
53475
|
+
// one kept on screen across the page change.
|
|
53476
|
+
const renderWithoutMovement = (contentContainer, render) => {
|
|
53477
|
+
if (!animation || !contentContainer) {
|
|
53478
|
+
flushSyncRendering(render);
|
|
53479
|
+
return;
|
|
53480
|
+
}
|
|
53481
|
+
contentContainer.style.transitionProperty = "none";
|
|
53482
|
+
flushSyncRendering(render);
|
|
53483
|
+
// The new style must be resolved while transitions are off — re-enabled
|
|
53484
|
+
// in the same recalc, the change would transition after all.
|
|
53485
|
+
// eslint-disable-next-line no-unused-expressions
|
|
53486
|
+
getComputedStyle(contentContainer).transitionProperty;
|
|
53487
|
+
contentContainer.style.transitionProperty = "";
|
|
53488
|
+
};
|
|
53489
|
+
|
|
53386
53490
|
// What opening LOOKS like here, and how to undo it — the one thing an
|
|
53387
53491
|
// expandable owns that a popup does not (see open_controller.js). Reassigned
|
|
53388
53492
|
// on every render so it always closes over the latest props.
|
|
@@ -53403,10 +53507,15 @@ const Expandable = props => {
|
|
|
53403
53507
|
// reopening during a collapse — read while the DOM still says closed.
|
|
53404
53508
|
const startRect = revealing ? contentContainer.getBoundingClientRect() : null;
|
|
53405
53509
|
cancelSettleWatch();
|
|
53406
|
-
|
|
53510
|
+
const renderOpened = () => {
|
|
53407
53511
|
setOpened(true);
|
|
53408
53512
|
setSettled(!revealing);
|
|
53409
|
-
}
|
|
53513
|
+
};
|
|
53514
|
+
if (silent) {
|
|
53515
|
+
renderWithoutMovement(contentContainer, renderOpened);
|
|
53516
|
+
} else {
|
|
53517
|
+
flushSyncRendering(renderOpened);
|
|
53518
|
+
}
|
|
53410
53519
|
if (hasAction) {
|
|
53411
53520
|
runUnwatched(() => effectiveAction.run());
|
|
53412
53521
|
}
|
|
@@ -53445,16 +53554,22 @@ const Expandable = props => {
|
|
|
53445
53554
|
cancelSettleWatch();
|
|
53446
53555
|
// `silent`: closed by the navigation that puts another page on screen,
|
|
53447
53556
|
// which is its movement (see page_change.js).
|
|
53448
|
-
const
|
|
53557
|
+
const silentClose = Boolean(closeEvent.detail.silent);
|
|
53558
|
+
const collapsing = animation && !silentClose && Boolean(contentContainerAtClose);
|
|
53449
53559
|
if (collapsing) {
|
|
53450
53560
|
// Now, while the content is still fully laid out — the collapsing
|
|
53451
53561
|
// track uncovers a content frozen at that size.
|
|
53452
53562
|
freezeContentSize();
|
|
53453
53563
|
}
|
|
53454
|
-
|
|
53564
|
+
const renderClosed = () => {
|
|
53455
53565
|
setOpened(false);
|
|
53456
53566
|
setSettled(!collapsing);
|
|
53457
|
-
}
|
|
53567
|
+
};
|
|
53568
|
+
if (silentClose) {
|
|
53569
|
+
renderWithoutMovement(contentContainerAtClose, renderClosed);
|
|
53570
|
+
} else {
|
|
53571
|
+
flushSyncRendering(renderClosed);
|
|
53572
|
+
}
|
|
53458
53573
|
if (restoreFocus) {
|
|
53459
53574
|
restoreFocus(closeEvent);
|
|
53460
53575
|
} else if (focusedBeforeClose && contentContainerAtClose && contentContainerAtClose.contains(focusedBeforeClose)) {
|
|
@@ -78086,9 +78201,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78086
78201
|
* does, and for a list that scrolls itself the page's own restoration cannot
|
|
78087
78202
|
* see. The position is kept under the list's `id` and the page's url, for
|
|
78088
78203
|
* the session (a reload comes back too); a list without an `id` of its own
|
|
78089
|
-
* has nothing to be remembered by
|
|
78090
|
-
* `
|
|
78091
|
-
*
|
|
78204
|
+
* has nothing to be remembered by, and two lists mounted at once under one
|
|
78205
|
+
* `id` — a popup over the page, the page a route transition is leaving —
|
|
78206
|
+
* overwrite each other (see docs/scroll.md). A fresh arrival at the page
|
|
78207
|
+
* opens at `defaultScrolled` either way, and so does a list the caller holds
|
|
78208
|
+
* through `scrolled`.
|
|
78092
78209
|
* @param {boolean} [props.deselectable]
|
|
78093
78210
|
* A single-select list allowed to hold nothing: the selected item, pressed
|
|
78094
78211
|
* again, lets go. Without it the list is a radio group — a choice, once
|