aberdeen 0.2.4 → 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 +140 -99
- package/dist/aberdeen.d.ts +649 -512
- package/dist/aberdeen.js +1147 -1704
- 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 +44 -13
- package/dist/route.js +138 -112
- package/dist/route.js.map +10 -1
- package/dist/transitions.d.ts +2 -2
- package/dist/transitions.js +30 -63
- 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 +1956 -1696
- package/src/helpers/reverseSortedSet.ts +188 -0
- package/src/prediction.ts +14 -9
- package/src/route.ts +149 -69
- package/src/transitions.ts +26 -43
- package/dist-min/aberdeen.d.ts +0 -573
- package/dist-min/prediction.d.ts +0 -29
- package/dist-min/route.d.ts +0 -16
- package/dist-min/transitions.d.ts +0 -18
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
type Item<T> = T & {[idx: symbol]: Item<T>}
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A set-like collection of objects that can do iteration sorted by a specified index property.
|
|
5
|
+
* It also allows retrieving an object by its index property, and quickly getting the object
|
|
6
|
+
* that comes immediately after a given object.
|
|
7
|
+
*
|
|
8
|
+
* It's implemented as a skiplist, maintaining all meta-data as part of the objects that it
|
|
9
|
+
* is tracking, for performance.
|
|
10
|
+
*/
|
|
11
|
+
export class ReverseSortedSet<T extends object> {
|
|
12
|
+
// A fake item, that is not actually T, but *does* contain symbols pointing at the first item for each level.
|
|
13
|
+
private tail: Item<T>
|
|
14
|
+
// As every SkipList instance has its own symbols, an object can be included in more than one SkipList.
|
|
15
|
+
private symbols: symbol[]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create an empty SortedSet.
|
|
19
|
+
*
|
|
20
|
+
* @param keyProp The name of the property that should be used as the index of this collection. Comparison
|
|
21
|
+
* using `<` will be done on this property, so it should probably be a number or a string (or something that
|
|
22
|
+
* has a useful toString-conversion).
|
|
23
|
+
*/
|
|
24
|
+
constructor(private keyProp: keyof T) {
|
|
25
|
+
this.tail = {} as Item<T>
|
|
26
|
+
this.symbols = [Symbol(0)]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add an object to the `SortedSet`.
|
|
31
|
+
*
|
|
32
|
+
* @param item The object to be added to the set. One or more properties with
|
|
33
|
+
* `Symbol` keys will be added to it, for `SortedSet` internals.
|
|
34
|
+
* @returns `true` if the item was added, or `false` if it was *not* added
|
|
35
|
+
* because the item already was part of this set.
|
|
36
|
+
*
|
|
37
|
+
* Note that though an item object may only be part of a particular `SortedSet`
|
|
38
|
+
* once, index properties may be duplicate and an item object may be part of
|
|
39
|
+
* more than one `SortedSet`.
|
|
40
|
+
*
|
|
41
|
+
* **IMPORTANT:** After adding an object, do not modify its index property,
|
|
42
|
+
* as this will lead to undefined (broken) behavior on the entire set.
|
|
43
|
+
*
|
|
44
|
+
* Time complexity: O(log n)
|
|
45
|
+
*/
|
|
46
|
+
add(item: T): boolean {
|
|
47
|
+
if (this.symbols[0] in item) return false // Already included
|
|
48
|
+
|
|
49
|
+
// Start at level 1. Keep upping the level by 1 with 1/8 chance.
|
|
50
|
+
const level = 1 + (Math.clz32(Math.random() * 0xFFFFFFFF) >> 2)
|
|
51
|
+
for(let l = this.symbols.length; l < level; l++) this.symbols.push(Symbol(l))
|
|
52
|
+
|
|
53
|
+
const keyProp = this.keyProp
|
|
54
|
+
const key = item[keyProp]
|
|
55
|
+
|
|
56
|
+
let prev: Item<T> | undefined
|
|
57
|
+
let current: Item<T> = this.tail;
|
|
58
|
+
for (let l = this.symbols.length-1; l>=0; l--) {
|
|
59
|
+
const symbol = this.symbols[l]
|
|
60
|
+
while ((prev = current[symbol] as Item<T>) && prev[keyProp] > key) current = prev;
|
|
61
|
+
if (l < level) {
|
|
62
|
+
(item as any)[symbol] = current[symbol];
|
|
63
|
+
(current as any)[symbol] = item;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true // Added
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param item An object to test for inclusion in the set.
|
|
72
|
+
* @returns true if this object item is already part of the set.
|
|
73
|
+
*/
|
|
74
|
+
has(item: T): boolean {
|
|
75
|
+
return this.symbols[0] in item;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove and return the last item.
|
|
80
|
+
* @returns what was previously the last item in the sorted set, or `undefined` if the set was empty.
|
|
81
|
+
*/
|
|
82
|
+
fetchLast(): T | undefined {
|
|
83
|
+
let item = this.tail[this.symbols[0]];
|
|
84
|
+
if (item) {
|
|
85
|
+
this.remove(item);
|
|
86
|
+
return item;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @returns whether the set is empty (`true`) or has at least one item (`false`).
|
|
92
|
+
*/
|
|
93
|
+
isEmpty(): boolean {
|
|
94
|
+
return this.tail[this.symbols[0]] === undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Retrieve an item object based on its index property value.
|
|
99
|
+
*
|
|
100
|
+
* @param indexValue The index property value to search for.
|
|
101
|
+
* @returns `undefined` if the index property value does not exist in the `SortedSet` or
|
|
102
|
+
* otherwise the *first* item object that has this index value (meaning any further
|
|
103
|
+
* instances can be iterated using `next()`).
|
|
104
|
+
*
|
|
105
|
+
* Time complexity: O(log n)
|
|
106
|
+
*/
|
|
107
|
+
get(indexValue: string|number): T | undefined {
|
|
108
|
+
const keyProp = this.keyProp
|
|
109
|
+
let current = this.tail;
|
|
110
|
+
let prev
|
|
111
|
+
for (let l = this.symbols.length-1; l>=0; l--) {
|
|
112
|
+
const symbol = this.symbols[l]
|
|
113
|
+
while ((prev = current[symbol] as Item<T>) && prev[keyProp] > indexValue) current = prev;
|
|
114
|
+
}
|
|
115
|
+
return current[this.symbols[0]]?.[keyProp] === indexValue ? current[this.symbols[0]] : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The iterator will go through the items in reverse index-order.
|
|
120
|
+
*/
|
|
121
|
+
*[Symbol.iterator](): IterableIterator<T> {
|
|
122
|
+
let symbol = this.symbols[0]
|
|
123
|
+
let node: Item<T> | undefined = this.tail[symbol] as Item<T>;
|
|
124
|
+
while (node) {
|
|
125
|
+
yield node;
|
|
126
|
+
node = node[symbol] as Item<T> | undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Given an item object, returns the one that comes right before in the set.
|
|
132
|
+
* @param item The object to start from.
|
|
133
|
+
* @returns The next object, or `undefined` if there is none.
|
|
134
|
+
*
|
|
135
|
+
* Time complexity: O(1)
|
|
136
|
+
*/
|
|
137
|
+
prev(item: T): T | undefined {
|
|
138
|
+
return (item as Item<T>)[this.symbols[0]]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Remove an item object from the set, deleting all meta-data keys that
|
|
143
|
+
* were created on `add()`.
|
|
144
|
+
* @param item The object to be removed.
|
|
145
|
+
* @returns `true` on success or `false` when the item was not part of the set.
|
|
146
|
+
*
|
|
147
|
+
* Time complexity: O(log n)
|
|
148
|
+
*/
|
|
149
|
+
remove(item: T): boolean {
|
|
150
|
+
if (!(this.symbols[0] in item)) return false;
|
|
151
|
+
const keyProp = this.keyProp
|
|
152
|
+
const prop = item[keyProp];
|
|
153
|
+
|
|
154
|
+
let prev: Item<T> | undefined
|
|
155
|
+
let current: Item<T> = this.tail;
|
|
156
|
+
|
|
157
|
+
for (let l = this.symbols.length - 1; l >= 0; l--) {
|
|
158
|
+
const symbol = this.symbols[l];
|
|
159
|
+
while ((prev = current[symbol] as Item<T>) && prev[keyProp] >= prop && prev !== item) current = prev
|
|
160
|
+
if (prev === item) {
|
|
161
|
+
(current as any)[symbol] = prev[symbol]
|
|
162
|
+
delete prev[symbol]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return prev === item
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove all items for the set.
|
|
171
|
+
*
|
|
172
|
+
* Time complexity: O(n)
|
|
173
|
+
*/
|
|
174
|
+
clear(): void {
|
|
175
|
+
const symbol = this.symbols[0];
|
|
176
|
+
let current: Item<T> | undefined = this.tail;
|
|
177
|
+
while (current) {
|
|
178
|
+
const prev = current[symbol] as Item<T> | undefined
|
|
179
|
+
for (const symbol of this.symbols) {
|
|
180
|
+
if (!(symbol in current)) break
|
|
181
|
+
delete current[symbol];
|
|
182
|
+
}
|
|
183
|
+
current = prev
|
|
184
|
+
}
|
|
185
|
+
this.tail = {} as Item<T>;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
package/src/prediction.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import {withEmitHandler} from 'aberdeen'
|
|
1
|
+
import {withEmitHandler, defaultEmitHandler} from './aberdeen.js'
|
|
2
|
+
import type { DatumType, TargetType } from './aberdeen.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Represents a set of changes that can be applied to proxied objects.
|
|
6
|
+
* This is an opaque type - its internal structure is not part of the public API.
|
|
7
|
+
* @private
|
|
8
|
+
*/
|
|
9
|
+
export type Patch = Map<TargetType, Map<any, [DatumType, DatumType]>>;
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
function recordPatch(func: () => void): Patch {
|
|
8
13
|
const recordingPatch = new Map()
|
|
9
|
-
withEmitHandler(function(index, newData, oldData) {
|
|
10
|
-
addToPatch(recordingPatch,
|
|
14
|
+
withEmitHandler(function(target, index, newData, oldData) {
|
|
15
|
+
addToPatch(recordingPatch, target, index, newData, oldData)
|
|
11
16
|
}, func)
|
|
12
17
|
return recordingPatch
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
function addToPatch(patch: Patch, collection:
|
|
20
|
+
function addToPatch(patch: Patch, collection: TargetType, index: any, newData: DatumType, oldData: DatumType) {
|
|
16
21
|
let collectionMap = patch.get(collection)
|
|
17
22
|
if (!collectionMap) {
|
|
18
23
|
collectionMap = new Map()
|
|
@@ -27,7 +32,7 @@ function addToPatch(patch: Patch, collection: ObsCollection, index: any, newData
|
|
|
27
32
|
function emitPatch(patch: Patch) {
|
|
28
33
|
for(let [collection, collectionMap] of patch) {
|
|
29
34
|
for(let [index, [newData, oldData]] of collectionMap) {
|
|
30
|
-
collection
|
|
35
|
+
defaultEmitHandler(collection, index, newData, oldData);
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -43,7 +48,7 @@ function mergePatch(target: Patch, source: Patch, reverse: boolean = false) {
|
|
|
43
48
|
function silentlyApplyPatch(patch: Patch, force: boolean = false): boolean {
|
|
44
49
|
for(let [collection, collectionMap] of patch) {
|
|
45
50
|
for(let [index, [newData, oldData]] of collectionMap) {
|
|
46
|
-
let actualData = collection
|
|
51
|
+
let actualData = (collection as any)[index]
|
|
47
52
|
if (actualData !== oldData) {
|
|
48
53
|
if (force) setTimeout(() => { throw new Error(`Applying invalid patch: data ${actualData} is unequal to expected old data ${oldData} for index ${index}`)}, 0)
|
|
49
54
|
else return false
|
|
@@ -52,7 +57,7 @@ function silentlyApplyPatch(patch: Patch, force: boolean = false): boolean {
|
|
|
52
57
|
}
|
|
53
58
|
for(let [collection, collectionMap] of patch) {
|
|
54
59
|
for(let [index, [newData, oldData]] of collectionMap) {
|
|
55
|
-
collection
|
|
60
|
+
(collection as any)[index] = newData
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
return true
|
package/src/route.ts
CHANGED
|
@@ -1,48 +1,88 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {getParentElement, runQueue, clean, proxy, observe, immediateObserve, unproxy, clone} from './aberdeen.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* - `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.
|
|
6
|
-
* - `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.
|
|
7
|
-
* - `search`: An observable object containing search parameters (a split up query string). For instance `{order: "date", title: "something"}` or just `{}`. Updates will be reflected in the URL, modifying the current history state.
|
|
8
|
-
* - `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.
|
|
9
|
-
* - `state`: The browser history *state* object for the current page. Creating or removing top-level keys will cause *pushing* a new entry to the browser history.
|
|
4
|
+
* The class for the singleton `route` object.
|
|
10
5
|
*
|
|
11
|
-
* The following key may also be written to `route` but will be immediately and silently removed:
|
|
12
|
-
* - `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...
|
|
13
|
-
* - `"push"`: Force creation of a new browser history entry.
|
|
14
|
-
* - `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
15
|
-
* - `"back"`: Unwind the history (like repeatedly pressing the *back* button) until we find a page that matches the given `path`, `search` and top-level `state` keys, and then *replace* that state by the full given state.
|
|
16
6
|
*/
|
|
17
|
-
export const route = new Store()
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export class Route {
|
|
9
|
+
/** 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. */
|
|
10
|
+
path!: string
|
|
11
|
+
/** 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. */
|
|
12
|
+
p!: string[]
|
|
13
|
+
/** 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. */
|
|
14
|
+
hash!: string
|
|
15
|
+
/** 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. */
|
|
16
|
+
search!: Record<string, string>
|
|
17
|
+
/** The `hash` interpreted as search parameters. So `"a=x&b=y"` becomes `{a: "x", b: "y"}`. */
|
|
18
|
+
id!: Record<string, any>
|
|
19
|
+
/** 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. */
|
|
20
|
+
aux!: Record<string, any>
|
|
21
|
+
/** The navigation depth of the current session. Starts at 1. Writing to this property has no effect. */
|
|
22
|
+
depth: number = 1
|
|
23
|
+
/** The navigation action that got us to this page. Writing to this property has no effect.
|
|
24
|
+
- `"load"`: An initial page load.
|
|
25
|
+
- `"back"` or `"forward"`: When we navigated backwards or forwards in the stack.
|
|
26
|
+
- `"push"`: When we added a new page on top of the stack.
|
|
27
|
+
*/
|
|
28
|
+
nav: 'load' | 'back' | 'forward' | 'push' = 'load'
|
|
29
|
+
/** 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...
|
|
30
|
+
- `"push"`: Force creation of a new browser history entry.
|
|
31
|
+
- `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
32
|
+
- `"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.
|
|
33
|
+
The `mode` key can be written to `route` but will be immediately and silently removed.
|
|
34
|
+
*/
|
|
35
|
+
mode: 'push' | 'replace' | 'back' | undefined
|
|
36
|
+
}
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 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.
|
|
40
|
+
*/
|
|
41
|
+
export const route = proxy(new Route())
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
let stateRoute = {
|
|
44
|
+
nonce: -1,
|
|
45
|
+
depth: 0,
|
|
46
|
+
}
|
|
29
47
|
|
|
30
48
|
// Reflect changes to the browser URL (back/forward navigation) in the `route` and `stack`.
|
|
31
49
|
function handleLocationUpdate(event?: PopStateEvent) {
|
|
50
|
+
let state = event?.state || {}
|
|
51
|
+
let nav: 'load' | 'back' | 'forward' | 'push' = 'load'
|
|
52
|
+
if (state.route?.nonce == null) {
|
|
53
|
+
state.route = {
|
|
54
|
+
nonce: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
|
|
55
|
+
depth: 1,
|
|
56
|
+
}
|
|
57
|
+
history.replaceState(state, '')
|
|
58
|
+
} else if (stateRoute.nonce === state.route.nonce) {
|
|
59
|
+
nav = state.route.depth > stateRoute.depth ? 'forward' : 'back'
|
|
60
|
+
}
|
|
61
|
+
stateRoute = state.route
|
|
62
|
+
|
|
63
|
+
if (unproxy(route).mode === 'back') {
|
|
64
|
+
route.depth = stateRoute.depth
|
|
65
|
+
// We are still in the process of searching for a page in our navigation history..
|
|
66
|
+
updateHistory()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
const search: any= {}
|
|
33
71
|
for(let [k, v] of new URLSearchParams(location.search)) {
|
|
34
72
|
search[k] = v
|
|
35
73
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
route.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
74
|
+
|
|
75
|
+
route.path = location.pathname
|
|
76
|
+
route.p = location.pathname.slice(1).split('/')
|
|
77
|
+
route.search = search
|
|
78
|
+
route.hash = location.hash
|
|
79
|
+
route.id = state.id
|
|
80
|
+
route.aux = state.aux
|
|
81
|
+
route.depth = stateRoute.depth
|
|
82
|
+
route.nav = nav
|
|
83
|
+
|
|
84
|
+
// Forward or back event. Redraw synchronously, because we can!
|
|
85
|
+
if (event) runQueue();
|
|
46
86
|
}
|
|
47
87
|
handleLocationUpdate()
|
|
48
88
|
window.addEventListener("popstate", handleLocationUpdate);
|
|
@@ -52,80 +92,120 @@ window.addEventListener("popstate", handleLocationUpdate);
|
|
|
52
92
|
// initiated `set` will see the canonical form (instead of doing a rerender shortly after,
|
|
53
93
|
// or crashing due to non-canonical data).
|
|
54
94
|
function updatePath(): void {
|
|
55
|
-
let path = route.
|
|
56
|
-
if (path == null && route.
|
|
57
|
-
return updateP()
|
|
95
|
+
let path = route.path
|
|
96
|
+
if (path == null && unproxy(route).p) {
|
|
97
|
+
return updateP()
|
|
58
98
|
}
|
|
59
|
-
path = ''+path
|
|
99
|
+
path = ''+(path || '/')
|
|
60
100
|
if (!path.startsWith('/')) path = '/'+path
|
|
61
|
-
route.
|
|
62
|
-
route.
|
|
101
|
+
route.path = path
|
|
102
|
+
route.p = path.slice(1).split('/')
|
|
63
103
|
}
|
|
64
104
|
immediateObserve(updatePath)
|
|
65
105
|
|
|
66
106
|
function updateP(): void {
|
|
67
|
-
const p = route.
|
|
68
|
-
|
|
69
|
-
if (p == null && route.peek('path')) {
|
|
107
|
+
const p = route.p
|
|
108
|
+
if (p == null && unproxy(route).path) {
|
|
70
109
|
return updatePath()
|
|
71
110
|
}
|
|
72
111
|
if (!(p instanceof Array)) {
|
|
73
112
|
console.error(`aberdeen route: 'p' must be a non-empty array, not ${JSON.stringify(p)}`)
|
|
74
|
-
route.
|
|
113
|
+
route.p = [''] // This will cause a recursive call this observer.
|
|
75
114
|
} else if (p.length == 0) {
|
|
76
|
-
route.
|
|
115
|
+
route.p = [''] // This will cause a recursive call this observer.
|
|
77
116
|
} else {
|
|
78
|
-
route.
|
|
117
|
+
route.path = '/' + p.join('/')
|
|
79
118
|
}
|
|
80
119
|
}
|
|
81
120
|
immediateObserve(updateP)
|
|
82
121
|
|
|
83
122
|
immediateObserve(() => {
|
|
84
|
-
if (route.
|
|
123
|
+
if (!route.search || typeof route.search !== 'object') route.search = {}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
immediateObserve(() => {
|
|
127
|
+
if (!route.id || typeof route.id !== 'object') route.id = {}
|
|
85
128
|
})
|
|
86
129
|
|
|
87
130
|
immediateObserve(() => {
|
|
88
|
-
if (route.
|
|
131
|
+
if (!route.aux || typeof route.aux !== 'object') route.aux = {}
|
|
89
132
|
})
|
|
90
133
|
|
|
91
134
|
immediateObserve(() => {
|
|
92
|
-
let hash = ''+(route.
|
|
135
|
+
let hash = ''+(route.hash || '')
|
|
93
136
|
if (hash && !hash.startsWith('#')) hash = '#'+hash
|
|
94
|
-
route.
|
|
137
|
+
route.hash = hash
|
|
95
138
|
})
|
|
96
139
|
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
function isSamePage(path: string, state: any): boolean {
|
|
141
|
+
return location.pathname === path && JSON.stringify(history.state.id) === JSON.stringify(state.id)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function updateHistory() {
|
|
99
145
|
// Get and delete mode without triggering anything.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
let mode = route.mode
|
|
147
|
+
const state = {
|
|
148
|
+
id: clone(route.id),
|
|
149
|
+
aux: clone(route.aux),
|
|
150
|
+
route: stateRoute,
|
|
151
|
+
}
|
|
103
152
|
|
|
104
153
|
// Construct the URL.
|
|
105
|
-
const path = route.
|
|
106
|
-
const search = new URLSearchParams(route.get('search')).toString()
|
|
107
|
-
const url = (search ? path+'?'+search : path) + route.get('hash')
|
|
154
|
+
const path = route.path
|
|
108
155
|
|
|
109
156
|
// Change browser state, according to `mode`.
|
|
110
157
|
if (mode === 'back') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
goDelta--
|
|
116
|
-
stack.pop()
|
|
158
|
+
route.nav = 'back'
|
|
159
|
+
if (!isSamePage(path, state) && (history.state.route?.depth||0) > 1) {
|
|
160
|
+
history.back()
|
|
161
|
+
return
|
|
117
162
|
}
|
|
118
|
-
|
|
163
|
+
mode = 'replace'
|
|
119
164
|
// We'll replace the state async, to give the history.go the time to take affect first.
|
|
120
|
-
setTimeout(() => history.replaceState(state, '', url), 0)
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
//setTimeout(() => history.replaceState(state, '', url), 0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (mode) route.mode = undefined
|
|
169
|
+
const search = new URLSearchParams(route.search).toString()
|
|
170
|
+
const url = (search ? path+'?'+search : path) + route.hash
|
|
171
|
+
|
|
172
|
+
if (mode === 'push' || (!mode && !isSamePage(path, state))) {
|
|
173
|
+
stateRoute.depth++ // stateRoute === state.route
|
|
123
174
|
history.pushState(state, '', url)
|
|
124
|
-
|
|
175
|
+
route.nav = 'push'
|
|
176
|
+
route.depth = stateRoute.depth
|
|
125
177
|
} else {
|
|
178
|
+
// Default to `push` when the URL changed or top-level state keys changed.
|
|
126
179
|
history.replaceState(state, '', url)
|
|
127
|
-
stack[stack.length-1] = {url,state}
|
|
128
180
|
}
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// This deferred-mode observer will update the URL and history based on `route` changes.
|
|
184
|
+
observe(updateHistory)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Restore and store the vertical and horizontal scroll position for
|
|
189
|
+
* the parent element to the page state.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} name - A unique (within this page) name for this
|
|
192
|
+
* scrollable element. Defaults to 'main'.
|
|
193
|
+
*
|
|
194
|
+
* The scroll position will be persisted in `route.aux.scroll.<name>`.
|
|
195
|
+
*/
|
|
196
|
+
export function persistScroll(name: string = 'main') {
|
|
197
|
+
const el = getParentElement()
|
|
198
|
+
el.addEventListener('scroll', onScroll)
|
|
199
|
+
clean(() => el.removeEventListener('scroll', onScroll))
|
|
131
200
|
|
|
201
|
+
let restore = unproxy(route).aux.scroll?.name
|
|
202
|
+
if (restore) {
|
|
203
|
+
Object.assign(el, restore)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function onScroll() {
|
|
207
|
+
route.mode = 'replace'
|
|
208
|
+
if (!route.aux.scroll) route.aux.scroll = {}
|
|
209
|
+
route.aux.scroll[name] = {scrollTop: el.scrollTop, scrollLeft: el.scrollLeft}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/transitions.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import {scheduleDomReader, scheduleDomWriter} from 'aberdeen'
|
|
2
|
-
|
|
3
1
|
const FADE_TIME = 400
|
|
4
2
|
const GROW_SHRINK_TRANSITION = `margin ${FADE_TIME}ms ease-out, transform ${FADE_TIME}ms ease-out`
|
|
5
3
|
|
|
@@ -20,34 +18,19 @@ function getGrowShrinkProps(el: HTMLElement) {
|
|
|
20
18
|
* The transition doesn't look great for table elements, and may have problems
|
|
21
19
|
* for other specific cases as well.
|
|
22
20
|
*/
|
|
23
|
-
export function grow(el: HTMLElement)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
scheduleDomReader(() => {
|
|
37
|
-
// Make sure the layouting has been performed, to cause transitions to trigger
|
|
38
|
-
el.offsetHeight
|
|
39
|
-
scheduleDomWriter(() => {
|
|
40
|
-
// Do the transitions
|
|
41
|
-
el.style.transition = GROW_SHRINK_TRANSITION
|
|
42
|
-
for(let prop in props) el.style[prop as any] = ""
|
|
43
|
-
setTimeout(() => {
|
|
44
|
-
// Reset the element to a clean state
|
|
45
|
-
el.style.transition = ""
|
|
46
|
-
}, FADE_TIME)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
})
|
|
21
|
+
export async function grow(el: HTMLElement) {
|
|
22
|
+
let props = getGrowShrinkProps(el)
|
|
23
|
+
Object.assign(el.style, props)
|
|
24
|
+
|
|
25
|
+
// Make sure the layouting has been performed, to cause transitions to trigger
|
|
26
|
+
el.offsetHeight
|
|
27
|
+
|
|
28
|
+
el.style.transition = GROW_SHRINK_TRANSITION
|
|
29
|
+
for(let prop in props) el.style[prop as any] = ''
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
// Disable transitions.
|
|
32
|
+
el.style.transition = ''
|
|
33
|
+
}, FADE_TIME)
|
|
51
34
|
}
|
|
52
35
|
|
|
53
36
|
/** Do a shrink transition for the given element, and remove it from the DOM
|
|
@@ -58,16 +41,16 @@ export function grow(el: HTMLElement): void {
|
|
|
58
41
|
* The transition doesn't look great for table elements, and may have problems
|
|
59
42
|
* for other specific cases as well.
|
|
60
43
|
*/
|
|
61
|
-
export function shrink(el: HTMLElement)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
})
|
|
73
|
-
}
|
|
44
|
+
export async function shrink(el: HTMLElement) {
|
|
45
|
+
// Get original layout info
|
|
46
|
+
const props = getGrowShrinkProps(el)
|
|
47
|
+
|
|
48
|
+
// Batch starting transitions in the write phase.
|
|
49
|
+
el.style.transition = GROW_SHRINK_TRANSITION
|
|
50
|
+
Object.assign(el.style, props)
|
|
51
|
+
|
|
52
|
+
// Remove the element after the transition is done.
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
el.remove()
|
|
55
|
+
}, FADE_TIME)
|
|
56
|
+
}
|