aberdeen 0.4.0 → 0.5.0
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/LICENSE.txt +1 -1
- package/README.md +134 -96
- package/dist/aberdeen.d.ts +637 -528
- package/dist/aberdeen.js +1144 -1957
- package/dist/aberdeen.js.map +11 -1
- package/dist/helpers/reverseSortedSet.d.ts +91 -0
- package/dist/prediction.d.ts +7 -3
- package/dist/prediction.js +77 -93
- package/dist/prediction.js.map +10 -1
- package/dist/route.d.ts +36 -19
- package/dist/route.js +131 -158
- package/dist/route.js.map +10 -1
- package/dist/transitions.js +30 -70
- package/dist/transitions.js.map +10 -1
- package/dist-min/aberdeen.js +7 -2
- package/dist-min/aberdeen.js.map +11 -1
- package/dist-min/prediction.js +4 -2
- package/dist-min/prediction.js.map +10 -1
- package/dist-min/route.js +4 -2
- package/dist-min/route.js.map +10 -1
- package/dist-min/transitions.js +4 -2
- package/dist-min/transitions.js.map +10 -1
- package/package.json +20 -23
- package/src/aberdeen.ts +1918 -1814
- package/src/helpers/reverseSortedSet.ts +188 -0
- package/src/prediction.ts +14 -9
- package/src/route.ts +81 -64
- package/src/transitions.ts +1 -14
- package/dist-min/aberdeen.d.ts +0 -601
- package/dist-min/prediction.d.ts +0 -29
- package/dist-min/route.d.ts +0 -30
- package/dist-min/transitions.d.ts +0 -18
package/dist/prediction.js
CHANGED
|
@@ -1,110 +1,94 @@
|
|
|
1
|
-
|
|
1
|
+
// src/prediction.ts
|
|
2
|
+
import { withEmitHandler, defaultEmitHandler } from "./aberdeen.js";
|
|
2
3
|
function recordPatch(func) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const recordingPatch = new Map;
|
|
5
|
+
withEmitHandler(function(target, index, newData, oldData) {
|
|
6
|
+
addToPatch(recordingPatch, target, index, newData, oldData);
|
|
7
|
+
}, func);
|
|
8
|
+
return recordingPatch;
|
|
8
9
|
}
|
|
9
10
|
function addToPatch(patch, collection, index, newData, oldData) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
let collectionMap = patch.get(collection);
|
|
12
|
+
if (!collectionMap) {
|
|
13
|
+
collectionMap = new Map;
|
|
14
|
+
patch.set(collection, collectionMap);
|
|
15
|
+
}
|
|
16
|
+
let prev = collectionMap.get(index);
|
|
17
|
+
if (prev)
|
|
18
|
+
oldData = prev[1];
|
|
19
|
+
if (newData === oldData)
|
|
20
|
+
collectionMap.delete(index);
|
|
21
|
+
else
|
|
22
|
+
collectionMap.set(index, [newData, oldData]);
|
|
22
23
|
}
|
|
23
24
|
function emitPatch(patch) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
25
|
+
for (let [collection, collectionMap] of patch) {
|
|
26
|
+
for (let [index, [newData, oldData]] of collectionMap) {
|
|
27
|
+
defaultEmitHandler(collection, index, newData, oldData);
|
|
28
28
|
}
|
|
29
|
+
}
|
|
29
30
|
}
|
|
30
31
|
function mergePatch(target, source, reverse = false) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
32
|
+
for (let [collection, collectionMap] of source) {
|
|
33
|
+
for (let [index, [newData, oldData]] of collectionMap) {
|
|
34
|
+
addToPatch(target, collection, index, reverse ? oldData : newData, reverse ? newData : oldData);
|
|
35
35
|
}
|
|
36
|
+
}
|
|
36
37
|
}
|
|
37
38
|
function silentlyApplyPatch(patch, force = false) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
for (let [collection, collectionMap] of patch) {
|
|
40
|
+
for (let [index, [newData, oldData]] of collectionMap) {
|
|
41
|
+
let actualData = collection[index];
|
|
42
|
+
if (actualData !== oldData) {
|
|
43
|
+
if (force)
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
throw new Error(`Applying invalid patch: data ${actualData} is unequal to expected old data ${oldData} for index ${index}`);
|
|
46
|
+
}, 0);
|
|
47
|
+
else
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
}
|
|
52
|
+
for (let [collection, collectionMap] of patch) {
|
|
53
|
+
for (let [index, [newData, oldData]] of collectionMap) {
|
|
54
|
+
collection[index] = newData;
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* to immediately reflect state (as closely as possible) that we expect the server
|
|
63
|
-
* to communicate back to us later on.
|
|
64
|
-
* @returns A `Patch` object. Don't modify it. This is only meant to be passed to `applyCanon`.
|
|
65
|
-
*/
|
|
66
|
-
export function applyPrediction(predictFunc) {
|
|
67
|
-
let patch = recordPatch(predictFunc);
|
|
68
|
-
appliedPredictions.push(patch);
|
|
69
|
-
emitPatch(patch);
|
|
70
|
-
return patch;
|
|
59
|
+
var appliedPredictions = [];
|
|
60
|
+
function applyPrediction(predictFunc) {
|
|
61
|
+
let patch = recordPatch(predictFunc);
|
|
62
|
+
appliedPredictions.push(patch);
|
|
63
|
+
emitPatch(patch);
|
|
64
|
+
return patch;
|
|
71
65
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
mergePatch(resultPatch, prediction, true);
|
|
91
|
-
silentlyApplyPatch(resultPatch, true);
|
|
92
|
-
for (let prediction of dropPredictions) {
|
|
93
|
-
let pos = appliedPredictions.indexOf(prediction);
|
|
94
|
-
if (pos >= 0)
|
|
95
|
-
appliedPredictions.splice(pos, 1);
|
|
96
|
-
}
|
|
97
|
-
if (canonFunc)
|
|
98
|
-
mergePatch(resultPatch, recordPatch(canonFunc));
|
|
99
|
-
for (let idx = 0; idx < appliedPredictions.length; idx++) {
|
|
100
|
-
if (silentlyApplyPatch(appliedPredictions[idx])) {
|
|
101
|
-
mergePatch(resultPatch, appliedPredictions[idx]);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
appliedPredictions.splice(idx, 1);
|
|
105
|
-
idx--;
|
|
106
|
-
}
|
|
66
|
+
function applyCanon(canonFunc, dropPredictions = []) {
|
|
67
|
+
let resultPatch = new Map;
|
|
68
|
+
for (let prediction of appliedPredictions)
|
|
69
|
+
mergePatch(resultPatch, prediction, true);
|
|
70
|
+
silentlyApplyPatch(resultPatch, true);
|
|
71
|
+
for (let prediction of dropPredictions) {
|
|
72
|
+
let pos = appliedPredictions.indexOf(prediction);
|
|
73
|
+
if (pos >= 0)
|
|
74
|
+
appliedPredictions.splice(pos, 1);
|
|
75
|
+
}
|
|
76
|
+
if (canonFunc)
|
|
77
|
+
mergePatch(resultPatch, recordPatch(canonFunc));
|
|
78
|
+
for (let idx = 0;idx < appliedPredictions.length; idx++) {
|
|
79
|
+
if (silentlyApplyPatch(appliedPredictions[idx])) {
|
|
80
|
+
mergePatch(resultPatch, appliedPredictions[idx]);
|
|
81
|
+
} else {
|
|
82
|
+
appliedPredictions.splice(idx, 1);
|
|
83
|
+
idx--;
|
|
107
84
|
}
|
|
108
|
-
|
|
85
|
+
}
|
|
86
|
+
emitPatch(resultPatch);
|
|
109
87
|
}
|
|
110
|
-
|
|
88
|
+
export {
|
|
89
|
+
applyPrediction,
|
|
90
|
+
applyCanon
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//# debugId=64BCD82AC2BC0BA664756E2164756E21
|
|
94
|
+
//# sourceMappingURL=prediction.js.map
|
package/dist/prediction.js.map
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/prediction.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import {withEmitHandler, defaultEmitHandler} from './aberdeen.js'\nimport type { DatumType, TargetType } from './aberdeen.js';\n\n/** \n * Represents a set of changes that can be applied to proxied objects.\n * This is an opaque type - its internal structure is not part of the public API.\n * @private\n */\nexport type Patch = Map<TargetType, Map<any, [DatumType, DatumType]>>;\n\n\nfunction recordPatch(func: () => void): Patch {\n\tconst recordingPatch = new Map()\n\twithEmitHandler(function(target, index, newData, oldData) {\n\t\taddToPatch(recordingPatch, target, index, newData, oldData)\n\t}, func)\n\treturn recordingPatch\n}\n\nfunction addToPatch(patch: Patch, collection: TargetType, index: any, newData: DatumType, oldData: DatumType) {\n\tlet collectionMap = patch.get(collection)\n\tif (!collectionMap) {\n\t\tcollectionMap = new Map()\n\t\tpatch.set(collection, collectionMap)\n\t}\n\tlet prev = collectionMap.get(index)\n\tif (prev) oldData = prev[1]\n\tif (newData === oldData) collectionMap.delete(index)\n\telse collectionMap.set(index, [newData, oldData])\n}\n\nfunction emitPatch(patch: Patch) {\n\tfor(let [collection, collectionMap] of patch) {\n\t\tfor(let [index, [newData, oldData]] of collectionMap) {\n\t\t\tdefaultEmitHandler(collection, index, newData, oldData);\n\t\t}\n\t}\n}\n\nfunction mergePatch(target: Patch, source: Patch, reverse: boolean = false) {\n\tfor(let [collection, collectionMap] of source) {\n\t\tfor(let [index, [newData, oldData]] of collectionMap) {\n\t\t\taddToPatch(target, collection, index, reverse ? oldData : newData, reverse ? newData : oldData)\n\t\t}\n\t}\n}\n\nfunction silentlyApplyPatch(patch: Patch, force: boolean = false): boolean {\n\tfor(let [collection, collectionMap] of patch) {\n\t\tfor(let [index, [newData, oldData]] of collectionMap) {\n\t\t\tlet actualData = (collection as any)[index]\n\t\t\tif (actualData !== oldData) {\n\t\t\t\tif (force) setTimeout(() => { throw new Error(`Applying invalid patch: data ${actualData} is unequal to expected old data ${oldData} for index ${index}`)}, 0)\n\t\t\t\telse return false\n\t\t\t}\n\t\t}\n\t}\n\tfor(let [collection, collectionMap] of patch) {\n\t\tfor(let [index, [newData, oldData]] of collectionMap) {\n\t\t\t(collection as any)[index] = newData\n\t\t}\n\t}\n\treturn true\n}\n\n\nconst appliedPredictions: Array<Patch> = []\n\n/**\n * Run the provided function, while treating all changes to Observables as predictions,\n * meaning they will be reverted when changes come back from the server (or some other\n * async source).\n * @param predictFunc The function to run. It will generally modify some Observables\n * \tto immediately reflect state (as closely as possible) that we expect the server\n * to communicate back to us later on.\n * @returns A `Patch` object. Don't modify it. This is only meant to be passed to `applyCanon`.\n */\nexport function applyPrediction(predictFunc: () => void): Patch {\n\tlet patch = recordPatch(predictFunc)\n\tappliedPredictions.push(patch)\n\temitPatch(patch)\n\treturn patch\n}\n\n/**\n * Temporarily revert all outstanding predictions, optionally run the provided function\n * (which will generally make authoritative changes to the data based on a server response),\n * and then attempt to reapply the predictions on top of the new canonical state, dropping \n * any predictions that can no longer be applied cleanly (the data has been modified) or\n * that were specified in `dropPredictions`.\n * \n * All of this is done such that redraws are only triggered if the overall effect is an\n * actual change to an `Observable`.\n * @param canonFunc The function to run without any predictions applied. This will typically\n * make authoritative changes to the data, based on a server response.\n * @param dropPredictions An optional list of predictions (as returned by `applyPrediction`)\n * to undo. Typically, when a server response for a certain request is being handled,\n * you'd want to drop the prediction that was done for that request.\n */\nexport function applyCanon(canonFunc?: (() => void), dropPredictions: Array<Patch> = []) {\n\t\n\tlet resultPatch = new Map()\n\tfor(let prediction of appliedPredictions) mergePatch(resultPatch, prediction, true)\n\tsilentlyApplyPatch(resultPatch, true)\n\n\tfor(let prediction of dropPredictions) {\n\t\tlet pos = appliedPredictions.indexOf(prediction)\n\t\tif (pos >= 0) appliedPredictions.splice(pos, 1)\n\t}\n\tif (canonFunc) mergePatch(resultPatch, recordPatch(canonFunc))\n\n\tfor(let idx=0; idx<appliedPredictions.length; idx++) {\n\t\tif (silentlyApplyPatch(appliedPredictions[idx])) {\n\t\t\tmergePatch(resultPatch, appliedPredictions[idx])\n\t\t} else {\n\t\t\tappliedPredictions.splice(idx, 1)\n\t\t\tidx--\n\t\t}\n\t}\n\n\temitPatch(resultPatch)\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "64BCD82AC2BC0BA664756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/route.d.ts
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
|
-
import { Store } from './aberdeen.js';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
* - `path`: The current path of the URL split into components. For instance `/` or `/users/123/feed`. Updates will be reflected in the URL and will *push* a new entry to the browser history.
|
|
5
|
-
* - `p`: Array containing the path segments. For instance `[]` or `['users', 123, 'feed']`. Updates will be reflected in the URL and will *push* a new entry to the browser history. Also, the values of `p` and `path` will be synced.
|
|
6
|
-
* - `search`: An observable object containing search parameters (a split up query string). For instance `{order: "date", title: "something"}` or just `{}`. By default, updates will be reflected in the URL, replacing the current history state.
|
|
7
|
-
* - `hash`: The document hash part of the URL. For instance `"#section-title"`. It can also be an empty string. Updates will be reflected in the URL, modifying the current history state.
|
|
8
|
-
* - `id`: A part of the browser history *state* that is considered part of the page *identify*, meaning changes will (by default) cause a history push, and when going *back*, it must match.
|
|
9
|
-
* - `aux`: The auxiliary part of the browser history *state*, not considered part of the page *identity*. Changes will be reflected in the browser history using a replace.
|
|
10
|
-
* - `depth`: The navigation depth of the current session. Starts at 1. Writing to this property has no effect.
|
|
11
|
-
* - `nav`: The navigation action that got us to this page. Writing to this property has no effect.
|
|
12
|
-
* - `"load"`: An initial page load.
|
|
13
|
-
* - `"back"` or `"forward"`: When we navigated backwards or forwards in the stack.
|
|
14
|
-
* - `"push"`: When we added a new page on top of the stack.
|
|
2
|
+
* The class for the singleton `route` object.
|
|
15
3
|
*
|
|
16
|
-
* The following key may also be written to `route` but will be immediately and silently removed:
|
|
17
|
-
* - `mode`: As described above, this library takes a best guess about whether pushing an item to the browser history makes sense or not. When `mode` is...
|
|
18
|
-
* - `"push"`: Force creation of a new browser history entry.
|
|
19
|
-
* - `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
20
|
-
* - `"back"`: Unwind the history (like repeatedly pressing the *back* button) until we find a page that matches the given `path` and `id`, and then *replace* that state by the full given state.
|
|
21
4
|
*/
|
|
22
|
-
export declare
|
|
5
|
+
export declare class Route {
|
|
6
|
+
/** The current path of the URL split into components. For instance `/` or `/users/123/feed`. Updates will be reflected in the URL and will *push* a new entry to the browser history. */
|
|
7
|
+
path: string;
|
|
8
|
+
/** Array containing the path segments. For instance `[]` or `['users', 123, 'feed']`. Updates will be reflected in the URL and will *push* a new entry to the browser history. Also, the values of `p` and `path` will be synced. */
|
|
9
|
+
p: string[];
|
|
10
|
+
/** An observable object containing search parameters (a split up query string). For instance `{order: "date", title: "something"}` or just `{}`. By default, updates will be reflected in the URL, replacing the current history state. */
|
|
11
|
+
hash: string;
|
|
12
|
+
/** A part of the browser history *state* that is considered part of the page *identify*, meaning changes will (by default) cause a history push, and when going *back*, it must match. */
|
|
13
|
+
search: Record<string, string>;
|
|
14
|
+
/** The `hash` interpreted as search parameters. So `"a=x&b=y"` becomes `{a: "x", b: "y"}`. */
|
|
15
|
+
id: Record<string, any>;
|
|
16
|
+
/** The auxiliary part of the browser history *state*, not considered part of the page *identity*. Changes will be reflected in the browser history using a replace. */
|
|
17
|
+
aux: Record<string, any>;
|
|
18
|
+
/** The navigation depth of the current session. Starts at 1. Writing to this property has no effect. */
|
|
19
|
+
depth: number;
|
|
20
|
+
/** The navigation action that got us to this page. Writing to this property has no effect.
|
|
21
|
+
- `"load"`: An initial page load.
|
|
22
|
+
- `"back"` or `"forward"`: When we navigated backwards or forwards in the stack.
|
|
23
|
+
- `"push"`: When we added a new page on top of the stack.
|
|
24
|
+
*/
|
|
25
|
+
nav: 'load' | 'back' | 'forward' | 'push';
|
|
26
|
+
/** As described above, this library takes a best guess about whether pushing an item to the browser history makes sense or not. When `mode` is...
|
|
27
|
+
- `"push"`: Force creation of a new browser history entry.
|
|
28
|
+
- `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
29
|
+
- `"back"`: Unwind the history (like repeatedly pressing the *back* button) until we find a page that matches the given `path` and `id` (or that is the first page in our stack), and then *replace* that state by the full given state.
|
|
30
|
+
The `mode` key can be written to `route` but will be immediately and silently removed.
|
|
31
|
+
*/
|
|
32
|
+
mode: 'push' | 'replace' | 'back' | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The singleton {@link Route} object reflecting the current URL and browser history state. You can make changes to it to affect the URL and browser history. See {@link Route} for details.
|
|
36
|
+
*/
|
|
37
|
+
export declare const route: Route;
|
|
23
38
|
/**
|
|
24
39
|
* Restore and store the vertical and horizontal scroll position for
|
|
25
40
|
* the parent element to the page state.
|
|
26
41
|
*
|
|
27
42
|
* @param {string} name - A unique (within this page) name for this
|
|
28
43
|
* scrollable element. Defaults to 'main'.
|
|
44
|
+
*
|
|
45
|
+
* The scroll position will be persisted in `route.aux.scroll.<name>`.
|
|
29
46
|
*/
|
|
30
47
|
export declare function persistScroll(name?: string): void;
|
package/dist/route.js
CHANGED
|
@@ -1,182 +1,155 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* - `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
20
|
-
* - `"back"`: Unwind the history (like repeatedly pressing the *back* button) until we find a page that matches the given `path` and `id`, and then *replace* that state by the full given state.
|
|
21
|
-
*/
|
|
22
|
-
// Idea: split `state` into `pstate` (primary) and `astate` (auxilary). The former is used to determine if new pages should be pushed and if, when moving back, a page matches.
|
|
23
|
-
// Moving back is done by just doing history.back() while depth > 1, and repeating in handleLocationUpdate until matching page is found (or overriding base page).
|
|
24
|
-
export const route = new Store();
|
|
25
|
-
let stateRoute = {
|
|
26
|
-
nonce: -1,
|
|
27
|
-
depth: 0,
|
|
1
|
+
// src/route.ts
|
|
2
|
+
import { getParentElement, runQueue, clean, proxy, observe, immediateObserve, unproxy, clone } from "./aberdeen.js";
|
|
3
|
+
|
|
4
|
+
class Route {
|
|
5
|
+
path;
|
|
6
|
+
p;
|
|
7
|
+
hash;
|
|
8
|
+
search;
|
|
9
|
+
id;
|
|
10
|
+
aux;
|
|
11
|
+
depth = 1;
|
|
12
|
+
nav = "load";
|
|
13
|
+
mode;
|
|
14
|
+
}
|
|
15
|
+
var route = proxy(new Route);
|
|
16
|
+
var stateRoute = {
|
|
17
|
+
nonce: -1,
|
|
18
|
+
depth: 0
|
|
28
19
|
};
|
|
29
|
-
// Reflect changes to the browser URL (back/forward navigation) in the `route` and `stack`.
|
|
30
20
|
function handleLocationUpdate(event) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
depth: stateRoute.depth,
|
|
63
|
-
nav,
|
|
64
|
-
});
|
|
65
|
-
// Forward or back event. Redraw synchronously, because we can!
|
|
66
|
-
if (event)
|
|
67
|
-
runQueue();
|
|
21
|
+
let state = event?.state || {};
|
|
22
|
+
let nav = "load";
|
|
23
|
+
if (state.route?.nonce == null) {
|
|
24
|
+
state.route = {
|
|
25
|
+
nonce: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
|
|
26
|
+
depth: 1
|
|
27
|
+
};
|
|
28
|
+
history.replaceState(state, "");
|
|
29
|
+
} else if (stateRoute.nonce === state.route.nonce) {
|
|
30
|
+
nav = state.route.depth > stateRoute.depth ? "forward" : "back";
|
|
31
|
+
}
|
|
32
|
+
stateRoute = state.route;
|
|
33
|
+
if (unproxy(route).mode === "back") {
|
|
34
|
+
route.depth = stateRoute.depth;
|
|
35
|
+
updateHistory();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const search = {};
|
|
39
|
+
for (let [k, v] of new URLSearchParams(location.search)) {
|
|
40
|
+
search[k] = v;
|
|
41
|
+
}
|
|
42
|
+
route.path = location.pathname;
|
|
43
|
+
route.p = location.pathname.slice(1).split("/");
|
|
44
|
+
route.search = search;
|
|
45
|
+
route.hash = location.hash;
|
|
46
|
+
route.id = state.id;
|
|
47
|
+
route.aux = state.aux;
|
|
48
|
+
route.depth = stateRoute.depth;
|
|
49
|
+
route.nav = nav;
|
|
50
|
+
if (event)
|
|
51
|
+
runQueue();
|
|
68
52
|
}
|
|
69
53
|
handleLocationUpdate();
|
|
70
54
|
window.addEventListener("popstate", handleLocationUpdate);
|
|
71
|
-
// These immediate-mode observers will rewrite the data in `route` to its canonical form.
|
|
72
|
-
// We want to to this immediately, so that user-code running immediately after a user-code
|
|
73
|
-
// initiated `set` will see the canonical form (instead of doing a rerender shortly after,
|
|
74
|
-
// or crashing due to non-canonical data).
|
|
75
55
|
function updatePath() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
let path = route.path;
|
|
57
|
+
if (path == null && unproxy(route).p) {
|
|
58
|
+
return updateP();
|
|
59
|
+
}
|
|
60
|
+
path = "" + (path || "/");
|
|
61
|
+
if (!path.startsWith("/"))
|
|
62
|
+
path = "/" + path;
|
|
63
|
+
route.path = path;
|
|
64
|
+
route.p = path.slice(1).split("/");
|
|
85
65
|
}
|
|
86
66
|
immediateObserve(updatePath);
|
|
87
67
|
function updateP() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
route('path').set('/' + p.join('/'));
|
|
101
|
-
}
|
|
68
|
+
const p = route.p;
|
|
69
|
+
if (p == null && unproxy(route).path) {
|
|
70
|
+
return updatePath();
|
|
71
|
+
}
|
|
72
|
+
if (!(p instanceof Array)) {
|
|
73
|
+
console.error(`aberdeen route: 'p' must be a non-empty array, not ${JSON.stringify(p)}`);
|
|
74
|
+
route.p = [""];
|
|
75
|
+
} else if (p.length == 0) {
|
|
76
|
+
route.p = [""];
|
|
77
|
+
} else {
|
|
78
|
+
route.path = "/" + p.join("/");
|
|
79
|
+
}
|
|
102
80
|
}
|
|
103
81
|
immediateObserve(updateP);
|
|
104
82
|
immediateObserve(() => {
|
|
105
|
-
|
|
106
|
-
|
|
83
|
+
if (!route.search || typeof route.search !== "object")
|
|
84
|
+
route.search = {};
|
|
85
|
+
});
|
|
86
|
+
immediateObserve(() => {
|
|
87
|
+
if (!route.id || typeof route.id !== "object")
|
|
88
|
+
route.id = {};
|
|
107
89
|
});
|
|
108
90
|
immediateObserve(() => {
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
if (!route.aux || typeof route.aux !== "object")
|
|
92
|
+
route.aux = {};
|
|
111
93
|
});
|
|
112
94
|
immediateObserve(() => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
let hash = "" + (route.hash || "");
|
|
96
|
+
if (hash && !hash.startsWith("#"))
|
|
97
|
+
hash = "#" + hash;
|
|
98
|
+
route.hash = hash;
|
|
117
99
|
});
|
|
118
100
|
function isSamePage(path, state) {
|
|
119
|
-
|
|
101
|
+
return location.pathname === path && JSON.stringify(history.state.id) === JSON.stringify(state.id);
|
|
120
102
|
}
|
|
121
103
|
function updateHistory() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
route('nav').set('back');
|
|
135
|
-
if (!isSamePage(path, state) && (((_a = history.state.route) === null || _a === void 0 ? void 0 : _a.depth) || 0) > 1) {
|
|
136
|
-
history.back();
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
mode = 'replace';
|
|
140
|
-
// We'll replace the state async, to give the history.go the time to take affect first.
|
|
141
|
-
//setTimeout(() => history.replaceState(state, '', url), 0)
|
|
142
|
-
}
|
|
143
|
-
if (mode)
|
|
144
|
-
route('mode').delete();
|
|
145
|
-
const search = new URLSearchParams(route('search').get()).toString();
|
|
146
|
-
const url = (search ? path + '?' + search : path) + route('hash').get();
|
|
147
|
-
if (mode === 'push' || (!mode && !isSamePage(path, state))) {
|
|
148
|
-
stateRoute.depth++; // stateRoute === state.route
|
|
149
|
-
history.pushState(state, '', url);
|
|
150
|
-
route.merge({
|
|
151
|
-
nav: 'push',
|
|
152
|
-
depth: stateRoute.depth
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
// Default to `push` when the URL changed or top-level state keys changed.
|
|
157
|
-
history.replaceState(state, '', url);
|
|
104
|
+
let mode = route.mode;
|
|
105
|
+
const state = {
|
|
106
|
+
id: clone(route.id),
|
|
107
|
+
aux: clone(route.aux),
|
|
108
|
+
route: stateRoute
|
|
109
|
+
};
|
|
110
|
+
const path = route.path;
|
|
111
|
+
if (mode === "back") {
|
|
112
|
+
route.nav = "back";
|
|
113
|
+
if (!isSamePage(path, state) && (history.state.route?.depth || 0) > 1) {
|
|
114
|
+
history.back();
|
|
115
|
+
return;
|
|
158
116
|
}
|
|
117
|
+
mode = "replace";
|
|
118
|
+
}
|
|
119
|
+
if (mode)
|
|
120
|
+
route.mode = undefined;
|
|
121
|
+
const search = new URLSearchParams(route.search).toString();
|
|
122
|
+
const url = (search ? path + "?" + search : path) + route.hash;
|
|
123
|
+
if (mode === "push" || !mode && !isSamePage(path, state)) {
|
|
124
|
+
stateRoute.depth++;
|
|
125
|
+
history.pushState(state, "", url);
|
|
126
|
+
route.nav = "push";
|
|
127
|
+
route.depth = stateRoute.depth;
|
|
128
|
+
} else {
|
|
129
|
+
history.replaceState(state, "", url);
|
|
130
|
+
}
|
|
159
131
|
}
|
|
160
|
-
// This deferred-mode observer will update the URL and history based on `route` changes.
|
|
161
132
|
observe(updateHistory);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
function onScroll() {
|
|
178
|
-
route('mode').set('replace');
|
|
179
|
-
route('state', 'scroll', name).set({ scrollTop: el.scrollTop, scrollLeft: el.scrollLeft });
|
|
180
|
-
}
|
|
133
|
+
function persistScroll(name = "main") {
|
|
134
|
+
const el = getParentElement();
|
|
135
|
+
el.addEventListener("scroll", onScroll);
|
|
136
|
+
clean(() => el.removeEventListener("scroll", onScroll));
|
|
137
|
+
let restore = unproxy(route).aux.scroll?.name;
|
|
138
|
+
if (restore) {
|
|
139
|
+
Object.assign(el, restore);
|
|
140
|
+
}
|
|
141
|
+
function onScroll() {
|
|
142
|
+
route.mode = "replace";
|
|
143
|
+
if (!route.aux.scroll)
|
|
144
|
+
route.aux.scroll = {};
|
|
145
|
+
route.aux.scroll[name] = { scrollTop: el.scrollTop, scrollLeft: el.scrollLeft };
|
|
146
|
+
}
|
|
181
147
|
}
|
|
182
|
-
|
|
148
|
+
export {
|
|
149
|
+
route,
|
|
150
|
+
persistScroll,
|
|
151
|
+
Route
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
//# debugId=917712AC1764DFEF64756E2164756E21
|
|
155
|
+
//# sourceMappingURL=route.js.map
|