@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/jsenv_navi.js
CHANGED
|
@@ -3548,8 +3548,11 @@ const actionWeakMap = new WeakMap();
|
|
|
3548
3548
|
* than a first load (data_states.md). Called at the start of every run whose
|
|
3549
3549
|
* value is undefined; the answer replaces it as usual, and `undefined` means
|
|
3550
3550
|
* "nothing known". A resource `GET` uses it to draw the row its store holds.
|
|
3551
|
-
*
|
|
3552
|
-
*
|
|
3551
|
+
* @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
|
|
3552
|
+
* to new params starts the new instance without the previous params' answer
|
|
3553
|
+
* (see createActionProxyFromSignal). For an answer that would say something
|
|
3554
|
+
* false about the new params (a `GET` drawing another row), not for one that
|
|
3555
|
+
* stands in while the next one comes (a list of search results).
|
|
3553
3556
|
* @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
|
|
3554
3557
|
* answer stays good once it has landed. Without it an answer lives exactly as
|
|
3555
3558
|
* long as something references it: the screen that asked goes away, and
|
|
@@ -3592,6 +3595,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3592
3595
|
resultToValue,
|
|
3593
3596
|
valueToData,
|
|
3594
3597
|
provisionalValue,
|
|
3598
|
+
inheritData = true,
|
|
3595
3599
|
dataDefault,
|
|
3596
3600
|
data = dataDefault,
|
|
3597
3601
|
|
|
@@ -4255,6 +4259,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4255
4259
|
const privateProperties = {
|
|
4256
4260
|
valueInitial,
|
|
4257
4261
|
provisionalValue,
|
|
4262
|
+
inheritData,
|
|
4258
4263
|
|
|
4259
4264
|
performRun,
|
|
4260
4265
|
performReset,
|
|
@@ -4287,7 +4292,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4287
4292
|
* a list of filters changes, providing real-time results without user interaction.
|
|
4288
4293
|
* @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
|
|
4289
4294
|
* By default (true), the proxy carries over the previous target's value and error into the new action —
|
|
4290
|
-
* unless the action declares
|
|
4295
|
+
* unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
|
|
4291
4296
|
* This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
|
|
4292
4297
|
* new action completes, not when it starts loading. Code that needs to distinguish loading state can still
|
|
4293
4298
|
* check `action.runningState`, while code that just reads `action.data` always sees the most recent
|
|
@@ -4334,12 +4339,12 @@ const createActionProxyFromSignal = (
|
|
|
4334
4339
|
let currentActionPrivateProperties = getActionPrivateProperties(action);
|
|
4335
4340
|
let actionTargetPreviousWeakRef = null;
|
|
4336
4341
|
|
|
4337
|
-
// An action
|
|
4338
|
-
//
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4342
|
+
// An action whose answer for other params would be false for these (a
|
|
4343
|
+
// resource GET: another row) says so itself, and is not handed it. What it
|
|
4344
|
+
// does not know stays unknown: a skeleton, or its provisionalValue, never a
|
|
4345
|
+
// different row.
|
|
4341
4346
|
const inheritsAnswer =
|
|
4342
|
-
inheritData &&
|
|
4347
|
+
inheritData && getActionPrivateProperties(action).inheritData;
|
|
4343
4348
|
const createTarget = (params) => {
|
|
4344
4349
|
if (inheritsAnswer) {
|
|
4345
4350
|
const previousActionTarget = actionTargetPreviousWeakRef?.deref();
|
|
@@ -10242,6 +10247,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
|
|
|
10242
10247
|
|
|
10243
10248
|
let setupRoutesCalled = false;
|
|
10244
10249
|
let activeRouteSet = null;
|
|
10250
|
+
// True once the routes have been matched against an address. Before that, no
|
|
10251
|
+
// route matching says nothing about the page: nothing has been asked yet.
|
|
10252
|
+
const routedSignal = signal(false);
|
|
10245
10253
|
|
|
10246
10254
|
const routePreloadMap = new WeakMap();
|
|
10247
10255
|
const registerRoutePreload = (route, preload) => {
|
|
@@ -10666,6 +10674,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10666
10674
|
}
|
|
10667
10675
|
// Reset flag after URL -> Signal synchronization is complete
|
|
10668
10676
|
isUpdatingRoutesFromUrl = false;
|
|
10677
|
+
routedSignal.value = true;
|
|
10669
10678
|
Object.assign(returnValue, { matchingRouteSet });
|
|
10670
10679
|
for (const [route, repairedParams] of repairedParamsByRoute) {
|
|
10671
10680
|
route.replaceParams(repairedParams, {
|
|
@@ -10715,6 +10724,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10715
10724
|
droppingRouteSet = null;
|
|
10716
10725
|
setupRoutesCalled = false;
|
|
10717
10726
|
activeRouteSet = null;
|
|
10727
|
+
routedSignal.value = false;
|
|
10718
10728
|
};
|
|
10719
10729
|
return {
|
|
10720
10730
|
updateRoutes,
|
|
@@ -15033,6 +15043,13 @@ const createOpenController = (
|
|
|
15033
15043
|
change();
|
|
15034
15044
|
};
|
|
15035
15045
|
const runChange = (change, { opened, event }) => {
|
|
15046
|
+
// What the controller answers about itself is the decision, written before
|
|
15047
|
+
// the change runs, whatever runs it. Whoever asks meanwhile reads it: the
|
|
15048
|
+
// render an opening forces (onOpen, mountContent) reads the open state
|
|
15049
|
+
// the caller holds, and an onOpen writing the popup's own `signal` has
|
|
15050
|
+
// useOpenPropsEffectOnOpenController asking for this very opening — a
|
|
15051
|
+
// second one, nested in the first, if `opened` still said closed.
|
|
15052
|
+
controller.opened = opened;
|
|
15036
15053
|
const applyChange = () => {
|
|
15037
15054
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
15038
15055
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
@@ -15048,12 +15065,8 @@ const createOpenController = (
|
|
|
15048
15065
|
applyChange();
|
|
15049
15066
|
return;
|
|
15050
15067
|
}
|
|
15051
|
-
//
|
|
15052
|
-
//
|
|
15053
|
-
// useOpenPropsEffectOnOpenController, which writes it back into the
|
|
15054
|
-
// caller's own signal), and a request arriving before the change lands
|
|
15055
|
-
// runs it first rather than reading a DOM that disagrees.
|
|
15056
|
-
controller.opened = opened;
|
|
15068
|
+
// A request arriving before the change lands runs it first rather than
|
|
15069
|
+
// reading a DOM that disagrees.
|
|
15057
15070
|
changeAwaitingTransition = applyChange;
|
|
15058
15071
|
transitionChange(
|
|
15059
15072
|
() => {
|
|
@@ -15066,7 +15079,6 @@ const createOpenController = (
|
|
|
15066
15079
|
};
|
|
15067
15080
|
|
|
15068
15081
|
const performClose = (closeEvent) => {
|
|
15069
|
-
controller.opened = false;
|
|
15070
15082
|
// Read before any close effect touches the DOM: closing a native <dialog>
|
|
15071
15083
|
// hands the focus back to whatever held it at showModal() time, so by the
|
|
15072
15084
|
// time the close cleanup runs, the popup's content has already lost the
|
|
@@ -15334,7 +15346,6 @@ const createOpenController = (
|
|
|
15334
15346
|
// told it opened right after (see popup_content_mount.js and
|
|
15335
15347
|
// use_displayed_layout_effect.js).
|
|
15336
15348
|
controller.openedInDom = true;
|
|
15337
|
-
controller.opened = true;
|
|
15338
15349
|
// Which press it opened during, so the release of that press is not
|
|
15339
15350
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
15340
15351
|
controller.pressCountAtOpen = pressCount;
|
|
@@ -15761,8 +15772,10 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15761
15772
|
return undefined;
|
|
15762
15773
|
}
|
|
15763
15774
|
// Skip when the controller is already in the desired state.
|
|
15764
|
-
// openController.opened
|
|
15765
|
-
//
|
|
15775
|
+
// openController.opened is what the controller decided, written before an
|
|
15776
|
+
// opening renders anything (see runChange), so it is the authoritative
|
|
15777
|
+
// check against feedback loops — the popup's own onOpen writing its
|
|
15778
|
+
// `signal` included.
|
|
15766
15779
|
if (open === openController.opened) {
|
|
15767
15780
|
return undefined;
|
|
15768
15781
|
}
|
|
@@ -25828,6 +25841,171 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
|
|
|
25828
25841
|
return childStore !== undefined && childStore === getChildStore(ownerId);
|
|
25829
25842
|
};
|
|
25830
25843
|
|
|
25844
|
+
/*
|
|
25845
|
+
* What a previous document kept of the page it was on (see keepPageOnScreen in
|
|
25846
|
+
* nav/page_kept.js), waiting for the reads that draw it again, and what those
|
|
25847
|
+
* reads are made of on the way out: rows with their relations inline.
|
|
25848
|
+
*
|
|
25849
|
+
* A kept read is found by the read's name and the params it was asked with
|
|
25850
|
+
* (getKeptReadKey). It is handed out once: the first run of the read draws it
|
|
25851
|
+
* while its request is out, and from then on the store holds the rows. Handed
|
|
25852
|
+
* out again, after a reset say, it would write the previous document's rows
|
|
25853
|
+
* over a fresher answer.
|
|
25854
|
+
*
|
|
25855
|
+
* This module is the half the resources see: `resource()` and the range reader
|
|
25856
|
+
* take from it, and say which of their reads can be kept at all. Which reads
|
|
25857
|
+
* are on the page on screen, and what the slot is written into, is
|
|
25858
|
+
* page_kept.js.
|
|
25859
|
+
*/
|
|
25860
|
+
|
|
25861
|
+
// key → what the read answered in the previous document, not handed out yet
|
|
25862
|
+
const offeredReadMap = new Map();
|
|
25863
|
+
const offerKeptReads = (entryMap) => {
|
|
25864
|
+
offeredReadMap.clear();
|
|
25865
|
+
for (const [key, entry] of entryMap) {
|
|
25866
|
+
offeredReadMap.set(key, entry);
|
|
25867
|
+
}
|
|
25868
|
+
};
|
|
25869
|
+
const withdrawKeptReads = () => {
|
|
25870
|
+
offeredReadMap.clear();
|
|
25871
|
+
};
|
|
25872
|
+
const takeKeptRead = (key) => {
|
|
25873
|
+
const entry = offeredReadMap.get(key);
|
|
25874
|
+
if (entry === undefined) {
|
|
25875
|
+
return undefined;
|
|
25876
|
+
}
|
|
25877
|
+
offeredReadMap.delete(key);
|
|
25878
|
+
return entry;
|
|
25879
|
+
};
|
|
25880
|
+
|
|
25881
|
+
// The root actions whose answer is rows of a store, written and read back the
|
|
25882
|
+
// same way: a resource's GET and GET_MANY. Anything else a route runs (a page's
|
|
25883
|
+
// code, a computation) has nothing a later document could draw.
|
|
25884
|
+
const keepableReadWeakSet = new WeakSet();
|
|
25885
|
+
const markKeepableRead = (rootAction) => {
|
|
25886
|
+
keepableReadWeakSet.add(rootAction);
|
|
25887
|
+
};
|
|
25888
|
+
const isKeepableRead = (action) => {
|
|
25889
|
+
const rootAction = action.rootAction || action;
|
|
25890
|
+
return keepableReadWeakSet.has(rootAction);
|
|
25891
|
+
};
|
|
25892
|
+
|
|
25893
|
+
// The range readers a <List.Items> on screen reads through, each with the number
|
|
25894
|
+
// of lists reading it: the reads of the page a route action does not cover.
|
|
25895
|
+
const shownRangeReadersSignal = signal(new Map());
|
|
25896
|
+
const showRangeReader = (reader) => {
|
|
25897
|
+
const shownMap = new Map(shownRangeReadersSignal.peek());
|
|
25898
|
+
shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
|
|
25899
|
+
shownRangeReadersSignal.value = shownMap;
|
|
25900
|
+
return () => {
|
|
25901
|
+
const shownMapNow = new Map(shownRangeReadersSignal.peek());
|
|
25902
|
+
const count = shownMapNow.get(reader);
|
|
25903
|
+
if (count === 1) {
|
|
25904
|
+
shownMapNow.delete(reader);
|
|
25905
|
+
} else {
|
|
25906
|
+
shownMapNow.set(reader, count - 1);
|
|
25907
|
+
}
|
|
25908
|
+
shownRangeReadersSignal.value = shownMapNow;
|
|
25909
|
+
};
|
|
25910
|
+
};
|
|
25911
|
+
|
|
25912
|
+
// A row already in the store is the fresher one: the kept copy only stands in
|
|
25913
|
+
// for rows nothing has answered yet. Its relations are written as they were
|
|
25914
|
+
// kept, which a real answer corrects like any other.
|
|
25915
|
+
const upsertKeptItem = (store, keptItem) => {
|
|
25916
|
+
const id = keptItem[store.idKey];
|
|
25917
|
+
if (id !== undefined) {
|
|
25918
|
+
const existing = store.select(id);
|
|
25919
|
+
if (existing) {
|
|
25920
|
+
return existing;
|
|
25921
|
+
}
|
|
25922
|
+
}
|
|
25923
|
+
return store.upsert(keptItem);
|
|
25924
|
+
};
|
|
25925
|
+
|
|
25926
|
+
const getKeptReadKey = (name, params) => {
|
|
25927
|
+
return `${name}|${getParamsKey(params)}`;
|
|
25928
|
+
};
|
|
25929
|
+
// `true` is what a route action without params is bound to (see routeAction),
|
|
25930
|
+
// and a key holding undefined says nothing a missing one does not.
|
|
25931
|
+
const getParamsKey = (params) => {
|
|
25932
|
+
if (params === undefined || params === NO_PARAMS || params === true) {
|
|
25933
|
+
return "";
|
|
25934
|
+
}
|
|
25935
|
+
if (isPlainParams(params)) {
|
|
25936
|
+
const keys = Object.keys(params).filter((key) => params[key] !== undefined);
|
|
25937
|
+
if (keys.length === 0) {
|
|
25938
|
+
return "";
|
|
25939
|
+
}
|
|
25940
|
+
}
|
|
25941
|
+
return stableStringify(params);
|
|
25942
|
+
};
|
|
25943
|
+
const isPlainParams = (value) => {
|
|
25944
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25945
|
+
};
|
|
25946
|
+
const stableStringify = (value) => {
|
|
25947
|
+
if (value === null || typeof value !== "object") {
|
|
25948
|
+
return JSON.stringify(value);
|
|
25949
|
+
}
|
|
25950
|
+
if (Array.isArray(value)) {
|
|
25951
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
25952
|
+
}
|
|
25953
|
+
const keys = Object.keys(value)
|
|
25954
|
+
.filter((key) => value[key] !== undefined)
|
|
25955
|
+
.sort();
|
|
25956
|
+
const parts = [];
|
|
25957
|
+
for (const key of keys) {
|
|
25958
|
+
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
25959
|
+
}
|
|
25960
|
+
return `{${parts.join(",")}}`;
|
|
25961
|
+
};
|
|
25962
|
+
|
|
25963
|
+
// What a read's data is made of, ready to be written: a row, or the rows of a
|
|
25964
|
+
// list, each with its relations inline.
|
|
25965
|
+
const serializeData = (data) => {
|
|
25966
|
+
if (Array.isArray(data)) {
|
|
25967
|
+
return data.map((item) => serializeItem(item, new Set()));
|
|
25968
|
+
}
|
|
25969
|
+
return serializeItem(data, new Set());
|
|
25970
|
+
};
|
|
25971
|
+
// The item with its relations inline: a relation value stands for what its
|
|
25972
|
+
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
25973
|
+
// twice on the same path is written as its id — the setters accept both.
|
|
25974
|
+
const serializeItem = (item, ancestorSet) => {
|
|
25975
|
+
ancestorSet.add(item);
|
|
25976
|
+
const serialized = {};
|
|
25977
|
+
for (const key of Object.keys(item)) {
|
|
25978
|
+
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
25979
|
+
}
|
|
25980
|
+
ancestorSet.delete(item);
|
|
25981
|
+
return serialized;
|
|
25982
|
+
};
|
|
25983
|
+
const serializeValue = (value, ancestorSet) => {
|
|
25984
|
+
if (value === null || typeof value !== "object") {
|
|
25985
|
+
return value;
|
|
25986
|
+
}
|
|
25987
|
+
if (Array.isArray(value)) {
|
|
25988
|
+
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
25989
|
+
}
|
|
25990
|
+
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
25991
|
+
if (objectSignal) {
|
|
25992
|
+
const held = objectSignal.value;
|
|
25993
|
+
if (held === undefined || held === null) {
|
|
25994
|
+
return null;
|
|
25995
|
+
}
|
|
25996
|
+
return serializeValue(held, ancestorSet);
|
|
25997
|
+
}
|
|
25998
|
+
if (typeof value.toJSON === "function") {
|
|
25999
|
+
return value;
|
|
26000
|
+
}
|
|
26001
|
+
if (ancestorSet.has(value)) {
|
|
26002
|
+
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26003
|
+
? value[SYMBOL_IDENTITY]
|
|
26004
|
+
: null;
|
|
26005
|
+
}
|
|
26006
|
+
return serializeItem(value, ancestorSet);
|
|
26007
|
+
};
|
|
26008
|
+
|
|
25831
26009
|
const paramScopeWeakSet = createIterableWeakSet();
|
|
25832
26010
|
let paramScopeIdCounter = 0;
|
|
25833
26011
|
const getParamScope = (params) => {
|
|
@@ -25998,66 +26176,6 @@ const createResourcePersistence = (
|
|
|
25998
26176
|
};
|
|
25999
26177
|
};
|
|
26000
26178
|
|
|
26001
|
-
const getParamsKey = (params) => {
|
|
26002
|
-
if (params === undefined || params === NO_PARAMS) {
|
|
26003
|
-
return "";
|
|
26004
|
-
}
|
|
26005
|
-
return stableStringify(params);
|
|
26006
|
-
};
|
|
26007
|
-
|
|
26008
|
-
const stableStringify = (value) => {
|
|
26009
|
-
if (value === null || typeof value !== "object") {
|
|
26010
|
-
return JSON.stringify(value);
|
|
26011
|
-
}
|
|
26012
|
-
if (Array.isArray(value)) {
|
|
26013
|
-
return `[${value.map(stableStringify).join(",")}]`;
|
|
26014
|
-
}
|
|
26015
|
-
const keys = Object.keys(value).sort();
|
|
26016
|
-
const parts = [];
|
|
26017
|
-
for (const key of keys) {
|
|
26018
|
-
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
26019
|
-
}
|
|
26020
|
-
return `{${parts.join(",")}}`;
|
|
26021
|
-
};
|
|
26022
|
-
|
|
26023
|
-
// The item with its relations inline: a relation value stands for what its
|
|
26024
|
-
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
26025
|
-
// twice on the same path is written as its id — the setters accept both.
|
|
26026
|
-
const serializeItem = (item, ancestorSet) => {
|
|
26027
|
-
ancestorSet.add(item);
|
|
26028
|
-
const serialized = {};
|
|
26029
|
-
for (const key of Object.keys(item)) {
|
|
26030
|
-
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
26031
|
-
}
|
|
26032
|
-
ancestorSet.delete(item);
|
|
26033
|
-
return serialized;
|
|
26034
|
-
};
|
|
26035
|
-
const serializeValue = (value, ancestorSet) => {
|
|
26036
|
-
if (value === null || typeof value !== "object") {
|
|
26037
|
-
return value;
|
|
26038
|
-
}
|
|
26039
|
-
if (Array.isArray(value)) {
|
|
26040
|
-
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
26041
|
-
}
|
|
26042
|
-
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
26043
|
-
if (objectSignal) {
|
|
26044
|
-
const held = objectSignal.value;
|
|
26045
|
-
if (held === undefined || held === null) {
|
|
26046
|
-
return null;
|
|
26047
|
-
}
|
|
26048
|
-
return serializeValue(held, ancestorSet);
|
|
26049
|
-
}
|
|
26050
|
-
if (typeof value.toJSON === "function") {
|
|
26051
|
-
return value;
|
|
26052
|
-
}
|
|
26053
|
-
if (ancestorSet.has(value)) {
|
|
26054
|
-
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26055
|
-
? value[SYMBOL_IDENTITY]
|
|
26056
|
-
: null;
|
|
26057
|
-
}
|
|
26058
|
-
return serializeItem(value, ancestorSet);
|
|
26059
|
-
};
|
|
26060
|
-
|
|
26061
26179
|
/*
|
|
26062
26180
|
* GET_RANGE: reading a resource one slice at a time, for a list that draws its
|
|
26063
26181
|
* rows as it goes (`<List.Items itemsAction>`).
|
|
@@ -26094,9 +26212,23 @@ const serializeValue = (value, ancestorSet) => {
|
|
|
26094
26212
|
* of what a rerun is for an action. Every reader made by `bindParams` shares
|
|
26095
26213
|
* the signal and the compositions of the one it comes from: the params say
|
|
26096
26214
|
* which slices are read, not which collection.
|
|
26215
|
+
*
|
|
26216
|
+
* A composition also outlives the document when a list on the page on screen
|
|
26217
|
+
* reads it (see keepPageOnScreen): the ranks around the window it draws are
|
|
26218
|
+
* written with their rows, and the first read of the next document finds them
|
|
26219
|
+
* as a composition left by a list that went away — the same return, after a
|
|
26220
|
+
* longer absence.
|
|
26097
26221
|
*/
|
|
26098
26222
|
|
|
26099
26223
|
|
|
26224
|
+
// The ranks kept of a composition for the next document: the window a list
|
|
26225
|
+
// draws, widened by at least one step on each side, and snapped to steps so a
|
|
26226
|
+
// scroll within one draws the same ranks and writes nothing.
|
|
26227
|
+
const KEPT_RANK_STEP = 20;
|
|
26228
|
+
// Counts the answers every composition took, so that one landing can be told
|
|
26229
|
+
// from one seen before (see readKept).
|
|
26230
|
+
let answerSequence = 0;
|
|
26231
|
+
|
|
26100
26232
|
const createRangeReader = (
|
|
26101
26233
|
actionName,
|
|
26102
26234
|
callback,
|
|
@@ -26105,6 +26237,9 @@ const createRangeReader = (
|
|
|
26105
26237
|
params: boundParams,
|
|
26106
26238
|
invalidationSignal = signal(0),
|
|
26107
26239
|
compositionSet = new Set(),
|
|
26240
|
+
// Bumped whenever what is kept of a composition for the next document
|
|
26241
|
+
// changes (see readKept); shared like the compositions themselves.
|
|
26242
|
+
keptVersionSignal = signal(0),
|
|
26108
26243
|
},
|
|
26109
26244
|
) => {
|
|
26110
26245
|
// Which composition this reader is about: the values its params hold, not its
|
|
@@ -26121,6 +26256,40 @@ const createRangeReader = (
|
|
|
26121
26256
|
}
|
|
26122
26257
|
return null;
|
|
26123
26258
|
};
|
|
26259
|
+
const keptKeyOf = (params) => getKeptReadKey(actionName, params);
|
|
26260
|
+
const bumpKeptVersion = () => {
|
|
26261
|
+
keptVersionSignal.value = keptVersionSignal.peek() + 1;
|
|
26262
|
+
};
|
|
26263
|
+
// The composition the previous document kept for these params, entering the
|
|
26264
|
+
// store and the compositions as if a list had just left it.
|
|
26265
|
+
const restoreKeptComposition = (params) => {
|
|
26266
|
+
const kept = takeKeptRead(keptKeyOf(params));
|
|
26267
|
+
if (!kept || typeof kept.count !== "number" || !kept.items) {
|
|
26268
|
+
return null;
|
|
26269
|
+
}
|
|
26270
|
+
const composition = {
|
|
26271
|
+
params,
|
|
26272
|
+
idByIndex: new Map(),
|
|
26273
|
+
count: kept.count,
|
|
26274
|
+
keptFrom: undefined,
|
|
26275
|
+
keptTo: undefined,
|
|
26276
|
+
answeredAt: 0,
|
|
26277
|
+
};
|
|
26278
|
+
for (const rank of Object.keys(kept.items)) {
|
|
26279
|
+
const index = Number(rank);
|
|
26280
|
+
const item = upsertKeptItem(store, kept.items[rank]);
|
|
26281
|
+
composition.idByIndex.set(index, item[store.idKey]);
|
|
26282
|
+
if (composition.keptFrom === undefined || index < composition.keptFrom) {
|
|
26283
|
+
composition.keptFrom = index;
|
|
26284
|
+
}
|
|
26285
|
+
if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
|
|
26286
|
+
composition.keptTo = index + 1;
|
|
26287
|
+
}
|
|
26288
|
+
}
|
|
26289
|
+
compositionSet.add(composition);
|
|
26290
|
+
bumpKeptVersion();
|
|
26291
|
+
return composition;
|
|
26292
|
+
};
|
|
26124
26293
|
const readRange = async (range = {}) => {
|
|
26125
26294
|
const { signal, ...rangeParams } = range;
|
|
26126
26295
|
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
@@ -26154,11 +26323,15 @@ const createRangeReader = (
|
|
|
26154
26323
|
readRange.invalidate = () => {
|
|
26155
26324
|
compositionSet.clear();
|
|
26156
26325
|
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
26326
|
+
bumpKeptVersion();
|
|
26157
26327
|
};
|
|
26158
26328
|
// The rows of a composition, drawn from the store: a rank whose row is gone
|
|
26159
26329
|
// from the store resolves to nothing and is asked for again.
|
|
26160
26330
|
readRange.readComposition = () => {
|
|
26161
|
-
const
|
|
26331
|
+
const params = currentParams();
|
|
26332
|
+
const composition = untracked(
|
|
26333
|
+
() => findComposition(params) || restoreKeptComposition(params),
|
|
26334
|
+
);
|
|
26162
26335
|
if (!composition) {
|
|
26163
26336
|
return null;
|
|
26164
26337
|
}
|
|
@@ -26182,18 +26355,85 @@ const createRangeReader = (
|
|
|
26182
26355
|
const params = currentParams();
|
|
26183
26356
|
let composition = findComposition(params);
|
|
26184
26357
|
if (!composition) {
|
|
26185
|
-
composition = {
|
|
26358
|
+
composition = {
|
|
26359
|
+
params,
|
|
26360
|
+
idByIndex: new Map(),
|
|
26361
|
+
count,
|
|
26362
|
+
keptFrom: undefined,
|
|
26363
|
+
keptTo: undefined,
|
|
26364
|
+
answeredAt: 0,
|
|
26365
|
+
};
|
|
26186
26366
|
compositionSet.add(composition);
|
|
26187
26367
|
} else if (replace) {
|
|
26188
26368
|
composition.idByIndex = new Map();
|
|
26189
26369
|
}
|
|
26190
26370
|
composition.count = count;
|
|
26371
|
+
answerSequence++;
|
|
26372
|
+
composition.answeredAt = answerSequence;
|
|
26191
26373
|
for (const [index, item] of byIndex) {
|
|
26192
26374
|
const id = item ? item[store.idKey] : undefined;
|
|
26193
26375
|
if (id !== undefined) {
|
|
26194
26376
|
composition.idByIndex.set(index, id);
|
|
26195
26377
|
}
|
|
26196
26378
|
}
|
|
26379
|
+
bumpKeptVersion();
|
|
26380
|
+
};
|
|
26381
|
+
// The ranks [windowFrom, windowTo) a list draws of this composition: what is
|
|
26382
|
+
// kept of it for the next document is around them (see readKept).
|
|
26383
|
+
readRange.frameComposition = (windowFrom, windowTo) => {
|
|
26384
|
+
const composition = findComposition(currentParams());
|
|
26385
|
+
if (!composition) {
|
|
26386
|
+
return;
|
|
26387
|
+
}
|
|
26388
|
+
let keptFrom =
|
|
26389
|
+
Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
|
|
26390
|
+
KEPT_RANK_STEP;
|
|
26391
|
+
if (keptFrom < 0) {
|
|
26392
|
+
keptFrom = 0;
|
|
26393
|
+
}
|
|
26394
|
+
const keptTo =
|
|
26395
|
+
Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
|
|
26396
|
+
if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
|
|
26397
|
+
return;
|
|
26398
|
+
}
|
|
26399
|
+
composition.keptFrom = keptFrom;
|
|
26400
|
+
composition.keptTo = keptTo;
|
|
26401
|
+
bumpKeptVersion();
|
|
26402
|
+
};
|
|
26403
|
+
// What a document keeps of the composition these params read, for the
|
|
26404
|
+
// next one (see keepPageOnScreen): the count, and the ranks around the
|
|
26405
|
+
// window last drawn with their rows inline. Read inside an effect, it
|
|
26406
|
+
// follows the composition and the rows. `answeredAt` grows with every answer
|
|
26407
|
+
// the composition takes (0: none in this document, restored from a kept one).
|
|
26408
|
+
readRange.readKept = () => {
|
|
26409
|
+
// eslint-disable-next-line no-unused-expressions
|
|
26410
|
+
keptVersionSignal.value;
|
|
26411
|
+
const params = resolveParams(boundParams);
|
|
26412
|
+
const key = keptKeyOf(params);
|
|
26413
|
+
const composition = findComposition(params);
|
|
26414
|
+
if (!composition || composition.count === undefined) {
|
|
26415
|
+
return { key, answeredAt: 0, readEntry: () => undefined };
|
|
26416
|
+
}
|
|
26417
|
+
const readEntry = () => {
|
|
26418
|
+
const items = {};
|
|
26419
|
+
for (const [index, id] of composition.idByIndex) {
|
|
26420
|
+
if (
|
|
26421
|
+
composition.keptFrom !== undefined &&
|
|
26422
|
+
index < composition.keptFrom
|
|
26423
|
+
) {
|
|
26424
|
+
continue;
|
|
26425
|
+
}
|
|
26426
|
+
if (composition.keptTo !== undefined && index >= composition.keptTo) {
|
|
26427
|
+
continue;
|
|
26428
|
+
}
|
|
26429
|
+
const item = store.select(id);
|
|
26430
|
+
if (item) {
|
|
26431
|
+
items[index] = serializeItem(item, new Set());
|
|
26432
|
+
}
|
|
26433
|
+
}
|
|
26434
|
+
return { count: composition.count, items };
|
|
26435
|
+
};
|
|
26436
|
+
return { key, answeredAt: composition.answeredAt, readEntry };
|
|
26197
26437
|
};
|
|
26198
26438
|
// The same trade a list makes with the rows it holds, applied to what is kept
|
|
26199
26439
|
// for the next mount. Two lists on one collection each trim by their own
|
|
@@ -26204,11 +26444,16 @@ const createRangeReader = (
|
|
|
26204
26444
|
if (!composition || composition.idByIndex.size <= budget) {
|
|
26205
26445
|
return;
|
|
26206
26446
|
}
|
|
26447
|
+
let trimmed = false;
|
|
26207
26448
|
for (const index of composition.idByIndex.keys()) {
|
|
26208
26449
|
if (index < keepFrom || index > keepTo) {
|
|
26209
26450
|
composition.idByIndex.delete(index);
|
|
26451
|
+
trimmed = true;
|
|
26210
26452
|
}
|
|
26211
26453
|
}
|
|
26454
|
+
if (trimmed) {
|
|
26455
|
+
bumpKeptVersion();
|
|
26456
|
+
}
|
|
26212
26457
|
};
|
|
26213
26458
|
// Memoized for the reasons an action's bindParams is (see actions.js): params
|
|
26214
26459
|
// and the reader they make have synchronized lifetimes, and params equal in
|
|
@@ -26224,6 +26469,7 @@ const createRangeReader = (
|
|
|
26224
26469
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
26225
26470
|
invalidationSignal,
|
|
26226
26471
|
compositionSet,
|
|
26472
|
+
keptVersionSignal,
|
|
26227
26473
|
});
|
|
26228
26474
|
readerByParams.set(paramsToBind, reader);
|
|
26229
26475
|
return reader;
|
|
@@ -27658,7 +27904,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27658
27904
|
`${name}.${verb}`,
|
|
27659
27905
|
declarationSite,
|
|
27660
27906
|
);
|
|
27661
|
-
|
|
27907
|
+
const action = createAction(callback, {
|
|
27662
27908
|
name: `${name}.${verb}`,
|
|
27663
27909
|
meta: { verb, isMany: false, paramScope },
|
|
27664
27910
|
resultToValue: (result, action) => {
|
|
@@ -27694,17 +27940,33 @@ const createRestActionFactoryForRoot = (
|
|
|
27694
27940
|
},
|
|
27695
27941
|
valueToData: (itemId) => store.select(itemId),
|
|
27696
27942
|
// While the request is out, the row the store already holds for these
|
|
27697
|
-
// params is drawn
|
|
27943
|
+
// params is drawn, or the row the page kept from the previous document
|
|
27944
|
+
// (kept_reads.js): the action starts on the "refresh over a known
|
|
27698
27945
|
// answer" line of data_states.md rather than the first-load one.
|
|
27699
27946
|
provisionalValue:
|
|
27700
27947
|
verb === "GET"
|
|
27701
|
-
? (params) => {
|
|
27702
|
-
const item = untracked(() =>
|
|
27948
|
+
? (params, action) => {
|
|
27949
|
+
const item = untracked(() => {
|
|
27950
|
+
const itemForGet = findItemForGet(params);
|
|
27951
|
+
if (itemForGet) {
|
|
27952
|
+
return itemForGet;
|
|
27953
|
+
}
|
|
27954
|
+
const keptItem = takeKeptRead(
|
|
27955
|
+
getKeptReadKey(action.name, params),
|
|
27956
|
+
);
|
|
27957
|
+
return keptItem ? upsertKeptItem(store, keptItem) : null;
|
|
27958
|
+
});
|
|
27703
27959
|
return item ? item[idKey] : undefined;
|
|
27704
27960
|
}
|
|
27705
27961
|
: undefined,
|
|
27962
|
+
// Another params' answer is another row.
|
|
27963
|
+
inheritData: verb !== "GET",
|
|
27706
27964
|
completeSideEffect: onActionComplete,
|
|
27707
27965
|
});
|
|
27966
|
+
if (verb === "GET") {
|
|
27967
|
+
markKeepableRead(action);
|
|
27968
|
+
}
|
|
27969
|
+
return action;
|
|
27708
27970
|
};
|
|
27709
27971
|
const createActionAffectingManyItems = (
|
|
27710
27972
|
verb,
|
|
@@ -27719,7 +27981,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27719
27981
|
return itemArray.map((item) => item[idKey]);
|
|
27720
27982
|
};
|
|
27721
27983
|
|
|
27722
|
-
|
|
27984
|
+
const action = createAction(callback, {
|
|
27723
27985
|
meta: { verb, isMany: true, paramScope },
|
|
27724
27986
|
name: `${name}.${verb}_MANY`,
|
|
27725
27987
|
resultToValue: applyResultToValue,
|
|
@@ -27727,6 +27989,25 @@ const createRestActionFactoryForRoot = (
|
|
|
27727
27989
|
// undefined so a screen can tell "not asked yet" from "answered: none".
|
|
27728
27990
|
valueToData: (idArray) =>
|
|
27729
27991
|
idArray === undefined ? undefined : store.selectAll(idArray),
|
|
27992
|
+
// The rows the page kept from the previous document (kept_reads.js),
|
|
27993
|
+
// drawn while the request is out. A binding moving to new params still
|
|
27994
|
+
// hands over the previous answer when there are none: a list standing
|
|
27995
|
+
// in for the next one beats a skeleton.
|
|
27996
|
+
provisionalValue:
|
|
27997
|
+
verb === "GET"
|
|
27998
|
+
? (params, action) =>
|
|
27999
|
+
untracked(() => {
|
|
28000
|
+
const keptItems = takeKeptRead(
|
|
28001
|
+
getKeptReadKey(action.name, params),
|
|
28002
|
+
);
|
|
28003
|
+
if (!Array.isArray(keptItems)) {
|
|
28004
|
+
return undefined;
|
|
28005
|
+
}
|
|
28006
|
+
return keptItems.map(
|
|
28007
|
+
(keptItem) => upsertKeptItem(store, keptItem)[idKey],
|
|
28008
|
+
);
|
|
28009
|
+
})
|
|
28010
|
+
: undefined,
|
|
27730
28011
|
completeSideEffect: (actionCompleted) => {
|
|
27731
28012
|
onActionComplete(actionCompleted);
|
|
27732
28013
|
if (
|
|
@@ -27742,6 +28023,10 @@ const createRestActionFactoryForRoot = (
|
|
|
27742
28023
|
return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
|
|
27743
28024
|
},
|
|
27744
28025
|
});
|
|
28026
|
+
if (verb === "GET") {
|
|
28027
|
+
markKeepableRead(action);
|
|
28028
|
+
}
|
|
28029
|
+
return action;
|
|
27745
28030
|
};
|
|
27746
28031
|
|
|
27747
28032
|
return createActionForRoot;
|
|
@@ -27752,10 +28037,12 @@ const createRestActionFactoryForRoot = (
|
|
|
27752
28037
|
// NetworkPolicyError carrying the policy's reason. A read is called or not depending
|
|
27753
28038
|
// on where the policy answers reads from. Answered from the store, a GET of a
|
|
27754
28039
|
// root resource completes with the row the store holds for it (see
|
|
27755
|
-
// findItemInStore)
|
|
27756
|
-
//
|
|
27757
|
-
//
|
|
27758
|
-
//
|
|
28040
|
+
// findItemInStore), and a root GET or GET_MANY holding rows for these params
|
|
28041
|
+
// (the kept page, drawn while the request is out) with those — handing them
|
|
28042
|
+
// back is an upsert without effect, so the action completes with what it had
|
|
28043
|
+
// and nothing is asked. A relationship GET has no row of its own to answer
|
|
28044
|
+
// with and settles with the same error. A completed GET asked to rerun never
|
|
28045
|
+
// gets here (actions.js holds it).
|
|
27759
28046
|
const applyNetworkPolicy = (
|
|
27760
28047
|
restCallback,
|
|
27761
28048
|
{ verb, isMany, findItemInStore },
|
|
@@ -27769,6 +28056,12 @@ const applyNetworkPolicy = (
|
|
|
27769
28056
|
if (!policy.readsFromStore) {
|
|
27770
28057
|
return restCallback(params, context);
|
|
27771
28058
|
}
|
|
28059
|
+
if (isKeepableRead(context.action)) {
|
|
28060
|
+
const dataHeld = context.action.dataSignal.peek();
|
|
28061
|
+
if (dataHeld !== undefined) {
|
|
28062
|
+
return dataHeld;
|
|
28063
|
+
}
|
|
28064
|
+
}
|
|
27772
28065
|
if (!isMany && findItemInStore) {
|
|
27773
28066
|
const item = findItemInStore(params, context.action);
|
|
27774
28067
|
if (item) {
|
|
@@ -37919,6 +38212,37 @@ const writeCover = (edge, element, size) => {
|
|
|
37919
38212
|
style.setProperty(property, value);
|
|
37920
38213
|
};
|
|
37921
38214
|
|
|
38215
|
+
// Every route action, with whether it asks something of the address at the
|
|
38216
|
+
// moment: its route matches and its params getter returns something.
|
|
38217
|
+
const routeActionEntriesSignal = signal([]);
|
|
38218
|
+
let activeRouteActionsPrevious = new Set();
|
|
38219
|
+
/**
|
|
38220
|
+
* The route actions asking something for the page on screen: their route
|
|
38221
|
+
* matches and their params getter returns something — the running ones, not
|
|
38222
|
+
* every matching one, so an action bound to every page that asks only when a
|
|
38223
|
+
* popup is open over it counts while it is open. Read to rerun what the page
|
|
38224
|
+
* reads (the app back from the background), and by `keepPageOnScreen`.
|
|
38225
|
+
* The set is handed out again only when its members change.
|
|
38226
|
+
*
|
|
38227
|
+
* A page's code is declared as a route action too; `action.meta.verb` says
|
|
38228
|
+
* which ones read data.
|
|
38229
|
+
*
|
|
38230
|
+
* @type {import("@preact/signals").ReadonlySignal<Set<object>>}
|
|
38231
|
+
*/
|
|
38232
|
+
const activeRouteActionsSignal = computed(() => {
|
|
38233
|
+
const active = new Set();
|
|
38234
|
+
for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
|
|
38235
|
+
if (askingSignal.value) {
|
|
38236
|
+
active.add(routeAction);
|
|
38237
|
+
}
|
|
38238
|
+
}
|
|
38239
|
+
if (haveSameMembers(active, activeRouteActionsPrevious)) {
|
|
38240
|
+
return activeRouteActionsPrevious;
|
|
38241
|
+
}
|
|
38242
|
+
activeRouteActionsPrevious = active;
|
|
38243
|
+
return active;
|
|
38244
|
+
});
|
|
38245
|
+
|
|
37922
38246
|
/**
|
|
37923
38247
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
37924
38248
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
@@ -37976,6 +38300,13 @@ const routeAction = (
|
|
|
37976
38300
|
return params;
|
|
37977
38301
|
};
|
|
37978
38302
|
const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
|
|
38303
|
+
routeActionEntriesSignal.value = [
|
|
38304
|
+
...routeActionEntriesSignal.peek(),
|
|
38305
|
+
{
|
|
38306
|
+
routeAction: actionBoundToRoute,
|
|
38307
|
+
askingSignal: computed(() => Boolean(readParamsAsked())),
|
|
38308
|
+
},
|
|
38309
|
+
];
|
|
37979
38310
|
for (const route of routes) {
|
|
37980
38311
|
// Asked of the route's params as they are, like the effect asks them.
|
|
37981
38312
|
registerRouteReload(route, () =>
|
|
@@ -38059,6 +38390,343 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
38059
38390
|
return anyMatchingSignal;
|
|
38060
38391
|
};
|
|
38061
38392
|
|
|
38393
|
+
const haveSameMembers = (setA, setB) => {
|
|
38394
|
+
if (setA.size !== setB.size) {
|
|
38395
|
+
return false;
|
|
38396
|
+
}
|
|
38397
|
+
for (const value of setA) {
|
|
38398
|
+
if (!setB.has(value)) {
|
|
38399
|
+
return false;
|
|
38400
|
+
}
|
|
38401
|
+
}
|
|
38402
|
+
return true;
|
|
38403
|
+
};
|
|
38404
|
+
|
|
38405
|
+
/*
|
|
38406
|
+
* The page on screen, kept for the next document: what its reads answered,
|
|
38407
|
+
* written into a signal the app hands over, and drawn again by the same reads
|
|
38408
|
+
* in a document opening on the same page (a reload, a tab the system
|
|
38409
|
+
* discarded). The read side is state/rest/kept_reads.js; this is what decides
|
|
38410
|
+
* which reads are the page, and when the slot is written.
|
|
38411
|
+
*
|
|
38412
|
+
* The unit is the page, not the resource. One resource is read by several
|
|
38413
|
+
* screens — a search, someone's games, a game opened from anywhere — and kept
|
|
38414
|
+
* per resource it would keep every row ever opened. The page's reads are what
|
|
38415
|
+
* navi already runs for it: the route actions asking something for the page
|
|
38416
|
+
* (activeRouteActionsSignal) and the compositions the lists on screen read
|
|
38417
|
+
* (shownRangeReadersSignal). A search typed on the page is neither, and is not
|
|
38418
|
+
* kept.
|
|
38419
|
+
*
|
|
38420
|
+
* A read is written once it has answered in this document. Before that it
|
|
38421
|
+
* keeps what the previous document kept for it, never what it shows meanwhile:
|
|
38422
|
+
* an answer handed over from other params (a list standing in for the next
|
|
38423
|
+
* one) says nothing about these.
|
|
38424
|
+
*
|
|
38425
|
+
* The write waits for the page to settle and is flushed when the document is
|
|
38426
|
+
* hidden. A navigation goes through pages half there — the route matched, its
|
|
38427
|
+
* list not mounted yet, the page left still drawn under a transition — and
|
|
38428
|
+
* each would otherwise be written out in full. Hidden is the moment the system
|
|
38429
|
+
* may discard the document, so nothing is lost by waiting until then.
|
|
38430
|
+
*
|
|
38431
|
+
* The signal turning `undefined` from outside (sign-out) forgets the page, and
|
|
38432
|
+
* nothing is written until a read lands again: written at once, the page still
|
|
38433
|
+
* on screen would put back the account just left.
|
|
38434
|
+
*/
|
|
38435
|
+
|
|
38436
|
+
const WRITE_DELAY = 1000;
|
|
38437
|
+
const NOTHING_PENDING = {};
|
|
38438
|
+
let stopKeepingCurrent = null;
|
|
38439
|
+
|
|
38440
|
+
/**
|
|
38441
|
+
* Keeps what the page on screen reads, so that a new document opening on it —
|
|
38442
|
+
* a reload, a pull-to-refresh, a tab the system discarded in the background —
|
|
38443
|
+
* draws it again while its reads go out, rather than skeletons.
|
|
38444
|
+
*
|
|
38445
|
+
* What the page reads is what navi runs for it: the route actions asking
|
|
38446
|
+
* something for it (see `activeRouteActionsSignal`) whose action is a resource
|
|
38447
|
+
* `GET` or `GET_MANY`, and the `GET_RANGE` compositions the `<List.Items>` on
|
|
38448
|
+
* screen read, around the window they draw. A navigation replaces the slot with
|
|
38449
|
+
* the next page's reads, and the page left is dropped. In the next document,
|
|
38450
|
+
* the first run of the same read with the same params draws the kept answer as
|
|
38451
|
+
* its provisional value: `data` set while `loading` is `true`, the answer
|
|
38452
|
+
* replacing it. Under a network policy answering reads from the store, the
|
|
38453
|
+
* kept answer answers the read. The copy follows the store: a `PUT` on a row
|
|
38454
|
+
* rewrites it.
|
|
38455
|
+
*
|
|
38456
|
+
* Call it before the routes start: the first runs are the ones that look.
|
|
38457
|
+
*
|
|
38458
|
+
* @param {object} options
|
|
38459
|
+
* @param {import("@preact/signals").Signal} options.signal - where the slot is
|
|
38460
|
+
* written: a `stateSignal` with `persists: true` and `type: "object"`, its
|
|
38461
|
+
* `id` holding whatever makes a kept page unusable (the deployed version).
|
|
38462
|
+
* Writing `undefined` into it (sign-out) forgets the page, and nothing is
|
|
38463
|
+
* written again until a read lands.
|
|
38464
|
+
* @param {() => boolean} [options.when] - read whenever the slot is about to be
|
|
38465
|
+
* read or written: `false` reads nothing, writes nothing, and empties the
|
|
38466
|
+
* signal (a tab viewing as someone else). A function, so it can read a signal.
|
|
38467
|
+
* @param {object[]} [options.always] - routes whose reads stay kept once the
|
|
38468
|
+
* page is left, until it is visited again: the page an app opens on when it
|
|
38469
|
+
* is launched rather than reloaded.
|
|
38470
|
+
* @returns {() => void} stops keeping the page.
|
|
38471
|
+
* @see docs/resource.md — what is kept and what is not, what stays the app's
|
|
38472
|
+
*/
|
|
38473
|
+
const keepPageOnScreen = ({
|
|
38474
|
+
signal: keptSignal,
|
|
38475
|
+
when,
|
|
38476
|
+
always = [],
|
|
38477
|
+
} = {}) => {
|
|
38478
|
+
if (!isSignal(keptSignal)) {
|
|
38479
|
+
throw new TypeError(
|
|
38480
|
+
`keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
|
|
38481
|
+
);
|
|
38482
|
+
}
|
|
38483
|
+
if (stopKeepingCurrent) {
|
|
38484
|
+
stopKeepingCurrent();
|
|
38485
|
+
}
|
|
38486
|
+
const isAllowed = () => {
|
|
38487
|
+
return when ? Boolean(when()) : true;
|
|
38488
|
+
};
|
|
38489
|
+
const alwaysMatchingSignal = anyMatchingRouteSignal(always);
|
|
38490
|
+
|
|
38491
|
+
// What the signal held when it was read: the previous document's page.
|
|
38492
|
+
let loadedEntryMap = new Map();
|
|
38493
|
+
let loadedAlwaysKeys = [];
|
|
38494
|
+
// Which reads answered in this document (since the last forget): only
|
|
38495
|
+
// those write what they show.
|
|
38496
|
+
let answeredActionWeakSet = new WeakSet();
|
|
38497
|
+
let answeredRangeKeySet = new Set();
|
|
38498
|
+
// The state each action was last seen in, and the last answer seen of each
|
|
38499
|
+
// range reader: a read lands when they move, and not because the slot looks
|
|
38500
|
+
// again at something that had answered before a forget.
|
|
38501
|
+
const stateSeenWeakMap = new WeakMap();
|
|
38502
|
+
const answeredAtSeenWeakMap = new WeakMap();
|
|
38503
|
+
// Forgotten: nothing is written until a read lands.
|
|
38504
|
+
let paused = false;
|
|
38505
|
+
// The reads of the always-kept page, as they were the last time it was on
|
|
38506
|
+
// screen in this document; null until then.
|
|
38507
|
+
let alwaysReads = null;
|
|
38508
|
+
|
|
38509
|
+
const forget = () => {
|
|
38510
|
+
loadedEntryMap = new Map();
|
|
38511
|
+
loadedAlwaysKeys = [];
|
|
38512
|
+
answeredActionWeakSet = new WeakSet();
|
|
38513
|
+
answeredRangeKeySet = new Set();
|
|
38514
|
+
alwaysReads = null;
|
|
38515
|
+
paused = true;
|
|
38516
|
+
withdrawKeptReads();
|
|
38517
|
+
};
|
|
38518
|
+
|
|
38519
|
+
let lastWritten;
|
|
38520
|
+
let pendingWrite = NOTHING_PENDING;
|
|
38521
|
+
let writeTimeout;
|
|
38522
|
+
const flush = () => {
|
|
38523
|
+
clearTimeout(writeTimeout);
|
|
38524
|
+
if (pendingWrite === NOTHING_PENDING) {
|
|
38525
|
+
return;
|
|
38526
|
+
}
|
|
38527
|
+
const value = pendingWrite;
|
|
38528
|
+
pendingWrite = NOTHING_PENDING;
|
|
38529
|
+
if (compareTwoJsValues(value, lastWritten)) {
|
|
38530
|
+
return;
|
|
38531
|
+
}
|
|
38532
|
+
lastWritten = value;
|
|
38533
|
+
keptSignal.value = value;
|
|
38534
|
+
};
|
|
38535
|
+
const scheduleWrite = (value) => {
|
|
38536
|
+
pendingWrite = value;
|
|
38537
|
+
clearTimeout(writeTimeout);
|
|
38538
|
+
writeTimeout = setTimeout(flush, WRITE_DELAY);
|
|
38539
|
+
};
|
|
38540
|
+
const onVisibilityChange = () => {
|
|
38541
|
+
if (document.visibilityState === "hidden") {
|
|
38542
|
+
flush();
|
|
38543
|
+
}
|
|
38544
|
+
};
|
|
38545
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
38546
|
+
window.addEventListener("pagehide", flush);
|
|
38547
|
+
|
|
38548
|
+
// Runs at once with what the signal already holds (the slot read from
|
|
38549
|
+
// storage), then on every write — the slot's own ones are recognized and
|
|
38550
|
+
// left alone.
|
|
38551
|
+
const unsubscribe = keptSignal.subscribe((value) => {
|
|
38552
|
+
if (value === lastWritten) {
|
|
38553
|
+
return;
|
|
38554
|
+
}
|
|
38555
|
+
lastWritten = value;
|
|
38556
|
+
pendingWrite = NOTHING_PENDING;
|
|
38557
|
+
clearTimeout(writeTimeout);
|
|
38558
|
+
if (
|
|
38559
|
+
value === undefined ||
|
|
38560
|
+
value === null ||
|
|
38561
|
+
typeof value !== "object" ||
|
|
38562
|
+
!isAllowed()
|
|
38563
|
+
) {
|
|
38564
|
+
forget();
|
|
38565
|
+
return;
|
|
38566
|
+
}
|
|
38567
|
+
const entries =
|
|
38568
|
+
value.entries && typeof value.entries === "object" ? value.entries : {};
|
|
38569
|
+
loadedEntryMap = new Map(Object.entries(entries));
|
|
38570
|
+
loadedAlwaysKeys = Array.isArray(value.always)
|
|
38571
|
+
? value.always.filter((key) => loadedEntryMap.has(key))
|
|
38572
|
+
: [];
|
|
38573
|
+
alwaysReads = null;
|
|
38574
|
+
paused = false;
|
|
38575
|
+
offerKeptReads(loadedEntryMap);
|
|
38576
|
+
});
|
|
38577
|
+
|
|
38578
|
+
const actionReadWeakMap = new WeakMap();
|
|
38579
|
+
const actionReadOf = (action) => {
|
|
38580
|
+
let read = actionReadWeakMap.get(action);
|
|
38581
|
+
if (read) {
|
|
38582
|
+
return read;
|
|
38583
|
+
}
|
|
38584
|
+
read = () => {
|
|
38585
|
+
const state = action.runningStateSignal.value;
|
|
38586
|
+
const stateSeen = stateSeenWeakMap.get(action);
|
|
38587
|
+
stateSeenWeakMap.set(action, state);
|
|
38588
|
+
const landed = state === COMPLETED && stateSeen !== COMPLETED;
|
|
38589
|
+
if (landed) {
|
|
38590
|
+
answeredActionWeakSet.add(action);
|
|
38591
|
+
}
|
|
38592
|
+
return {
|
|
38593
|
+
key: getKeptReadKey(action.name, action.params),
|
|
38594
|
+
answered: answeredActionWeakSet.has(action),
|
|
38595
|
+
landed,
|
|
38596
|
+
readEntry: () => {
|
|
38597
|
+
const data = action.dataSignal.value;
|
|
38598
|
+
if (data === undefined || data === null) {
|
|
38599
|
+
return undefined;
|
|
38600
|
+
}
|
|
38601
|
+
return serializeData(data);
|
|
38602
|
+
},
|
|
38603
|
+
};
|
|
38604
|
+
};
|
|
38605
|
+
actionReadWeakMap.set(action, read);
|
|
38606
|
+
return read;
|
|
38607
|
+
};
|
|
38608
|
+
const rangeReadWeakMap = new WeakMap();
|
|
38609
|
+
const rangeReadOf = (reader) => {
|
|
38610
|
+
let read = rangeReadWeakMap.get(reader);
|
|
38611
|
+
if (read) {
|
|
38612
|
+
return read;
|
|
38613
|
+
}
|
|
38614
|
+
read = () => {
|
|
38615
|
+
const { key, answeredAt, readEntry } = reader.readKept();
|
|
38616
|
+
const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
|
|
38617
|
+
answeredAtSeenWeakMap.set(reader, answeredAt);
|
|
38618
|
+
const landed = answeredAt > answeredAtSeen;
|
|
38619
|
+
if (landed) {
|
|
38620
|
+
answeredRangeKeySet.add(key);
|
|
38621
|
+
}
|
|
38622
|
+
return {
|
|
38623
|
+
key,
|
|
38624
|
+
answered: answeredRangeKeySet.has(key),
|
|
38625
|
+
landed,
|
|
38626
|
+
readEntry,
|
|
38627
|
+
};
|
|
38628
|
+
};
|
|
38629
|
+
rangeReadWeakMap.set(reader, read);
|
|
38630
|
+
return read;
|
|
38631
|
+
};
|
|
38632
|
+
const readsOnScreen = () => {
|
|
38633
|
+
const reads = [];
|
|
38634
|
+
for (const routeAction of activeRouteActionsSignal.value) {
|
|
38635
|
+
// The instance is what is kept, so that a page left keeps what it read.
|
|
38636
|
+
// The binding's callSource names that instance and is rewritten once the
|
|
38637
|
+
// binding has moved to another one: read, it brings the slot along.
|
|
38638
|
+
// eslint-disable-next-line no-unused-expressions
|
|
38639
|
+
routeAction.callSource;
|
|
38640
|
+
const action = getActionPrivateProperties(routeAction).currentAction;
|
|
38641
|
+
if (action.params === NO_PARAMS || !isKeepableRead(action)) {
|
|
38642
|
+
continue;
|
|
38643
|
+
}
|
|
38644
|
+
reads.push(actionReadOf(action));
|
|
38645
|
+
}
|
|
38646
|
+
for (const reader of shownRangeReadersSignal.value.keys()) {
|
|
38647
|
+
reads.push(rangeReadOf(reader));
|
|
38648
|
+
}
|
|
38649
|
+
return reads;
|
|
38650
|
+
};
|
|
38651
|
+
|
|
38652
|
+
const stopEffect = effect(() => {
|
|
38653
|
+
const allowed = isAllowed();
|
|
38654
|
+
const routed = routedSignal.value;
|
|
38655
|
+
const alwaysMatching = alwaysMatchingSignal.value;
|
|
38656
|
+
if (!allowed) {
|
|
38657
|
+
forget();
|
|
38658
|
+
pendingWrite = undefined;
|
|
38659
|
+
flush();
|
|
38660
|
+
return;
|
|
38661
|
+
}
|
|
38662
|
+
if (!routed) {
|
|
38663
|
+
// No page yet: the slot is what the first reads will draw.
|
|
38664
|
+
return;
|
|
38665
|
+
}
|
|
38666
|
+
const screenReads = readsOnScreen();
|
|
38667
|
+
if (alwaysMatching) {
|
|
38668
|
+
alwaysReads = screenReads;
|
|
38669
|
+
}
|
|
38670
|
+
const entries = {};
|
|
38671
|
+
let landed = false;
|
|
38672
|
+
const collect = (reads) => {
|
|
38673
|
+
const keys = [];
|
|
38674
|
+
for (const read of reads) {
|
|
38675
|
+
const result = read();
|
|
38676
|
+
if (result.landed) {
|
|
38677
|
+
landed = true;
|
|
38678
|
+
}
|
|
38679
|
+
const entry = result.answered
|
|
38680
|
+
? result.readEntry()
|
|
38681
|
+
: loadedEntryMap.get(result.key);
|
|
38682
|
+
if (entry === undefined) {
|
|
38683
|
+
continue;
|
|
38684
|
+
}
|
|
38685
|
+
entries[result.key] = entry;
|
|
38686
|
+
keys.push(result.key);
|
|
38687
|
+
}
|
|
38688
|
+
return keys;
|
|
38689
|
+
};
|
|
38690
|
+
const screenKeys = collect(screenReads);
|
|
38691
|
+
let alwaysKeys;
|
|
38692
|
+
if (alwaysReads === screenReads) {
|
|
38693
|
+
alwaysKeys = screenKeys;
|
|
38694
|
+
} else if (alwaysReads) {
|
|
38695
|
+
alwaysKeys = collect(alwaysReads);
|
|
38696
|
+
} else {
|
|
38697
|
+
alwaysKeys = loadedAlwaysKeys;
|
|
38698
|
+
for (const key of alwaysKeys) {
|
|
38699
|
+
entries[key] = loadedEntryMap.get(key);
|
|
38700
|
+
}
|
|
38701
|
+
}
|
|
38702
|
+
if (paused) {
|
|
38703
|
+
if (!landed) {
|
|
38704
|
+
return;
|
|
38705
|
+
}
|
|
38706
|
+
paused = false;
|
|
38707
|
+
}
|
|
38708
|
+
if (screenKeys.length === 0 && alwaysKeys.length === 0) {
|
|
38709
|
+
scheduleWrite(undefined);
|
|
38710
|
+
return;
|
|
38711
|
+
}
|
|
38712
|
+
scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
|
|
38713
|
+
});
|
|
38714
|
+
|
|
38715
|
+
const stop = () => {
|
|
38716
|
+
flush();
|
|
38717
|
+
stopEffect();
|
|
38718
|
+
unsubscribe();
|
|
38719
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
38720
|
+
window.removeEventListener("pagehide", flush);
|
|
38721
|
+
withdrawKeptReads();
|
|
38722
|
+
if (stopKeepingCurrent === stop) {
|
|
38723
|
+
stopKeepingCurrent = null;
|
|
38724
|
+
}
|
|
38725
|
+
};
|
|
38726
|
+
stopKeepingCurrent = stop;
|
|
38727
|
+
return stop;
|
|
38728
|
+
};
|
|
38729
|
+
|
|
38062
38730
|
/**
|
|
38063
38731
|
* A popup that is the anchor, brought to the front.
|
|
38064
38732
|
*
|
|
@@ -65131,7 +65799,7 @@ const css$E = /* css */`
|
|
|
65131
65799
|
/* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
|
|
65132
65800
|
by default until .show()/.showModal() adds [open]) — needs its own
|
|
65133
65801
|
starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
|
|
65134
|
-
own backdropProps (recomputed from openController.
|
|
65802
|
+
own backdropProps (recomputed from openController.openedInDom on every
|
|
65135
65803
|
render, present from the very first one), then toggled by plain
|
|
65136
65804
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
65137
65805
|
display override — removing the attribute just lets this rule stop
|
|
@@ -66732,7 +67400,7 @@ const useDialogProps = props => {
|
|
|
66732
67400
|
"navi-out-of-flow": "",
|
|
66733
67401
|
"baseClassName": "navi_dialog_backdrop",
|
|
66734
67402
|
"aria-hidden": "true",
|
|
66735
|
-
// Recomputed fresh on every render from openController.
|
|
67403
|
+
// Recomputed fresh on every render from openController.openedInDom (not
|
|
66736
67404
|
// driven through a mount-time layout effect, unlike this file's own
|
|
66737
67405
|
// imperative open/close toggling below) — present in the DOM
|
|
66738
67406
|
// synchronously from the very first commit, matching this file's own
|
|
@@ -66742,7 +67410,7 @@ const useDialogProps = props => {
|
|
|
66742
67410
|
// needs — see popover.jsx's own identical prop for the full reasoning.
|
|
66743
67411
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
66744
67412
|
// Present from this very first render (recomputed fresh on every one
|
|
66745
|
-
// from openController.
|
|
67413
|
+
// from openController.openedInDom, not a frozen mount-time constant) so
|
|
66746
67414
|
// there's no gap for the browser to ever paint this plain-div backdrop
|
|
66747
67415
|
// visible before anything has actually opened it — see popover.jsx's
|
|
66748
67416
|
// own identical prop for the full reasoning, and this file's own CSS
|
|
@@ -66762,7 +67430,7 @@ const useDialogProps = props => {
|
|
|
66762
67430
|
// (kept once, not repeated here).
|
|
66763
67431
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
66764
67432
|
// Present from the very first render (recomputed fresh from
|
|
66765
|
-
// openController.
|
|
67433
|
+
// openController.openedInDom every time, not a frozen mount-time constant —
|
|
66766
67434
|
// see popover.jsx's own identical prop for the full reasoning) so a
|
|
66767
67435
|
// consumer whose own CSS also sets display (e.g. Popup's flex prop)
|
|
66768
67436
|
// can't silently defeat showModal()/close()'s native open/close — see
|
|
@@ -67138,7 +67806,7 @@ const css$D = /* css */`
|
|
|
67138
67806
|
without this it would flash visible for one frame on mount, before
|
|
67139
67807
|
openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
|
|
67140
67808
|
set from usePopoverProps' own contentProps (recomputed from
|
|
67141
|
-
openController.
|
|
67809
|
+
openController.openedInDom on every render, present from the very first
|
|
67142
67810
|
one — see there for why), then toggled by plain
|
|
67143
67811
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
67144
67812
|
display override: removing the attribute just lets this rule stop
|
|
@@ -68494,7 +69162,7 @@ const usePopoverProps = props => {
|
|
|
68494
69162
|
"navi-out-of-flow": "",
|
|
68495
69163
|
"baseClassName": "navi_popover_backdrop",
|
|
68496
69164
|
"aria-hidden": "true",
|
|
68497
|
-
// Recomputed fresh on every render from openController.
|
|
69165
|
+
// Recomputed fresh on every render from openController.openedInDom (not a
|
|
68498
69166
|
// frozen mount-time constant, same pattern as navi-hidden just below)
|
|
68499
69167
|
// rather than driven through a mount-time layout effect — its own
|
|
68500
69168
|
// actual open/close toggling is still done entirely imperatively
|
|
@@ -68569,7 +69237,7 @@ const usePopoverProps = props => {
|
|
|
68569
69237
|
// starting-hidden default) — present from this very first render so
|
|
68570
69238
|
// there's no gap for the browser to ever paint it visible before
|
|
68571
69239
|
// anything has actually opened it. Recomputed fresh on every render from
|
|
68572
|
-
// openController.
|
|
69240
|
+
// openController.openedInDom (not a frozen mount-time constant) — Preact
|
|
68573
69241
|
// only touches the DOM for a prop whose value actually changed since
|
|
68574
69242
|
// the last render, so as long as this always reflects the *current*
|
|
68575
69243
|
// truth, it never fights the imperative
|
|
@@ -72995,7 +73663,6 @@ const ListUI = props => {
|
|
|
72995
73663
|
// What the runs ask for and stand for, in items: a source paginates in items,
|
|
72996
73664
|
// and asks for the first picture's items and the rest in one round trip.
|
|
72997
73665
|
listItems.pageSize = PAGE_SIZE_DEFAULT;
|
|
72998
|
-
listItems.scrolled = scrolled ?? defaultScrolled;
|
|
72999
73666
|
const {
|
|
73000
73667
|
virtualItemSizeSignal,
|
|
73001
73668
|
renderWindow,
|
|
@@ -73335,6 +74002,45 @@ const useListScrollSync = ({
|
|
|
73335
74002
|
columns
|
|
73336
74003
|
}) => {
|
|
73337
74004
|
const debugScroll = useDebugScroll();
|
|
74005
|
+
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
74006
|
+
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
74007
|
+
// the caller moving the list, so the hold is armed again — that is what makes
|
|
74008
|
+
// it controlled. A `defaultScrolled` that changes is where the list opens,
|
|
74009
|
+
// read again from something that answered again (a count fresher than the
|
|
74010
|
+
// one kept from the previous visit): followed for as long as nobody has
|
|
74011
|
+
// moved the list, and compared by value, since it is said at every render.
|
|
74012
|
+
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
74013
|
+
// list opens where it opens.
|
|
74014
|
+
const startPlaceRef = useRef(null);
|
|
74015
|
+
if (startPlaceRef.current === null) {
|
|
74016
|
+
startPlaceRef.current = {
|
|
74017
|
+
userTookOver: false,
|
|
74018
|
+
wanted: scrolled ?? defaultScrolled,
|
|
74019
|
+
scrolled,
|
|
74020
|
+
defaultScrolled,
|
|
74021
|
+
way: null,
|
|
74022
|
+
arrived: false
|
|
74023
|
+
};
|
|
74024
|
+
} else {
|
|
74025
|
+
const place = startPlaceRef.current;
|
|
74026
|
+
if (place.scrolled !== scrolled) {
|
|
74027
|
+
place.scrolled = scrolled;
|
|
74028
|
+
place.defaultScrolled = defaultScrolled;
|
|
74029
|
+
place.wanted = scrolled ?? defaultScrolled;
|
|
74030
|
+
place.userTookOver = false;
|
|
74031
|
+
} else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
|
|
74032
|
+
place.defaultScrolled = defaultScrolled;
|
|
74033
|
+
if (!place.userTookOver) {
|
|
74034
|
+
place.wanted = defaultScrolled;
|
|
74035
|
+
// Placed again from where it stands: the way to the previous place
|
|
74036
|
+
// says nothing about whose the next scrolls are.
|
|
74037
|
+
place.way = null;
|
|
74038
|
+
place.arrived = false;
|
|
74039
|
+
}
|
|
74040
|
+
}
|
|
74041
|
+
}
|
|
74042
|
+
const scrolledWanted = startPlaceRef.current.wanted;
|
|
74043
|
+
listItems.scrolled = scrolledWanted;
|
|
73338
74044
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
73339
74045
|
// state: the render moves it where the list is held (holdWindow) without a
|
|
73340
74046
|
// commit of its own.
|
|
@@ -73349,7 +74055,7 @@ const useListScrollSync = ({
|
|
|
73349
74055
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
73350
74056
|
inLines: Boolean(columns),
|
|
73351
74057
|
windowLeavesItemsOut,
|
|
73352
|
-
scrolledWanted
|
|
74058
|
+
scrolledWanted,
|
|
73353
74059
|
// The fillers hold the room of items above the screen at this size: a
|
|
73354
74060
|
// size that changes moves what is on screen, like items landing above it.
|
|
73355
74061
|
beforeSizeChange: () => listItems.captureAnchor()
|
|
@@ -73680,25 +74386,7 @@ const useListScrollSync = ({
|
|
|
73680
74386
|
const newEnd = newStart + windowSize;
|
|
73681
74387
|
updateRenderWindow(newStart, newEnd);
|
|
73682
74388
|
};
|
|
73683
|
-
|
|
73684
|
-
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
73685
|
-
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
73686
|
-
// the caller moving the list, so the hold is armed again — that is what makes
|
|
73687
|
-
// it controlled.
|
|
73688
|
-
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
73689
|
-
// list opens where it opens.
|
|
73690
|
-
const scrolledWanted = scrolled ?? defaultScrolled;
|
|
73691
74389
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
73692
|
-
const startPlaceRef = useRef({
|
|
73693
|
-
userTookOver: false,
|
|
73694
|
-
wanted: scrolledWanted,
|
|
73695
|
-
way: null,
|
|
73696
|
-
arrived: false
|
|
73697
|
-
});
|
|
73698
|
-
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
73699
|
-
startPlaceRef.current.wanted = scrolledWanted;
|
|
73700
|
-
startPlaceRef.current.userTookOver = false;
|
|
73701
|
-
}
|
|
73702
74390
|
// Only one thing owns the scroll at a time: while the list is holding itself
|
|
73703
74391
|
// somewhere, the anchoring stays out of it (holding an item still is precisely
|
|
73704
74392
|
// not being at the end anymore once what is above it shrinks).
|
|
@@ -77418,6 +78106,17 @@ const useItemStore = ({
|
|
|
77418
78106
|
}
|
|
77419
78107
|
const pages = pagesRef.current;
|
|
77420
78108
|
const [, setPageVersion] = useState(0);
|
|
78109
|
+
// A list on screen is part of what the page reads: the composition it reads
|
|
78110
|
+
// is kept with the page for the next document (see keepPageOnScreen).
|
|
78111
|
+
useLayoutEffect(() => {
|
|
78112
|
+
if (inMemory || typeof itemsAction !== "function") {
|
|
78113
|
+
return undefined;
|
|
78114
|
+
}
|
|
78115
|
+
if (!itemsAction.isRangeReader) {
|
|
78116
|
+
return undefined;
|
|
78117
|
+
}
|
|
78118
|
+
return showRangeReader(itemsAction);
|
|
78119
|
+
}, [inMemory, itemsAction]);
|
|
77421
78120
|
// The ranks whose item is from before: the run came back to the screen, or
|
|
77422
78121
|
// the source said the collection moved. They stay drawn until a page confirms
|
|
77423
78122
|
// or replaces them, rank by rank. A page covers what the window framed when it
|
|
@@ -77551,6 +78250,7 @@ const useItemStore = ({
|
|
|
77551
78250
|
}
|
|
77552
78251
|
}
|
|
77553
78252
|
if (typeof itemsAction === "function" && itemsAction.trimComposition) {
|
|
78253
|
+
itemsAction.frameComposition(windowFrom, windowTo);
|
|
77554
78254
|
itemsAction.trimComposition(keepFrom, keepTo, budget);
|
|
77555
78255
|
}
|
|
77556
78256
|
},
|
|
@@ -78120,6 +78820,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78120
78820
|
* room has to be restated as a number by whoever asks. The `index` and
|
|
78121
78821
|
* `visibleCount` it also hands out say where to aim before the item is found,
|
|
78122
78822
|
* and how many items to draw before the first paint (see `renderBudget`).
|
|
78823
|
+
* A value that changes before anyone has moved the list is followed (a place
|
|
78824
|
+
* read from a count that answered again): it is compared by value, so it can
|
|
78825
|
+
* be computed at every render.
|
|
78123
78826
|
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
78124
78827
|
* The same, but held: the list goes back there every time this changes, even
|
|
78125
78828
|
* after the user has scrolled — the caller owns where the list is (see
|
|
@@ -93540,5 +94243,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
93540
94243
|
})
|
|
93541
94244
|
});
|
|
93542
94245
|
|
|
93543
|
-
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 };
|
|
94246
|
+
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 };
|
|
93544
94247
|
//# sourceMappingURL=jsenv_navi.js.map
|