@jsenv/navi 0.29.436 → 0.29.437
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 +820 -117
- package/dist/dev/jsenv_navi.js.map +15 -11
- package/dist/jsenv_navi.js +820 -117
- package/dist/jsenv_navi.js.map +15 -11
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/data_states.md +3 -1
- package/docs/list_refresh.md +4 -1
- package/docs/network_policy.md +9 -4
- package/docs/resource.md +93 -1
- package/docs/scroll.md +3 -1
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -3696,8 +3696,11 @@ const actionWeakMap = new WeakMap();
|
|
|
3696
3696
|
* than a first load (data_states.md). Called at the start of every run whose
|
|
3697
3697
|
* value is undefined; the answer replaces it as usual, and `undefined` means
|
|
3698
3698
|
* "nothing known". A resource `GET` uses it to draw the row its store holds.
|
|
3699
|
-
*
|
|
3700
|
-
*
|
|
3699
|
+
* @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
|
|
3700
|
+
* to new params starts the new instance without the previous params' answer
|
|
3701
|
+
* (see createActionProxyFromSignal). For an answer that would say something
|
|
3702
|
+
* false about the new params (a `GET` drawing another row), not for one that
|
|
3703
|
+
* stands in while the next one comes (a list of search results).
|
|
3701
3704
|
* @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
|
|
3702
3705
|
* answer stays good once it has landed. Without it an answer lives exactly as
|
|
3703
3706
|
* long as something references it: the screen that asked goes away, and
|
|
@@ -3740,6 +3743,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3740
3743
|
resultToValue,
|
|
3741
3744
|
valueToData,
|
|
3742
3745
|
provisionalValue,
|
|
3746
|
+
inheritData = true,
|
|
3743
3747
|
dataDefault,
|
|
3744
3748
|
data = dataDefault,
|
|
3745
3749
|
|
|
@@ -4416,6 +4420,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4416
4420
|
const privateProperties = {
|
|
4417
4421
|
valueInitial,
|
|
4418
4422
|
provisionalValue,
|
|
4423
|
+
inheritData,
|
|
4419
4424
|
|
|
4420
4425
|
performRun,
|
|
4421
4426
|
performReset,
|
|
@@ -4448,7 +4453,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4448
4453
|
* a list of filters changes, providing real-time results without user interaction.
|
|
4449
4454
|
* @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
|
|
4450
4455
|
* By default (true), the proxy carries over the previous target's value and error into the new action —
|
|
4451
|
-
* unless the action declares
|
|
4456
|
+
* unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
|
|
4452
4457
|
* This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
|
|
4453
4458
|
* new action completes, not when it starts loading. Code that needs to distinguish loading state can still
|
|
4454
4459
|
* check `action.runningState`, while code that just reads `action.data` always sees the most recent
|
|
@@ -4495,12 +4500,12 @@ const createActionProxyFromSignal = (
|
|
|
4495
4500
|
let currentActionPrivateProperties = getActionPrivateProperties(action);
|
|
4496
4501
|
let actionTargetPreviousWeakRef = null;
|
|
4497
4502
|
|
|
4498
|
-
// An action
|
|
4499
|
-
//
|
|
4500
|
-
//
|
|
4501
|
-
//
|
|
4503
|
+
// An action whose answer for other params would be false for these (a
|
|
4504
|
+
// resource GET: another row) says so itself, and is not handed it. What it
|
|
4505
|
+
// does not know stays unknown: a skeleton, or its provisionalValue, never a
|
|
4506
|
+
// different row.
|
|
4502
4507
|
const inheritsAnswer =
|
|
4503
|
-
inheritData &&
|
|
4508
|
+
inheritData && getActionPrivateProperties(action).inheritData;
|
|
4504
4509
|
const createTarget = (params) => {
|
|
4505
4510
|
if (inheritsAnswer) {
|
|
4506
4511
|
const previousActionTarget = actionTargetPreviousWeakRef?.deref();
|
|
@@ -10651,6 +10656,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
|
|
|
10651
10656
|
|
|
10652
10657
|
let setupRoutesCalled = false;
|
|
10653
10658
|
let activeRouteSet = null;
|
|
10659
|
+
// True once the routes have been matched against an address. Before that, no
|
|
10660
|
+
// route matching says nothing about the page: nothing has been asked yet.
|
|
10661
|
+
const routedSignal = signal(false);
|
|
10654
10662
|
|
|
10655
10663
|
const routePreloadMap = new WeakMap();
|
|
10656
10664
|
const registerRoutePreload = (route, preload) => {
|
|
@@ -11075,6 +11083,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
11075
11083
|
}
|
|
11076
11084
|
// Reset flag after URL -> Signal synchronization is complete
|
|
11077
11085
|
isUpdatingRoutesFromUrl = false;
|
|
11086
|
+
routedSignal.value = true;
|
|
11078
11087
|
Object.assign(returnValue, { matchingRouteSet });
|
|
11079
11088
|
for (const [route, repairedParams] of repairedParamsByRoute) {
|
|
11080
11089
|
route.replaceParams(repairedParams, {
|
|
@@ -11124,6 +11133,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
11124
11133
|
droppingRouteSet = null;
|
|
11125
11134
|
setupRoutesCalled = false;
|
|
11126
11135
|
activeRouteSet = null;
|
|
11136
|
+
routedSignal.value = false;
|
|
11127
11137
|
};
|
|
11128
11138
|
return {
|
|
11129
11139
|
updateRoutes,
|
|
@@ -15628,6 +15638,13 @@ const createOpenController = (
|
|
|
15628
15638
|
change();
|
|
15629
15639
|
};
|
|
15630
15640
|
const runChange = (change, { opened, event }) => {
|
|
15641
|
+
// What the controller answers about itself is the decision, written before
|
|
15642
|
+
// the change runs, whatever runs it. Whoever asks meanwhile reads it: the
|
|
15643
|
+
// render an opening forces (onOpen, mountContent) reads the open state
|
|
15644
|
+
// the caller holds, and an onOpen writing the popup's own `signal` has
|
|
15645
|
+
// useOpenPropsEffectOnOpenController asking for this very opening — a
|
|
15646
|
+
// second one, nested in the first, if `opened` still said closed.
|
|
15647
|
+
controller.opened = opened;
|
|
15631
15648
|
const applyChange = () => {
|
|
15632
15649
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
15633
15650
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
@@ -15643,12 +15660,8 @@ const createOpenController = (
|
|
|
15643
15660
|
applyChange();
|
|
15644
15661
|
return;
|
|
15645
15662
|
}
|
|
15646
|
-
//
|
|
15647
|
-
//
|
|
15648
|
-
// useOpenPropsEffectOnOpenController, which writes it back into the
|
|
15649
|
-
// caller's own signal), and a request arriving before the change lands
|
|
15650
|
-
// runs it first rather than reading a DOM that disagrees.
|
|
15651
|
-
controller.opened = opened;
|
|
15663
|
+
// A request arriving before the change lands runs it first rather than
|
|
15664
|
+
// reading a DOM that disagrees.
|
|
15652
15665
|
changeAwaitingTransition = applyChange;
|
|
15653
15666
|
transitionChange(
|
|
15654
15667
|
() => {
|
|
@@ -15661,7 +15674,6 @@ const createOpenController = (
|
|
|
15661
15674
|
};
|
|
15662
15675
|
|
|
15663
15676
|
const performClose = (closeEvent) => {
|
|
15664
|
-
controller.opened = false;
|
|
15665
15677
|
// Read before any close effect touches the DOM: closing a native <dialog>
|
|
15666
15678
|
// hands the focus back to whatever held it at showModal() time, so by the
|
|
15667
15679
|
// time the close cleanup runs, the popup's content has already lost the
|
|
@@ -15929,7 +15941,6 @@ const createOpenController = (
|
|
|
15929
15941
|
// told it opened right after (see popup_content_mount.js and
|
|
15930
15942
|
// use_displayed_layout_effect.js).
|
|
15931
15943
|
controller.openedInDom = true;
|
|
15932
|
-
controller.opened = true;
|
|
15933
15944
|
// Which press it opened during, so the release of that press is not
|
|
15934
15945
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
15935
15946
|
controller.pressCountAtOpen = pressCount;
|
|
@@ -16374,8 +16385,10 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
16374
16385
|
return undefined;
|
|
16375
16386
|
}
|
|
16376
16387
|
// Skip when the controller is already in the desired state.
|
|
16377
|
-
// openController.opened
|
|
16378
|
-
//
|
|
16388
|
+
// openController.opened is what the controller decided, written before an
|
|
16389
|
+
// opening renders anything (see runChange), so it is the authoritative
|
|
16390
|
+
// check against feedback loops — the popup's own onOpen writing its
|
|
16391
|
+
// `signal` included.
|
|
16379
16392
|
if (open === openController.opened) {
|
|
16380
16393
|
return undefined;
|
|
16381
16394
|
}
|
|
@@ -26725,6 +26738,171 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
|
|
|
26725
26738
|
return childStore !== undefined && childStore === getChildStore(ownerId);
|
|
26726
26739
|
};
|
|
26727
26740
|
|
|
26741
|
+
/*
|
|
26742
|
+
* What a previous document kept of the page it was on (see keepPageOnScreen in
|
|
26743
|
+
* nav/page_kept.js), waiting for the reads that draw it again, and what those
|
|
26744
|
+
* reads are made of on the way out: rows with their relations inline.
|
|
26745
|
+
*
|
|
26746
|
+
* A kept read is found by the read's name and the params it was asked with
|
|
26747
|
+
* (getKeptReadKey). It is handed out once: the first run of the read draws it
|
|
26748
|
+
* while its request is out, and from then on the store holds the rows. Handed
|
|
26749
|
+
* out again, after a reset say, it would write the previous document's rows
|
|
26750
|
+
* over a fresher answer.
|
|
26751
|
+
*
|
|
26752
|
+
* This module is the half the resources see: `resource()` and the range reader
|
|
26753
|
+
* take from it, and say which of their reads can be kept at all. Which reads
|
|
26754
|
+
* are on the page on screen, and what the slot is written into, is
|
|
26755
|
+
* page_kept.js.
|
|
26756
|
+
*/
|
|
26757
|
+
|
|
26758
|
+
// key → what the read answered in the previous document, not handed out yet
|
|
26759
|
+
const offeredReadMap = new Map();
|
|
26760
|
+
const offerKeptReads = (entryMap) => {
|
|
26761
|
+
offeredReadMap.clear();
|
|
26762
|
+
for (const [key, entry] of entryMap) {
|
|
26763
|
+
offeredReadMap.set(key, entry);
|
|
26764
|
+
}
|
|
26765
|
+
};
|
|
26766
|
+
const withdrawKeptReads = () => {
|
|
26767
|
+
offeredReadMap.clear();
|
|
26768
|
+
};
|
|
26769
|
+
const takeKeptRead = (key) => {
|
|
26770
|
+
const entry = offeredReadMap.get(key);
|
|
26771
|
+
if (entry === undefined) {
|
|
26772
|
+
return undefined;
|
|
26773
|
+
}
|
|
26774
|
+
offeredReadMap.delete(key);
|
|
26775
|
+
return entry;
|
|
26776
|
+
};
|
|
26777
|
+
|
|
26778
|
+
// The root actions whose answer is rows of a store, written and read back the
|
|
26779
|
+
// same way: a resource's GET and GET_MANY. Anything else a route runs (a page's
|
|
26780
|
+
// code, a computation) has nothing a later document could draw.
|
|
26781
|
+
const keepableReadWeakSet = new WeakSet();
|
|
26782
|
+
const markKeepableRead = (rootAction) => {
|
|
26783
|
+
keepableReadWeakSet.add(rootAction);
|
|
26784
|
+
};
|
|
26785
|
+
const isKeepableRead = (action) => {
|
|
26786
|
+
const rootAction = action.rootAction || action;
|
|
26787
|
+
return keepableReadWeakSet.has(rootAction);
|
|
26788
|
+
};
|
|
26789
|
+
|
|
26790
|
+
// The range readers a <List.Items> on screen reads through, each with the number
|
|
26791
|
+
// of lists reading it: the reads of the page a route action does not cover.
|
|
26792
|
+
const shownRangeReadersSignal = signal(new Map());
|
|
26793
|
+
const showRangeReader = (reader) => {
|
|
26794
|
+
const shownMap = new Map(shownRangeReadersSignal.peek());
|
|
26795
|
+
shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
|
|
26796
|
+
shownRangeReadersSignal.value = shownMap;
|
|
26797
|
+
return () => {
|
|
26798
|
+
const shownMapNow = new Map(shownRangeReadersSignal.peek());
|
|
26799
|
+
const count = shownMapNow.get(reader);
|
|
26800
|
+
if (count === 1) {
|
|
26801
|
+
shownMapNow.delete(reader);
|
|
26802
|
+
} else {
|
|
26803
|
+
shownMapNow.set(reader, count - 1);
|
|
26804
|
+
}
|
|
26805
|
+
shownRangeReadersSignal.value = shownMapNow;
|
|
26806
|
+
};
|
|
26807
|
+
};
|
|
26808
|
+
|
|
26809
|
+
// A row already in the store is the fresher one: the kept copy only stands in
|
|
26810
|
+
// for rows nothing has answered yet. Its relations are written as they were
|
|
26811
|
+
// kept, which a real answer corrects like any other.
|
|
26812
|
+
const upsertKeptItem = (store, keptItem) => {
|
|
26813
|
+
const id = keptItem[store.idKey];
|
|
26814
|
+
if (id !== undefined) {
|
|
26815
|
+
const existing = store.select(id);
|
|
26816
|
+
if (existing) {
|
|
26817
|
+
return existing;
|
|
26818
|
+
}
|
|
26819
|
+
}
|
|
26820
|
+
return store.upsert(keptItem);
|
|
26821
|
+
};
|
|
26822
|
+
|
|
26823
|
+
const getKeptReadKey = (name, params) => {
|
|
26824
|
+
return `${name}|${getParamsKey(params)}`;
|
|
26825
|
+
};
|
|
26826
|
+
// `true` is what a route action without params is bound to (see routeAction),
|
|
26827
|
+
// and a key holding undefined says nothing a missing one does not.
|
|
26828
|
+
const getParamsKey = (params) => {
|
|
26829
|
+
if (params === undefined || params === NO_PARAMS || params === true) {
|
|
26830
|
+
return "";
|
|
26831
|
+
}
|
|
26832
|
+
if (isPlainParams(params)) {
|
|
26833
|
+
const keys = Object.keys(params).filter((key) => params[key] !== undefined);
|
|
26834
|
+
if (keys.length === 0) {
|
|
26835
|
+
return "";
|
|
26836
|
+
}
|
|
26837
|
+
}
|
|
26838
|
+
return stableStringify(params);
|
|
26839
|
+
};
|
|
26840
|
+
const isPlainParams = (value) => {
|
|
26841
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26842
|
+
};
|
|
26843
|
+
const stableStringify = (value) => {
|
|
26844
|
+
if (value === null || typeof value !== "object") {
|
|
26845
|
+
return JSON.stringify(value);
|
|
26846
|
+
}
|
|
26847
|
+
if (Array.isArray(value)) {
|
|
26848
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
26849
|
+
}
|
|
26850
|
+
const keys = Object.keys(value)
|
|
26851
|
+
.filter((key) => value[key] !== undefined)
|
|
26852
|
+
.sort();
|
|
26853
|
+
const parts = [];
|
|
26854
|
+
for (const key of keys) {
|
|
26855
|
+
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
26856
|
+
}
|
|
26857
|
+
return `{${parts.join(",")}}`;
|
|
26858
|
+
};
|
|
26859
|
+
|
|
26860
|
+
// What a read's data is made of, ready to be written: a row, or the rows of a
|
|
26861
|
+
// list, each with its relations inline.
|
|
26862
|
+
const serializeData = (data) => {
|
|
26863
|
+
if (Array.isArray(data)) {
|
|
26864
|
+
return data.map((item) => serializeItem(item, new Set()));
|
|
26865
|
+
}
|
|
26866
|
+
return serializeItem(data, new Set());
|
|
26867
|
+
};
|
|
26868
|
+
// The item with its relations inline: a relation value stands for what its
|
|
26869
|
+
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
26870
|
+
// twice on the same path is written as its id — the setters accept both.
|
|
26871
|
+
const serializeItem = (item, ancestorSet) => {
|
|
26872
|
+
ancestorSet.add(item);
|
|
26873
|
+
const serialized = {};
|
|
26874
|
+
for (const key of Object.keys(item)) {
|
|
26875
|
+
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
26876
|
+
}
|
|
26877
|
+
ancestorSet.delete(item);
|
|
26878
|
+
return serialized;
|
|
26879
|
+
};
|
|
26880
|
+
const serializeValue = (value, ancestorSet) => {
|
|
26881
|
+
if (value === null || typeof value !== "object") {
|
|
26882
|
+
return value;
|
|
26883
|
+
}
|
|
26884
|
+
if (Array.isArray(value)) {
|
|
26885
|
+
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
26886
|
+
}
|
|
26887
|
+
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
26888
|
+
if (objectSignal) {
|
|
26889
|
+
const held = objectSignal.value;
|
|
26890
|
+
if (held === undefined || held === null) {
|
|
26891
|
+
return null;
|
|
26892
|
+
}
|
|
26893
|
+
return serializeValue(held, ancestorSet);
|
|
26894
|
+
}
|
|
26895
|
+
if (typeof value.toJSON === "function") {
|
|
26896
|
+
return value;
|
|
26897
|
+
}
|
|
26898
|
+
if (ancestorSet.has(value)) {
|
|
26899
|
+
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26900
|
+
? value[SYMBOL_IDENTITY]
|
|
26901
|
+
: null;
|
|
26902
|
+
}
|
|
26903
|
+
return serializeItem(value, ancestorSet);
|
|
26904
|
+
};
|
|
26905
|
+
|
|
26728
26906
|
const paramScopeWeakSet = createIterableWeakSet();
|
|
26729
26907
|
let paramScopeIdCounter = 0;
|
|
26730
26908
|
const getParamScope = (params) => {
|
|
@@ -26895,66 +27073,6 @@ const createResourcePersistence = (
|
|
|
26895
27073
|
};
|
|
26896
27074
|
};
|
|
26897
27075
|
|
|
26898
|
-
const getParamsKey = (params) => {
|
|
26899
|
-
if (params === undefined || params === NO_PARAMS) {
|
|
26900
|
-
return "";
|
|
26901
|
-
}
|
|
26902
|
-
return stableStringify(params);
|
|
26903
|
-
};
|
|
26904
|
-
|
|
26905
|
-
const stableStringify = (value) => {
|
|
26906
|
-
if (value === null || typeof value !== "object") {
|
|
26907
|
-
return JSON.stringify(value);
|
|
26908
|
-
}
|
|
26909
|
-
if (Array.isArray(value)) {
|
|
26910
|
-
return `[${value.map(stableStringify).join(",")}]`;
|
|
26911
|
-
}
|
|
26912
|
-
const keys = Object.keys(value).sort();
|
|
26913
|
-
const parts = [];
|
|
26914
|
-
for (const key of keys) {
|
|
26915
|
-
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
26916
|
-
}
|
|
26917
|
-
return `{${parts.join(",")}}`;
|
|
26918
|
-
};
|
|
26919
|
-
|
|
26920
|
-
// The item with its relations inline: a relation value stands for what its
|
|
26921
|
-
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
26922
|
-
// twice on the same path is written as its id — the setters accept both.
|
|
26923
|
-
const serializeItem = (item, ancestorSet) => {
|
|
26924
|
-
ancestorSet.add(item);
|
|
26925
|
-
const serialized = {};
|
|
26926
|
-
for (const key of Object.keys(item)) {
|
|
26927
|
-
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
26928
|
-
}
|
|
26929
|
-
ancestorSet.delete(item);
|
|
26930
|
-
return serialized;
|
|
26931
|
-
};
|
|
26932
|
-
const serializeValue = (value, ancestorSet) => {
|
|
26933
|
-
if (value === null || typeof value !== "object") {
|
|
26934
|
-
return value;
|
|
26935
|
-
}
|
|
26936
|
-
if (Array.isArray(value)) {
|
|
26937
|
-
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
26938
|
-
}
|
|
26939
|
-
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
26940
|
-
if (objectSignal) {
|
|
26941
|
-
const held = objectSignal.value;
|
|
26942
|
-
if (held === undefined || held === null) {
|
|
26943
|
-
return null;
|
|
26944
|
-
}
|
|
26945
|
-
return serializeValue(held, ancestorSet);
|
|
26946
|
-
}
|
|
26947
|
-
if (typeof value.toJSON === "function") {
|
|
26948
|
-
return value;
|
|
26949
|
-
}
|
|
26950
|
-
if (ancestorSet.has(value)) {
|
|
26951
|
-
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26952
|
-
? value[SYMBOL_IDENTITY]
|
|
26953
|
-
: null;
|
|
26954
|
-
}
|
|
26955
|
-
return serializeItem(value, ancestorSet);
|
|
26956
|
-
};
|
|
26957
|
-
|
|
26958
27076
|
/*
|
|
26959
27077
|
* GET_RANGE: reading a resource one slice at a time, for a list that draws its
|
|
26960
27078
|
* rows as it goes (`<List.Items itemsAction>`).
|
|
@@ -26991,9 +27109,23 @@ const serializeValue = (value, ancestorSet) => {
|
|
|
26991
27109
|
* of what a rerun is for an action. Every reader made by `bindParams` shares
|
|
26992
27110
|
* the signal and the compositions of the one it comes from: the params say
|
|
26993
27111
|
* which slices are read, not which collection.
|
|
27112
|
+
*
|
|
27113
|
+
* A composition also outlives the document when a list on the page on screen
|
|
27114
|
+
* reads it (see keepPageOnScreen): the ranks around the window it draws are
|
|
27115
|
+
* written with their rows, and the first read of the next document finds them
|
|
27116
|
+
* as a composition left by a list that went away — the same return, after a
|
|
27117
|
+
* longer absence.
|
|
26994
27118
|
*/
|
|
26995
27119
|
|
|
26996
27120
|
|
|
27121
|
+
// The ranks kept of a composition for the next document: the window a list
|
|
27122
|
+
// draws, widened by at least one step on each side, and snapped to steps so a
|
|
27123
|
+
// scroll within one draws the same ranks and writes nothing.
|
|
27124
|
+
const KEPT_RANK_STEP = 20;
|
|
27125
|
+
// Counts the answers every composition took, so that one landing can be told
|
|
27126
|
+
// from one seen before (see readKept).
|
|
27127
|
+
let answerSequence = 0;
|
|
27128
|
+
|
|
26997
27129
|
const createRangeReader = (
|
|
26998
27130
|
actionName,
|
|
26999
27131
|
callback,
|
|
@@ -27002,6 +27134,9 @@ const createRangeReader = (
|
|
|
27002
27134
|
params: boundParams,
|
|
27003
27135
|
invalidationSignal = signal(0),
|
|
27004
27136
|
compositionSet = new Set(),
|
|
27137
|
+
// Bumped whenever what is kept of a composition for the next document
|
|
27138
|
+
// changes (see readKept); shared like the compositions themselves.
|
|
27139
|
+
keptVersionSignal = signal(0),
|
|
27005
27140
|
},
|
|
27006
27141
|
) => {
|
|
27007
27142
|
// Which composition this reader is about: the values its params hold, not its
|
|
@@ -27018,6 +27153,40 @@ const createRangeReader = (
|
|
|
27018
27153
|
}
|
|
27019
27154
|
return null;
|
|
27020
27155
|
};
|
|
27156
|
+
const keptKeyOf = (params) => getKeptReadKey(actionName, params);
|
|
27157
|
+
const bumpKeptVersion = () => {
|
|
27158
|
+
keptVersionSignal.value = keptVersionSignal.peek() + 1;
|
|
27159
|
+
};
|
|
27160
|
+
// The composition the previous document kept for these params, entering the
|
|
27161
|
+
// store and the compositions as if a list had just left it.
|
|
27162
|
+
const restoreKeptComposition = (params) => {
|
|
27163
|
+
const kept = takeKeptRead(keptKeyOf(params));
|
|
27164
|
+
if (!kept || typeof kept.count !== "number" || !kept.items) {
|
|
27165
|
+
return null;
|
|
27166
|
+
}
|
|
27167
|
+
const composition = {
|
|
27168
|
+
params,
|
|
27169
|
+
idByIndex: new Map(),
|
|
27170
|
+
count: kept.count,
|
|
27171
|
+
keptFrom: undefined,
|
|
27172
|
+
keptTo: undefined,
|
|
27173
|
+
answeredAt: 0,
|
|
27174
|
+
};
|
|
27175
|
+
for (const rank of Object.keys(kept.items)) {
|
|
27176
|
+
const index = Number(rank);
|
|
27177
|
+
const item = upsertKeptItem(store, kept.items[rank]);
|
|
27178
|
+
composition.idByIndex.set(index, item[store.idKey]);
|
|
27179
|
+
if (composition.keptFrom === undefined || index < composition.keptFrom) {
|
|
27180
|
+
composition.keptFrom = index;
|
|
27181
|
+
}
|
|
27182
|
+
if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
|
|
27183
|
+
composition.keptTo = index + 1;
|
|
27184
|
+
}
|
|
27185
|
+
}
|
|
27186
|
+
compositionSet.add(composition);
|
|
27187
|
+
bumpKeptVersion();
|
|
27188
|
+
return composition;
|
|
27189
|
+
};
|
|
27021
27190
|
const readRange = async (range = {}) => {
|
|
27022
27191
|
const { signal, ...rangeParams } = range;
|
|
27023
27192
|
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
@@ -27051,11 +27220,15 @@ const createRangeReader = (
|
|
|
27051
27220
|
readRange.invalidate = () => {
|
|
27052
27221
|
compositionSet.clear();
|
|
27053
27222
|
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
27223
|
+
bumpKeptVersion();
|
|
27054
27224
|
};
|
|
27055
27225
|
// The rows of a composition, drawn from the store: a rank whose row is gone
|
|
27056
27226
|
// from the store resolves to nothing and is asked for again.
|
|
27057
27227
|
readRange.readComposition = () => {
|
|
27058
|
-
const
|
|
27228
|
+
const params = currentParams();
|
|
27229
|
+
const composition = untracked(
|
|
27230
|
+
() => findComposition(params) || restoreKeptComposition(params),
|
|
27231
|
+
);
|
|
27059
27232
|
if (!composition) {
|
|
27060
27233
|
return null;
|
|
27061
27234
|
}
|
|
@@ -27079,18 +27252,85 @@ const createRangeReader = (
|
|
|
27079
27252
|
const params = currentParams();
|
|
27080
27253
|
let composition = findComposition(params);
|
|
27081
27254
|
if (!composition) {
|
|
27082
|
-
composition = {
|
|
27255
|
+
composition = {
|
|
27256
|
+
params,
|
|
27257
|
+
idByIndex: new Map(),
|
|
27258
|
+
count,
|
|
27259
|
+
keptFrom: undefined,
|
|
27260
|
+
keptTo: undefined,
|
|
27261
|
+
answeredAt: 0,
|
|
27262
|
+
};
|
|
27083
27263
|
compositionSet.add(composition);
|
|
27084
27264
|
} else if (replace) {
|
|
27085
27265
|
composition.idByIndex = new Map();
|
|
27086
27266
|
}
|
|
27087
27267
|
composition.count = count;
|
|
27268
|
+
answerSequence++;
|
|
27269
|
+
composition.answeredAt = answerSequence;
|
|
27088
27270
|
for (const [index, item] of byIndex) {
|
|
27089
27271
|
const id = item ? item[store.idKey] : undefined;
|
|
27090
27272
|
if (id !== undefined) {
|
|
27091
27273
|
composition.idByIndex.set(index, id);
|
|
27092
27274
|
}
|
|
27093
27275
|
}
|
|
27276
|
+
bumpKeptVersion();
|
|
27277
|
+
};
|
|
27278
|
+
// The ranks [windowFrom, windowTo) a list draws of this composition: what is
|
|
27279
|
+
// kept of it for the next document is around them (see readKept).
|
|
27280
|
+
readRange.frameComposition = (windowFrom, windowTo) => {
|
|
27281
|
+
const composition = findComposition(currentParams());
|
|
27282
|
+
if (!composition) {
|
|
27283
|
+
return;
|
|
27284
|
+
}
|
|
27285
|
+
let keptFrom =
|
|
27286
|
+
Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
|
|
27287
|
+
KEPT_RANK_STEP;
|
|
27288
|
+
if (keptFrom < 0) {
|
|
27289
|
+
keptFrom = 0;
|
|
27290
|
+
}
|
|
27291
|
+
const keptTo =
|
|
27292
|
+
Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
|
|
27293
|
+
if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
|
|
27294
|
+
return;
|
|
27295
|
+
}
|
|
27296
|
+
composition.keptFrom = keptFrom;
|
|
27297
|
+
composition.keptTo = keptTo;
|
|
27298
|
+
bumpKeptVersion();
|
|
27299
|
+
};
|
|
27300
|
+
// What a document keeps of the composition these params read, for the
|
|
27301
|
+
// next one (see keepPageOnScreen): the count, and the ranks around the
|
|
27302
|
+
// window last drawn with their rows inline. Read inside an effect, it
|
|
27303
|
+
// follows the composition and the rows. `answeredAt` grows with every answer
|
|
27304
|
+
// the composition takes (0: none in this document, restored from a kept one).
|
|
27305
|
+
readRange.readKept = () => {
|
|
27306
|
+
// eslint-disable-next-line no-unused-expressions
|
|
27307
|
+
keptVersionSignal.value;
|
|
27308
|
+
const params = resolveParams(boundParams);
|
|
27309
|
+
const key = keptKeyOf(params);
|
|
27310
|
+
const composition = findComposition(params);
|
|
27311
|
+
if (!composition || composition.count === undefined) {
|
|
27312
|
+
return { key, answeredAt: 0, readEntry: () => undefined };
|
|
27313
|
+
}
|
|
27314
|
+
const readEntry = () => {
|
|
27315
|
+
const items = {};
|
|
27316
|
+
for (const [index, id] of composition.idByIndex) {
|
|
27317
|
+
if (
|
|
27318
|
+
composition.keptFrom !== undefined &&
|
|
27319
|
+
index < composition.keptFrom
|
|
27320
|
+
) {
|
|
27321
|
+
continue;
|
|
27322
|
+
}
|
|
27323
|
+
if (composition.keptTo !== undefined && index >= composition.keptTo) {
|
|
27324
|
+
continue;
|
|
27325
|
+
}
|
|
27326
|
+
const item = store.select(id);
|
|
27327
|
+
if (item) {
|
|
27328
|
+
items[index] = serializeItem(item, new Set());
|
|
27329
|
+
}
|
|
27330
|
+
}
|
|
27331
|
+
return { count: composition.count, items };
|
|
27332
|
+
};
|
|
27333
|
+
return { key, answeredAt: composition.answeredAt, readEntry };
|
|
27094
27334
|
};
|
|
27095
27335
|
// The same trade a list makes with the rows it holds, applied to what is kept
|
|
27096
27336
|
// for the next mount. Two lists on one collection each trim by their own
|
|
@@ -27101,11 +27341,16 @@ const createRangeReader = (
|
|
|
27101
27341
|
if (!composition || composition.idByIndex.size <= budget) {
|
|
27102
27342
|
return;
|
|
27103
27343
|
}
|
|
27344
|
+
let trimmed = false;
|
|
27104
27345
|
for (const index of composition.idByIndex.keys()) {
|
|
27105
27346
|
if (index < keepFrom || index > keepTo) {
|
|
27106
27347
|
composition.idByIndex.delete(index);
|
|
27348
|
+
trimmed = true;
|
|
27107
27349
|
}
|
|
27108
27350
|
}
|
|
27351
|
+
if (trimmed) {
|
|
27352
|
+
bumpKeptVersion();
|
|
27353
|
+
}
|
|
27109
27354
|
};
|
|
27110
27355
|
// Memoized for the reasons an action's bindParams is (see actions.js): params
|
|
27111
27356
|
// and the reader they make have synchronized lifetimes, and params equal in
|
|
@@ -27121,6 +27366,7 @@ const createRangeReader = (
|
|
|
27121
27366
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
27122
27367
|
invalidationSignal,
|
|
27123
27368
|
compositionSet,
|
|
27369
|
+
keptVersionSignal,
|
|
27124
27370
|
});
|
|
27125
27371
|
readerByParams.set(paramsToBind, reader);
|
|
27126
27372
|
return reader;
|
|
@@ -28555,7 +28801,7 @@ const createRestActionFactoryForRoot = (
|
|
|
28555
28801
|
`${name}.${verb}`,
|
|
28556
28802
|
declarationSite,
|
|
28557
28803
|
);
|
|
28558
|
-
|
|
28804
|
+
const action = createAction(callback, {
|
|
28559
28805
|
name: `${name}.${verb}`,
|
|
28560
28806
|
meta: { verb, isMany: false, paramScope },
|
|
28561
28807
|
resultToValue: (result, action) => {
|
|
@@ -28591,17 +28837,33 @@ const createRestActionFactoryForRoot = (
|
|
|
28591
28837
|
},
|
|
28592
28838
|
valueToData: (itemId) => store.select(itemId),
|
|
28593
28839
|
// While the request is out, the row the store already holds for these
|
|
28594
|
-
// params is drawn
|
|
28840
|
+
// params is drawn, or the row the page kept from the previous document
|
|
28841
|
+
// (kept_reads.js): the action starts on the "refresh over a known
|
|
28595
28842
|
// answer" line of data_states.md rather than the first-load one.
|
|
28596
28843
|
provisionalValue:
|
|
28597
28844
|
verb === "GET"
|
|
28598
|
-
? (params) => {
|
|
28599
|
-
const item = untracked(() =>
|
|
28845
|
+
? (params, action) => {
|
|
28846
|
+
const item = untracked(() => {
|
|
28847
|
+
const itemForGet = findItemForGet(params);
|
|
28848
|
+
if (itemForGet) {
|
|
28849
|
+
return itemForGet;
|
|
28850
|
+
}
|
|
28851
|
+
const keptItem = takeKeptRead(
|
|
28852
|
+
getKeptReadKey(action.name, params),
|
|
28853
|
+
);
|
|
28854
|
+
return keptItem ? upsertKeptItem(store, keptItem) : null;
|
|
28855
|
+
});
|
|
28600
28856
|
return item ? item[idKey] : undefined;
|
|
28601
28857
|
}
|
|
28602
28858
|
: undefined,
|
|
28859
|
+
// Another params' answer is another row.
|
|
28860
|
+
inheritData: verb !== "GET",
|
|
28603
28861
|
completeSideEffect: onActionComplete,
|
|
28604
28862
|
});
|
|
28863
|
+
if (verb === "GET") {
|
|
28864
|
+
markKeepableRead(action);
|
|
28865
|
+
}
|
|
28866
|
+
return action;
|
|
28605
28867
|
};
|
|
28606
28868
|
const createActionAffectingManyItems = (
|
|
28607
28869
|
verb,
|
|
@@ -28616,7 +28878,7 @@ const createRestActionFactoryForRoot = (
|
|
|
28616
28878
|
return itemArray.map((item) => item[idKey]);
|
|
28617
28879
|
};
|
|
28618
28880
|
|
|
28619
|
-
|
|
28881
|
+
const action = createAction(callback, {
|
|
28620
28882
|
meta: { verb, isMany: true, paramScope },
|
|
28621
28883
|
name: `${name}.${verb}_MANY`,
|
|
28622
28884
|
resultToValue: applyResultToValue,
|
|
@@ -28624,6 +28886,25 @@ const createRestActionFactoryForRoot = (
|
|
|
28624
28886
|
// undefined so a screen can tell "not asked yet" from "answered: none".
|
|
28625
28887
|
valueToData: (idArray) =>
|
|
28626
28888
|
idArray === undefined ? undefined : store.selectAll(idArray),
|
|
28889
|
+
// The rows the page kept from the previous document (kept_reads.js),
|
|
28890
|
+
// drawn while the request is out. A binding moving to new params still
|
|
28891
|
+
// hands over the previous answer when there are none: a list standing
|
|
28892
|
+
// in for the next one beats a skeleton.
|
|
28893
|
+
provisionalValue:
|
|
28894
|
+
verb === "GET"
|
|
28895
|
+
? (params, action) =>
|
|
28896
|
+
untracked(() => {
|
|
28897
|
+
const keptItems = takeKeptRead(
|
|
28898
|
+
getKeptReadKey(action.name, params),
|
|
28899
|
+
);
|
|
28900
|
+
if (!Array.isArray(keptItems)) {
|
|
28901
|
+
return undefined;
|
|
28902
|
+
}
|
|
28903
|
+
return keptItems.map(
|
|
28904
|
+
(keptItem) => upsertKeptItem(store, keptItem)[idKey],
|
|
28905
|
+
);
|
|
28906
|
+
})
|
|
28907
|
+
: undefined,
|
|
28627
28908
|
completeSideEffect: (actionCompleted) => {
|
|
28628
28909
|
onActionComplete(actionCompleted);
|
|
28629
28910
|
if (
|
|
@@ -28639,6 +28920,10 @@ const createRestActionFactoryForRoot = (
|
|
|
28639
28920
|
return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
|
|
28640
28921
|
},
|
|
28641
28922
|
});
|
|
28923
|
+
if (verb === "GET") {
|
|
28924
|
+
markKeepableRead(action);
|
|
28925
|
+
}
|
|
28926
|
+
return action;
|
|
28642
28927
|
};
|
|
28643
28928
|
|
|
28644
28929
|
return createActionForRoot;
|
|
@@ -28649,10 +28934,12 @@ const createRestActionFactoryForRoot = (
|
|
|
28649
28934
|
// NetworkPolicyError carrying the policy's reason. A read is called or not depending
|
|
28650
28935
|
// on where the policy answers reads from. Answered from the store, a GET of a
|
|
28651
28936
|
// root resource completes with the row the store holds for it (see
|
|
28652
|
-
// findItemInStore)
|
|
28653
|
-
//
|
|
28654
|
-
//
|
|
28655
|
-
//
|
|
28937
|
+
// findItemInStore), and a root GET or GET_MANY holding rows for these params
|
|
28938
|
+
// (the kept page, drawn while the request is out) with those — handing them
|
|
28939
|
+
// back is an upsert without effect, so the action completes with what it had
|
|
28940
|
+
// and nothing is asked. A relationship GET has no row of its own to answer
|
|
28941
|
+
// with and settles with the same error. A completed GET asked to rerun never
|
|
28942
|
+
// gets here (actions.js holds it).
|
|
28656
28943
|
const applyNetworkPolicy = (
|
|
28657
28944
|
restCallback,
|
|
28658
28945
|
{ verb, isMany, findItemInStore },
|
|
@@ -28666,6 +28953,12 @@ const applyNetworkPolicy = (
|
|
|
28666
28953
|
if (!policy.readsFromStore) {
|
|
28667
28954
|
return restCallback(params, context);
|
|
28668
28955
|
}
|
|
28956
|
+
if (isKeepableRead(context.action)) {
|
|
28957
|
+
const dataHeld = context.action.dataSignal.peek();
|
|
28958
|
+
if (dataHeld !== undefined) {
|
|
28959
|
+
return dataHeld;
|
|
28960
|
+
}
|
|
28961
|
+
}
|
|
28669
28962
|
if (!isMany && findItemInStore) {
|
|
28670
28963
|
const item = findItemInStore(params, context.action);
|
|
28671
28964
|
if (item) {
|
|
@@ -38915,6 +39208,37 @@ const writeCover = (edge, element, size) => {
|
|
|
38915
39208
|
style.setProperty(property, value);
|
|
38916
39209
|
};
|
|
38917
39210
|
|
|
39211
|
+
// Every route action, with whether it asks something of the address at the
|
|
39212
|
+
// moment: its route matches and its params getter returns something.
|
|
39213
|
+
const routeActionEntriesSignal = signal([]);
|
|
39214
|
+
let activeRouteActionsPrevious = new Set();
|
|
39215
|
+
/**
|
|
39216
|
+
* The route actions asking something for the page on screen: their route
|
|
39217
|
+
* matches and their params getter returns something — the running ones, not
|
|
39218
|
+
* every matching one, so an action bound to every page that asks only when a
|
|
39219
|
+
* popup is open over it counts while it is open. Read to rerun what the page
|
|
39220
|
+
* reads (the app back from the background), and by `keepPageOnScreen`.
|
|
39221
|
+
* The set is handed out again only when its members change.
|
|
39222
|
+
*
|
|
39223
|
+
* A page's code is declared as a route action too; `action.meta.verb` says
|
|
39224
|
+
* which ones read data.
|
|
39225
|
+
*
|
|
39226
|
+
* @type {import("@preact/signals").ReadonlySignal<Set<object>>}
|
|
39227
|
+
*/
|
|
39228
|
+
const activeRouteActionsSignal = computed(() => {
|
|
39229
|
+
const active = new Set();
|
|
39230
|
+
for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
|
|
39231
|
+
if (askingSignal.value) {
|
|
39232
|
+
active.add(routeAction);
|
|
39233
|
+
}
|
|
39234
|
+
}
|
|
39235
|
+
if (haveSameMembers(active, activeRouteActionsPrevious)) {
|
|
39236
|
+
return activeRouteActionsPrevious;
|
|
39237
|
+
}
|
|
39238
|
+
activeRouteActionsPrevious = active;
|
|
39239
|
+
return active;
|
|
39240
|
+
});
|
|
39241
|
+
|
|
38918
39242
|
/**
|
|
38919
39243
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
38920
39244
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
@@ -38972,6 +39296,13 @@ const routeAction = (
|
|
|
38972
39296
|
return params;
|
|
38973
39297
|
};
|
|
38974
39298
|
const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
|
|
39299
|
+
routeActionEntriesSignal.value = [
|
|
39300
|
+
...routeActionEntriesSignal.peek(),
|
|
39301
|
+
{
|
|
39302
|
+
routeAction: actionBoundToRoute,
|
|
39303
|
+
askingSignal: computed(() => Boolean(readParamsAsked())),
|
|
39304
|
+
},
|
|
39305
|
+
];
|
|
38975
39306
|
for (const route of routes) {
|
|
38976
39307
|
// Asked of the route's params as they are, like the effect asks them.
|
|
38977
39308
|
registerRouteReload(route, () =>
|
|
@@ -39055,6 +39386,343 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
39055
39386
|
return anyMatchingSignal;
|
|
39056
39387
|
};
|
|
39057
39388
|
|
|
39389
|
+
const haveSameMembers = (setA, setB) => {
|
|
39390
|
+
if (setA.size !== setB.size) {
|
|
39391
|
+
return false;
|
|
39392
|
+
}
|
|
39393
|
+
for (const value of setA) {
|
|
39394
|
+
if (!setB.has(value)) {
|
|
39395
|
+
return false;
|
|
39396
|
+
}
|
|
39397
|
+
}
|
|
39398
|
+
return true;
|
|
39399
|
+
};
|
|
39400
|
+
|
|
39401
|
+
/*
|
|
39402
|
+
* The page on screen, kept for the next document: what its reads answered,
|
|
39403
|
+
* written into a signal the app hands over, and drawn again by the same reads
|
|
39404
|
+
* in a document opening on the same page (a reload, a tab the system
|
|
39405
|
+
* discarded). The read side is state/rest/kept_reads.js; this is what decides
|
|
39406
|
+
* which reads are the page, and when the slot is written.
|
|
39407
|
+
*
|
|
39408
|
+
* The unit is the page, not the resource. One resource is read by several
|
|
39409
|
+
* screens — a search, someone's games, a game opened from anywhere — and kept
|
|
39410
|
+
* per resource it would keep every row ever opened. The page's reads are what
|
|
39411
|
+
* navi already runs for it: the route actions asking something for the page
|
|
39412
|
+
* (activeRouteActionsSignal) and the compositions the lists on screen read
|
|
39413
|
+
* (shownRangeReadersSignal). A search typed on the page is neither, and is not
|
|
39414
|
+
* kept.
|
|
39415
|
+
*
|
|
39416
|
+
* A read is written once it has answered in this document. Before that it
|
|
39417
|
+
* keeps what the previous document kept for it, never what it shows meanwhile:
|
|
39418
|
+
* an answer handed over from other params (a list standing in for the next
|
|
39419
|
+
* one) says nothing about these.
|
|
39420
|
+
*
|
|
39421
|
+
* The write waits for the page to settle and is flushed when the document is
|
|
39422
|
+
* hidden. A navigation goes through pages half there — the route matched, its
|
|
39423
|
+
* list not mounted yet, the page left still drawn under a transition — and
|
|
39424
|
+
* each would otherwise be written out in full. Hidden is the moment the system
|
|
39425
|
+
* may discard the document, so nothing is lost by waiting until then.
|
|
39426
|
+
*
|
|
39427
|
+
* The signal turning `undefined` from outside (sign-out) forgets the page, and
|
|
39428
|
+
* nothing is written until a read lands again: written at once, the page still
|
|
39429
|
+
* on screen would put back the account just left.
|
|
39430
|
+
*/
|
|
39431
|
+
|
|
39432
|
+
const WRITE_DELAY = 1000;
|
|
39433
|
+
const NOTHING_PENDING = {};
|
|
39434
|
+
let stopKeepingCurrent = null;
|
|
39435
|
+
|
|
39436
|
+
/**
|
|
39437
|
+
* Keeps what the page on screen reads, so that a new document opening on it —
|
|
39438
|
+
* a reload, a pull-to-refresh, a tab the system discarded in the background —
|
|
39439
|
+
* draws it again while its reads go out, rather than skeletons.
|
|
39440
|
+
*
|
|
39441
|
+
* What the page reads is what navi runs for it: the route actions asking
|
|
39442
|
+
* something for it (see `activeRouteActionsSignal`) whose action is a resource
|
|
39443
|
+
* `GET` or `GET_MANY`, and the `GET_RANGE` compositions the `<List.Items>` on
|
|
39444
|
+
* screen read, around the window they draw. A navigation replaces the slot with
|
|
39445
|
+
* the next page's reads, and the page left is dropped. In the next document,
|
|
39446
|
+
* the first run of the same read with the same params draws the kept answer as
|
|
39447
|
+
* its provisional value: `data` set while `loading` is `true`, the answer
|
|
39448
|
+
* replacing it. Under a network policy answering reads from the store, the
|
|
39449
|
+
* kept answer answers the read. The copy follows the store: a `PUT` on a row
|
|
39450
|
+
* rewrites it.
|
|
39451
|
+
*
|
|
39452
|
+
* Call it before the routes start: the first runs are the ones that look.
|
|
39453
|
+
*
|
|
39454
|
+
* @param {object} options
|
|
39455
|
+
* @param {import("@preact/signals").Signal} options.signal - where the slot is
|
|
39456
|
+
* written: a `stateSignal` with `persists: true` and `type: "object"`, its
|
|
39457
|
+
* `id` holding whatever makes a kept page unusable (the deployed version).
|
|
39458
|
+
* Writing `undefined` into it (sign-out) forgets the page, and nothing is
|
|
39459
|
+
* written again until a read lands.
|
|
39460
|
+
* @param {() => boolean} [options.when] - read whenever the slot is about to be
|
|
39461
|
+
* read or written: `false` reads nothing, writes nothing, and empties the
|
|
39462
|
+
* signal (a tab viewing as someone else). A function, so it can read a signal.
|
|
39463
|
+
* @param {object[]} [options.always] - routes whose reads stay kept once the
|
|
39464
|
+
* page is left, until it is visited again: the page an app opens on when it
|
|
39465
|
+
* is launched rather than reloaded.
|
|
39466
|
+
* @returns {() => void} stops keeping the page.
|
|
39467
|
+
* @see docs/resource.md — what is kept and what is not, what stays the app's
|
|
39468
|
+
*/
|
|
39469
|
+
const keepPageOnScreen = ({
|
|
39470
|
+
signal: keptSignal,
|
|
39471
|
+
when,
|
|
39472
|
+
always = [],
|
|
39473
|
+
} = {}) => {
|
|
39474
|
+
if (!isSignal(keptSignal)) {
|
|
39475
|
+
throw new TypeError(
|
|
39476
|
+
`keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
|
|
39477
|
+
);
|
|
39478
|
+
}
|
|
39479
|
+
if (stopKeepingCurrent) {
|
|
39480
|
+
stopKeepingCurrent();
|
|
39481
|
+
}
|
|
39482
|
+
const isAllowed = () => {
|
|
39483
|
+
return when ? Boolean(when()) : true;
|
|
39484
|
+
};
|
|
39485
|
+
const alwaysMatchingSignal = anyMatchingRouteSignal(always);
|
|
39486
|
+
|
|
39487
|
+
// What the signal held when it was read: the previous document's page.
|
|
39488
|
+
let loadedEntryMap = new Map();
|
|
39489
|
+
let loadedAlwaysKeys = [];
|
|
39490
|
+
// Which reads answered in this document (since the last forget): only
|
|
39491
|
+
// those write what they show.
|
|
39492
|
+
let answeredActionWeakSet = new WeakSet();
|
|
39493
|
+
let answeredRangeKeySet = new Set();
|
|
39494
|
+
// The state each action was last seen in, and the last answer seen of each
|
|
39495
|
+
// range reader: a read lands when they move, and not because the slot looks
|
|
39496
|
+
// again at something that had answered before a forget.
|
|
39497
|
+
const stateSeenWeakMap = new WeakMap();
|
|
39498
|
+
const answeredAtSeenWeakMap = new WeakMap();
|
|
39499
|
+
// Forgotten: nothing is written until a read lands.
|
|
39500
|
+
let paused = false;
|
|
39501
|
+
// The reads of the always-kept page, as they were the last time it was on
|
|
39502
|
+
// screen in this document; null until then.
|
|
39503
|
+
let alwaysReads = null;
|
|
39504
|
+
|
|
39505
|
+
const forget = () => {
|
|
39506
|
+
loadedEntryMap = new Map();
|
|
39507
|
+
loadedAlwaysKeys = [];
|
|
39508
|
+
answeredActionWeakSet = new WeakSet();
|
|
39509
|
+
answeredRangeKeySet = new Set();
|
|
39510
|
+
alwaysReads = null;
|
|
39511
|
+
paused = true;
|
|
39512
|
+
withdrawKeptReads();
|
|
39513
|
+
};
|
|
39514
|
+
|
|
39515
|
+
let lastWritten;
|
|
39516
|
+
let pendingWrite = NOTHING_PENDING;
|
|
39517
|
+
let writeTimeout;
|
|
39518
|
+
const flush = () => {
|
|
39519
|
+
clearTimeout(writeTimeout);
|
|
39520
|
+
if (pendingWrite === NOTHING_PENDING) {
|
|
39521
|
+
return;
|
|
39522
|
+
}
|
|
39523
|
+
const value = pendingWrite;
|
|
39524
|
+
pendingWrite = NOTHING_PENDING;
|
|
39525
|
+
if (compareTwoJsValues(value, lastWritten)) {
|
|
39526
|
+
return;
|
|
39527
|
+
}
|
|
39528
|
+
lastWritten = value;
|
|
39529
|
+
keptSignal.value = value;
|
|
39530
|
+
};
|
|
39531
|
+
const scheduleWrite = (value) => {
|
|
39532
|
+
pendingWrite = value;
|
|
39533
|
+
clearTimeout(writeTimeout);
|
|
39534
|
+
writeTimeout = setTimeout(flush, WRITE_DELAY);
|
|
39535
|
+
};
|
|
39536
|
+
const onVisibilityChange = () => {
|
|
39537
|
+
if (document.visibilityState === "hidden") {
|
|
39538
|
+
flush();
|
|
39539
|
+
}
|
|
39540
|
+
};
|
|
39541
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
39542
|
+
window.addEventListener("pagehide", flush);
|
|
39543
|
+
|
|
39544
|
+
// Runs at once with what the signal already holds (the slot read from
|
|
39545
|
+
// storage), then on every write — the slot's own ones are recognized and
|
|
39546
|
+
// left alone.
|
|
39547
|
+
const unsubscribe = keptSignal.subscribe((value) => {
|
|
39548
|
+
if (value === lastWritten) {
|
|
39549
|
+
return;
|
|
39550
|
+
}
|
|
39551
|
+
lastWritten = value;
|
|
39552
|
+
pendingWrite = NOTHING_PENDING;
|
|
39553
|
+
clearTimeout(writeTimeout);
|
|
39554
|
+
if (
|
|
39555
|
+
value === undefined ||
|
|
39556
|
+
value === null ||
|
|
39557
|
+
typeof value !== "object" ||
|
|
39558
|
+
!isAllowed()
|
|
39559
|
+
) {
|
|
39560
|
+
forget();
|
|
39561
|
+
return;
|
|
39562
|
+
}
|
|
39563
|
+
const entries =
|
|
39564
|
+
value.entries && typeof value.entries === "object" ? value.entries : {};
|
|
39565
|
+
loadedEntryMap = new Map(Object.entries(entries));
|
|
39566
|
+
loadedAlwaysKeys = Array.isArray(value.always)
|
|
39567
|
+
? value.always.filter((key) => loadedEntryMap.has(key))
|
|
39568
|
+
: [];
|
|
39569
|
+
alwaysReads = null;
|
|
39570
|
+
paused = false;
|
|
39571
|
+
offerKeptReads(loadedEntryMap);
|
|
39572
|
+
});
|
|
39573
|
+
|
|
39574
|
+
const actionReadWeakMap = new WeakMap();
|
|
39575
|
+
const actionReadOf = (action) => {
|
|
39576
|
+
let read = actionReadWeakMap.get(action);
|
|
39577
|
+
if (read) {
|
|
39578
|
+
return read;
|
|
39579
|
+
}
|
|
39580
|
+
read = () => {
|
|
39581
|
+
const state = action.runningStateSignal.value;
|
|
39582
|
+
const stateSeen = stateSeenWeakMap.get(action);
|
|
39583
|
+
stateSeenWeakMap.set(action, state);
|
|
39584
|
+
const landed = state === COMPLETED && stateSeen !== COMPLETED;
|
|
39585
|
+
if (landed) {
|
|
39586
|
+
answeredActionWeakSet.add(action);
|
|
39587
|
+
}
|
|
39588
|
+
return {
|
|
39589
|
+
key: getKeptReadKey(action.name, action.params),
|
|
39590
|
+
answered: answeredActionWeakSet.has(action),
|
|
39591
|
+
landed,
|
|
39592
|
+
readEntry: () => {
|
|
39593
|
+
const data = action.dataSignal.value;
|
|
39594
|
+
if (data === undefined || data === null) {
|
|
39595
|
+
return undefined;
|
|
39596
|
+
}
|
|
39597
|
+
return serializeData(data);
|
|
39598
|
+
},
|
|
39599
|
+
};
|
|
39600
|
+
};
|
|
39601
|
+
actionReadWeakMap.set(action, read);
|
|
39602
|
+
return read;
|
|
39603
|
+
};
|
|
39604
|
+
const rangeReadWeakMap = new WeakMap();
|
|
39605
|
+
const rangeReadOf = (reader) => {
|
|
39606
|
+
let read = rangeReadWeakMap.get(reader);
|
|
39607
|
+
if (read) {
|
|
39608
|
+
return read;
|
|
39609
|
+
}
|
|
39610
|
+
read = () => {
|
|
39611
|
+
const { key, answeredAt, readEntry } = reader.readKept();
|
|
39612
|
+
const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
|
|
39613
|
+
answeredAtSeenWeakMap.set(reader, answeredAt);
|
|
39614
|
+
const landed = answeredAt > answeredAtSeen;
|
|
39615
|
+
if (landed) {
|
|
39616
|
+
answeredRangeKeySet.add(key);
|
|
39617
|
+
}
|
|
39618
|
+
return {
|
|
39619
|
+
key,
|
|
39620
|
+
answered: answeredRangeKeySet.has(key),
|
|
39621
|
+
landed,
|
|
39622
|
+
readEntry,
|
|
39623
|
+
};
|
|
39624
|
+
};
|
|
39625
|
+
rangeReadWeakMap.set(reader, read);
|
|
39626
|
+
return read;
|
|
39627
|
+
};
|
|
39628
|
+
const readsOnScreen = () => {
|
|
39629
|
+
const reads = [];
|
|
39630
|
+
for (const routeAction of activeRouteActionsSignal.value) {
|
|
39631
|
+
// The instance is what is kept, so that a page left keeps what it read.
|
|
39632
|
+
// The binding's callSource names that instance and is rewritten once the
|
|
39633
|
+
// binding has moved to another one: read, it brings the slot along.
|
|
39634
|
+
// eslint-disable-next-line no-unused-expressions
|
|
39635
|
+
routeAction.callSource;
|
|
39636
|
+
const action = getActionPrivateProperties(routeAction).currentAction;
|
|
39637
|
+
if (action.params === NO_PARAMS || !isKeepableRead(action)) {
|
|
39638
|
+
continue;
|
|
39639
|
+
}
|
|
39640
|
+
reads.push(actionReadOf(action));
|
|
39641
|
+
}
|
|
39642
|
+
for (const reader of shownRangeReadersSignal.value.keys()) {
|
|
39643
|
+
reads.push(rangeReadOf(reader));
|
|
39644
|
+
}
|
|
39645
|
+
return reads;
|
|
39646
|
+
};
|
|
39647
|
+
|
|
39648
|
+
const stopEffect = effect(() => {
|
|
39649
|
+
const allowed = isAllowed();
|
|
39650
|
+
const routed = routedSignal.value;
|
|
39651
|
+
const alwaysMatching = alwaysMatchingSignal.value;
|
|
39652
|
+
if (!allowed) {
|
|
39653
|
+
forget();
|
|
39654
|
+
pendingWrite = undefined;
|
|
39655
|
+
flush();
|
|
39656
|
+
return;
|
|
39657
|
+
}
|
|
39658
|
+
if (!routed) {
|
|
39659
|
+
// No page yet: the slot is what the first reads will draw.
|
|
39660
|
+
return;
|
|
39661
|
+
}
|
|
39662
|
+
const screenReads = readsOnScreen();
|
|
39663
|
+
if (alwaysMatching) {
|
|
39664
|
+
alwaysReads = screenReads;
|
|
39665
|
+
}
|
|
39666
|
+
const entries = {};
|
|
39667
|
+
let landed = false;
|
|
39668
|
+
const collect = (reads) => {
|
|
39669
|
+
const keys = [];
|
|
39670
|
+
for (const read of reads) {
|
|
39671
|
+
const result = read();
|
|
39672
|
+
if (result.landed) {
|
|
39673
|
+
landed = true;
|
|
39674
|
+
}
|
|
39675
|
+
const entry = result.answered
|
|
39676
|
+
? result.readEntry()
|
|
39677
|
+
: loadedEntryMap.get(result.key);
|
|
39678
|
+
if (entry === undefined) {
|
|
39679
|
+
continue;
|
|
39680
|
+
}
|
|
39681
|
+
entries[result.key] = entry;
|
|
39682
|
+
keys.push(result.key);
|
|
39683
|
+
}
|
|
39684
|
+
return keys;
|
|
39685
|
+
};
|
|
39686
|
+
const screenKeys = collect(screenReads);
|
|
39687
|
+
let alwaysKeys;
|
|
39688
|
+
if (alwaysReads === screenReads) {
|
|
39689
|
+
alwaysKeys = screenKeys;
|
|
39690
|
+
} else if (alwaysReads) {
|
|
39691
|
+
alwaysKeys = collect(alwaysReads);
|
|
39692
|
+
} else {
|
|
39693
|
+
alwaysKeys = loadedAlwaysKeys;
|
|
39694
|
+
for (const key of alwaysKeys) {
|
|
39695
|
+
entries[key] = loadedEntryMap.get(key);
|
|
39696
|
+
}
|
|
39697
|
+
}
|
|
39698
|
+
if (paused) {
|
|
39699
|
+
if (!landed) {
|
|
39700
|
+
return;
|
|
39701
|
+
}
|
|
39702
|
+
paused = false;
|
|
39703
|
+
}
|
|
39704
|
+
if (screenKeys.length === 0 && alwaysKeys.length === 0) {
|
|
39705
|
+
scheduleWrite(undefined);
|
|
39706
|
+
return;
|
|
39707
|
+
}
|
|
39708
|
+
scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
|
|
39709
|
+
});
|
|
39710
|
+
|
|
39711
|
+
const stop = () => {
|
|
39712
|
+
flush();
|
|
39713
|
+
stopEffect();
|
|
39714
|
+
unsubscribe();
|
|
39715
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
39716
|
+
window.removeEventListener("pagehide", flush);
|
|
39717
|
+
withdrawKeptReads();
|
|
39718
|
+
if (stopKeepingCurrent === stop) {
|
|
39719
|
+
stopKeepingCurrent = null;
|
|
39720
|
+
}
|
|
39721
|
+
};
|
|
39722
|
+
stopKeepingCurrent = stop;
|
|
39723
|
+
return stop;
|
|
39724
|
+
};
|
|
39725
|
+
|
|
39058
39726
|
/**
|
|
39059
39727
|
* A popup that is the anchor, brought to the front.
|
|
39060
39728
|
*
|
|
@@ -66417,7 +67085,7 @@ const css$E = /* css */`
|
|
|
66417
67085
|
/* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
|
|
66418
67086
|
by default until .show()/.showModal() adds [open]) — needs its own
|
|
66419
67087
|
starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
|
|
66420
|
-
own backdropProps (recomputed from openController.
|
|
67088
|
+
own backdropProps (recomputed from openController.openedInDom on every
|
|
66421
67089
|
render, present from the very first one), then toggled by plain
|
|
66422
67090
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
66423
67091
|
display override — removing the attribute just lets this rule stop
|
|
@@ -68077,7 +68745,7 @@ const useDialogProps = props => {
|
|
|
68077
68745
|
"navi-out-of-flow": "",
|
|
68078
68746
|
"baseClassName": "navi_dialog_backdrop",
|
|
68079
68747
|
"aria-hidden": "true",
|
|
68080
|
-
// Recomputed fresh on every render from openController.
|
|
68748
|
+
// Recomputed fresh on every render from openController.openedInDom (not
|
|
68081
68749
|
// driven through a mount-time layout effect, unlike this file's own
|
|
68082
68750
|
// imperative open/close toggling below) — present in the DOM
|
|
68083
68751
|
// synchronously from the very first commit, matching this file's own
|
|
@@ -68087,7 +68755,7 @@ const useDialogProps = props => {
|
|
|
68087
68755
|
// needs — see popover.jsx's own identical prop for the full reasoning.
|
|
68088
68756
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
68089
68757
|
// Present from this very first render (recomputed fresh on every one
|
|
68090
|
-
// from openController.
|
|
68758
|
+
// from openController.openedInDom, not a frozen mount-time constant) so
|
|
68091
68759
|
// there's no gap for the browser to ever paint this plain-div backdrop
|
|
68092
68760
|
// visible before anything has actually opened it — see popover.jsx's
|
|
68093
68761
|
// own identical prop for the full reasoning, and this file's own CSS
|
|
@@ -68107,7 +68775,7 @@ const useDialogProps = props => {
|
|
|
68107
68775
|
// (kept once, not repeated here).
|
|
68108
68776
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
68109
68777
|
// Present from the very first render (recomputed fresh from
|
|
68110
|
-
// openController.
|
|
68778
|
+
// openController.openedInDom every time, not a frozen mount-time constant —
|
|
68111
68779
|
// see popover.jsx's own identical prop for the full reasoning) so a
|
|
68112
68780
|
// consumer whose own CSS also sets display (e.g. Popup's flex prop)
|
|
68113
68781
|
// can't silently defeat showModal()/close()'s native open/close — see
|
|
@@ -68510,7 +69178,7 @@ const css$D = /* css */`
|
|
|
68510
69178
|
without this it would flash visible for one frame on mount, before
|
|
68511
69179
|
openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
|
|
68512
69180
|
set from usePopoverProps' own contentProps (recomputed from
|
|
68513
|
-
openController.
|
|
69181
|
+
openController.openedInDom on every render, present from the very first
|
|
68514
69182
|
one — see there for why), then toggled by plain
|
|
68515
69183
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
68516
69184
|
display override: removing the attribute just lets this rule stop
|
|
@@ -69875,7 +70543,7 @@ const usePopoverProps = props => {
|
|
|
69875
70543
|
"navi-out-of-flow": "",
|
|
69876
70544
|
"baseClassName": "navi_popover_backdrop",
|
|
69877
70545
|
"aria-hidden": "true",
|
|
69878
|
-
// Recomputed fresh on every render from openController.
|
|
70546
|
+
// Recomputed fresh on every render from openController.openedInDom (not a
|
|
69879
70547
|
// frozen mount-time constant, same pattern as navi-hidden just below)
|
|
69880
70548
|
// rather than driven through a mount-time layout effect — its own
|
|
69881
70549
|
// actual open/close toggling is still done entirely imperatively
|
|
@@ -69950,7 +70618,7 @@ const usePopoverProps = props => {
|
|
|
69950
70618
|
// starting-hidden default) — present from this very first render so
|
|
69951
70619
|
// there's no gap for the browser to ever paint it visible before
|
|
69952
70620
|
// anything has actually opened it. Recomputed fresh on every render from
|
|
69953
|
-
// openController.
|
|
70621
|
+
// openController.openedInDom (not a frozen mount-time constant) — Preact
|
|
69954
70622
|
// only touches the DOM for a prop whose value actually changed since
|
|
69955
70623
|
// the last render, so as long as this always reflects the *current*
|
|
69956
70624
|
// truth, it never fights the imperative
|
|
@@ -74390,7 +75058,6 @@ const ListUI = props => {
|
|
|
74390
75058
|
// What the runs ask for and stand for, in items: a source paginates in items,
|
|
74391
75059
|
// and asks for the first picture's items and the rest in one round trip.
|
|
74392
75060
|
listItems.pageSize = PAGE_SIZE_DEFAULT;
|
|
74393
|
-
listItems.scrolled = scrolled ?? defaultScrolled;
|
|
74394
75061
|
const {
|
|
74395
75062
|
virtualItemSizeSignal,
|
|
74396
75063
|
renderWindow,
|
|
@@ -74769,6 +75436,45 @@ const useListScrollSync = ({
|
|
|
74769
75436
|
columns
|
|
74770
75437
|
}) => {
|
|
74771
75438
|
const debugScroll = useDebugScroll();
|
|
75439
|
+
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
75440
|
+
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
75441
|
+
// the caller moving the list, so the hold is armed again — that is what makes
|
|
75442
|
+
// it controlled. A `defaultScrolled` that changes is where the list opens,
|
|
75443
|
+
// read again from something that answered again (a count fresher than the
|
|
75444
|
+
// one kept from the previous visit): followed for as long as nobody has
|
|
75445
|
+
// moved the list, and compared by value, since it is said at every render.
|
|
75446
|
+
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
75447
|
+
// list opens where it opens.
|
|
75448
|
+
const startPlaceRef = useRef(null);
|
|
75449
|
+
if (startPlaceRef.current === null) {
|
|
75450
|
+
startPlaceRef.current = {
|
|
75451
|
+
userTookOver: false,
|
|
75452
|
+
wanted: scrolled ?? defaultScrolled,
|
|
75453
|
+
scrolled,
|
|
75454
|
+
defaultScrolled,
|
|
75455
|
+
way: null,
|
|
75456
|
+
arrived: false
|
|
75457
|
+
};
|
|
75458
|
+
} else {
|
|
75459
|
+
const place = startPlaceRef.current;
|
|
75460
|
+
if (place.scrolled !== scrolled) {
|
|
75461
|
+
place.scrolled = scrolled;
|
|
75462
|
+
place.defaultScrolled = defaultScrolled;
|
|
75463
|
+
place.wanted = scrolled ?? defaultScrolled;
|
|
75464
|
+
place.userTookOver = false;
|
|
75465
|
+
} else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
|
|
75466
|
+
place.defaultScrolled = defaultScrolled;
|
|
75467
|
+
if (!place.userTookOver) {
|
|
75468
|
+
place.wanted = defaultScrolled;
|
|
75469
|
+
// Placed again from where it stands: the way to the previous place
|
|
75470
|
+
// says nothing about whose the next scrolls are.
|
|
75471
|
+
place.way = null;
|
|
75472
|
+
place.arrived = false;
|
|
75473
|
+
}
|
|
75474
|
+
}
|
|
75475
|
+
}
|
|
75476
|
+
const scrolledWanted = startPlaceRef.current.wanted;
|
|
75477
|
+
listItems.scrolled = scrolledWanted;
|
|
74772
75478
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
74773
75479
|
// state: the render moves it where the list is held (holdWindow) without a
|
|
74774
75480
|
// commit of its own.
|
|
@@ -74783,7 +75489,7 @@ const useListScrollSync = ({
|
|
|
74783
75489
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
74784
75490
|
inLines: Boolean(columns),
|
|
74785
75491
|
windowLeavesItemsOut,
|
|
74786
|
-
scrolledWanted
|
|
75492
|
+
scrolledWanted,
|
|
74787
75493
|
// The fillers hold the room of items above the screen at this size: a
|
|
74788
75494
|
// size that changes moves what is on screen, like items landing above it.
|
|
74789
75495
|
beforeSizeChange: () => listItems.captureAnchor()
|
|
@@ -75116,25 +75822,7 @@ const useListScrollSync = ({
|
|
|
75116
75822
|
const newEnd = newStart + windowSize;
|
|
75117
75823
|
updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
|
|
75118
75824
|
};
|
|
75119
|
-
|
|
75120
|
-
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
75121
|
-
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
75122
|
-
// the caller moving the list, so the hold is armed again — that is what makes
|
|
75123
|
-
// it controlled.
|
|
75124
|
-
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
75125
|
-
// list opens where it opens.
|
|
75126
|
-
const scrolledWanted = scrolled ?? defaultScrolled;
|
|
75127
75825
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
75128
|
-
const startPlaceRef = useRef({
|
|
75129
|
-
userTookOver: false,
|
|
75130
|
-
wanted: scrolledWanted,
|
|
75131
|
-
way: null,
|
|
75132
|
-
arrived: false
|
|
75133
|
-
});
|
|
75134
|
-
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
75135
|
-
startPlaceRef.current.wanted = scrolledWanted;
|
|
75136
|
-
startPlaceRef.current.userTookOver = false;
|
|
75137
|
-
}
|
|
75138
75826
|
// Only one thing owns the scroll at a time: while the list is holding itself
|
|
75139
75827
|
// somewhere, the anchoring stays out of it (holding an item still is precisely
|
|
75140
75828
|
// not being at the end anymore once what is above it shrinks).
|
|
@@ -78962,6 +79650,17 @@ const useItemStore = ({
|
|
|
78962
79650
|
}
|
|
78963
79651
|
const pages = pagesRef.current;
|
|
78964
79652
|
const [, setPageVersion] = useState(0);
|
|
79653
|
+
// A list on screen is part of what the page reads: the composition it reads
|
|
79654
|
+
// is kept with the page for the next document (see keepPageOnScreen).
|
|
79655
|
+
useLayoutEffect(() => {
|
|
79656
|
+
if (inMemory || typeof itemsAction !== "function") {
|
|
79657
|
+
return undefined;
|
|
79658
|
+
}
|
|
79659
|
+
if (!itemsAction.isRangeReader) {
|
|
79660
|
+
return undefined;
|
|
79661
|
+
}
|
|
79662
|
+
return showRangeReader(itemsAction);
|
|
79663
|
+
}, [inMemory, itemsAction]);
|
|
78965
79664
|
// The ranks whose item is from before: the run came back to the screen, or
|
|
78966
79665
|
// the source said the collection moved. They stay drawn until a page confirms
|
|
78967
79666
|
// or replaces them, rank by rank. A page covers what the window framed when it
|
|
@@ -79095,6 +79794,7 @@ const useItemStore = ({
|
|
|
79095
79794
|
}
|
|
79096
79795
|
}
|
|
79097
79796
|
if (typeof itemsAction === "function" && itemsAction.trimComposition) {
|
|
79797
|
+
itemsAction.frameComposition(windowFrom, windowTo);
|
|
79098
79798
|
itemsAction.trimComposition(keepFrom, keepTo, budget);
|
|
79099
79799
|
}
|
|
79100
79800
|
},
|
|
@@ -79664,6 +80364,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
79664
80364
|
* room has to be restated as a number by whoever asks. The `index` and
|
|
79665
80365
|
* `visibleCount` it also hands out say where to aim before the item is found,
|
|
79666
80366
|
* and how many items to draw before the first paint (see `renderBudget`).
|
|
80367
|
+
* A value that changes before anyone has moved the list is followed (a place
|
|
80368
|
+
* read from a count that answered again): it is compared by value, so it can
|
|
80369
|
+
* be computed at every render.
|
|
79667
80370
|
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
79668
80371
|
* The same, but held: the list goes back there every time this changes, even
|
|
79669
80372
|
* after the user has scrolled — the caller owns where the list is (see
|
|
@@ -95758,5 +96461,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
95758
96461
|
})
|
|
95759
96462
|
});
|
|
95760
96463
|
|
|
95761
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
96464
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
95762
96465
|
//# sourceMappingURL=jsenv_navi.js.map
|