aberdeen 1.0.5 → 1.0.6
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/aberdeen.d.ts +3 -8
- package/dist/aberdeen.js +81 -77
- package/dist/aberdeen.js.map +4 -4
- package/dist/prediction.d.ts +2 -2
- package/dist/prediction.js +21 -22
- package/dist/prediction.js.map +3 -3
- package/dist/route.d.ts +2 -2
- package/dist/route.js +29 -15
- package/dist/route.js.map +3 -3
- package/dist/transitions.d.ts +14 -14
- package/dist/transitions.js +19 -6
- package/dist/transitions.js.map +3 -3
- package/dist-min/aberdeen.js +5 -5
- package/dist-min/aberdeen.js.map +4 -4
- package/dist-min/prediction.js +2 -2
- package/dist-min/prediction.js.map +3 -3
- package/dist-min/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/dist-min/transitions.js +2 -2
- package/dist-min/transitions.js.map +3 -3
- package/package.json +2 -1
- package/src/aberdeen.ts +588 -400
- package/src/helpers/reverseSortedSet.ts +187 -178
- package/src/prediction.ts +73 -55
- package/src/route.ts +115 -97
- package/src/transitions.ts +49 -37
package/src/route.ts
CHANGED
|
@@ -1,90 +1,99 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clean,
|
|
3
|
+
clone,
|
|
4
|
+
getParentElement,
|
|
5
|
+
immediateObserve,
|
|
6
|
+
observe,
|
|
7
|
+
proxy,
|
|
8
|
+
runQueue,
|
|
9
|
+
unproxy,
|
|
10
|
+
} from "./aberdeen.js";
|
|
2
11
|
|
|
3
12
|
/**
|
|
4
13
|
* The class for the singleton `route` object.
|
|
5
|
-
*
|
|
14
|
+
*
|
|
6
15
|
*/
|
|
7
16
|
|
|
8
17
|
export class Route {
|
|
9
18
|
/** 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
|
|
19
|
+
path!: string;
|
|
11
20
|
/** 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[]
|
|
21
|
+
p!: string[];
|
|
13
22
|
/** 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
|
|
23
|
+
hash!: string;
|
|
15
24
|
/** 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
|
|
25
|
+
search!: Record<string, string>;
|
|
17
26
|
/** The `hash` interpreted as search parameters. So `"a=x&b=y"` becomes `{a: "x", b: "y"}`. */
|
|
18
|
-
id!: Record<string, any
|
|
27
|
+
id!: Record<string, any>;
|
|
19
28
|
/** 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
|
|
29
|
+
aux!: Record<string, any>;
|
|
21
30
|
/** The navigation depth of the current session. Starts at 1. Writing to this property has no effect. */
|
|
22
|
-
depth
|
|
31
|
+
depth = 1;
|
|
23
32
|
/** The navigation action that got us to this page. Writing to this property has no effect.
|
|
24
33
|
- `"load"`: An initial page load.
|
|
25
34
|
- `"back"` or `"forward"`: When we navigated backwards or forwards in the stack.
|
|
26
35
|
- `"push"`: When we added a new page on top of the stack.
|
|
27
36
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
nav: "load" | "back" | "forward" | "push" = "load";
|
|
38
|
+
/** 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
39
|
- `"push"`: Force creation of a new browser history entry.
|
|
31
40
|
- `"replace"`: Update the current history entry, even when updates to other keys would normally cause a *push*.
|
|
32
41
|
- `"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
42
|
The `mode` key can be written to `route` but will be immediately and silently removed.
|
|
34
43
|
*/
|
|
35
|
-
mode:
|
|
44
|
+
mode: "push" | "replace" | "back" | undefined;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* 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
49
|
*/
|
|
41
|
-
export const route = proxy(new Route())
|
|
50
|
+
export const route = proxy(new Route());
|
|
42
51
|
|
|
43
52
|
let stateRoute = {
|
|
44
53
|
nonce: -1,
|
|
45
54
|
depth: 0,
|
|
46
|
-
}
|
|
55
|
+
};
|
|
47
56
|
|
|
48
57
|
// Reflect changes to the browser URL (back/forward navigation) in the `route` and `stack`.
|
|
49
58
|
function handleLocationUpdate(event?: PopStateEvent) {
|
|
50
|
-
|
|
51
|
-
let nav:
|
|
59
|
+
const state = event?.state || {};
|
|
60
|
+
let nav: "load" | "back" | "forward" | "push" = "load";
|
|
52
61
|
if (state.route?.nonce == null) {
|
|
53
62
|
state.route = {
|
|
54
63
|
nonce: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
|
|
55
64
|
depth: 1,
|
|
56
|
-
}
|
|
57
|
-
history.replaceState(state,
|
|
65
|
+
};
|
|
66
|
+
history.replaceState(state, "");
|
|
58
67
|
} else if (stateRoute.nonce === state.route.nonce) {
|
|
59
|
-
nav = state.route.depth > stateRoute.depth ?
|
|
68
|
+
nav = state.route.depth > stateRoute.depth ? "forward" : "back";
|
|
60
69
|
}
|
|
61
|
-
stateRoute = state.route
|
|
70
|
+
stateRoute = state.route;
|
|
62
71
|
|
|
63
|
-
if (unproxy(route).mode ===
|
|
64
|
-
route.depth = stateRoute.depth
|
|
72
|
+
if (unproxy(route).mode === "back") {
|
|
73
|
+
route.depth = stateRoute.depth;
|
|
65
74
|
// We are still in the process of searching for a page in our navigation history..
|
|
66
|
-
updateHistory()
|
|
67
|
-
return
|
|
75
|
+
updateHistory();
|
|
76
|
+
return;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
const search: any= {}
|
|
71
|
-
for(
|
|
72
|
-
search[k] = v
|
|
79
|
+
const search: any = {};
|
|
80
|
+
for (const [k, v] of new URLSearchParams(location.search)) {
|
|
81
|
+
search[k] = v;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
route.path =
|
|
76
|
-
route.p =
|
|
77
|
-
route.search = search
|
|
78
|
-
route.hash =
|
|
79
|
-
route.id =
|
|
80
|
-
route.aux =
|
|
81
|
-
route.depth =
|
|
82
|
-
route.nav = nav
|
|
84
|
+
route.path = location.pathname;
|
|
85
|
+
route.p = location.pathname.slice(1).split("/");
|
|
86
|
+
route.search = search;
|
|
87
|
+
route.hash = location.hash;
|
|
88
|
+
route.id = state.id;
|
|
89
|
+
route.aux = state.aux;
|
|
90
|
+
route.depth = stateRoute.depth;
|
|
91
|
+
route.nav = nav;
|
|
83
92
|
|
|
84
93
|
// Forward or back event. Redraw synchronously, because we can!
|
|
85
94
|
if (event) runQueue();
|
|
86
95
|
}
|
|
87
|
-
handleLocationUpdate()
|
|
96
|
+
handleLocationUpdate();
|
|
88
97
|
window.addEventListener("popstate", handleLocationUpdate);
|
|
89
98
|
|
|
90
99
|
// These immediate-mode observers will rewrite the data in `route` to its canonical form.
|
|
@@ -92,120 +101,129 @@ window.addEventListener("popstate", handleLocationUpdate);
|
|
|
92
101
|
// initiated `set` will see the canonical form (instead of doing a rerender shortly after,
|
|
93
102
|
// or crashing due to non-canonical data).
|
|
94
103
|
function updatePath(): void {
|
|
95
|
-
let path = route.path
|
|
104
|
+
let path = route.path;
|
|
96
105
|
if (path == null && unproxy(route).p) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
route.
|
|
106
|
+
updateP();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
path = `${path || "/"}`;
|
|
110
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
111
|
+
route.path = path;
|
|
112
|
+
route.p = path.slice(1).split("/");
|
|
103
113
|
}
|
|
104
|
-
immediateObserve(updatePath)
|
|
114
|
+
immediateObserve(updatePath);
|
|
105
115
|
|
|
106
|
-
function updateP()
|
|
107
|
-
const p = route.p
|
|
116
|
+
function updateP() {
|
|
117
|
+
const p = route.p;
|
|
108
118
|
if (p == null && unproxy(route).path) {
|
|
109
|
-
|
|
119
|
+
updatePath();
|
|
120
|
+
return;
|
|
110
121
|
}
|
|
111
122
|
if (!(p instanceof Array)) {
|
|
112
|
-
console.error(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
route.p = [
|
|
123
|
+
console.error(
|
|
124
|
+
`aberdeen route: 'p' must be a non-empty array, not ${JSON.stringify(p)}`,
|
|
125
|
+
);
|
|
126
|
+
route.p = [""]; // This will cause a recursive call this observer.
|
|
127
|
+
} else if (p.length === 0) {
|
|
128
|
+
route.p = [""]; // This will cause a recursive call this observer.
|
|
116
129
|
} else {
|
|
117
|
-
route.path =
|
|
130
|
+
route.path = `/${p.join("/")}`;
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
|
-
immediateObserve(updateP)
|
|
133
|
+
immediateObserve(updateP);
|
|
121
134
|
|
|
122
135
|
immediateObserve(() => {
|
|
123
|
-
if (!route.search || typeof route.search !==
|
|
124
|
-
})
|
|
136
|
+
if (!route.search || typeof route.search !== "object") route.search = {};
|
|
137
|
+
});
|
|
125
138
|
|
|
126
139
|
immediateObserve(() => {
|
|
127
|
-
if (!route.id || typeof route.id !==
|
|
128
|
-
})
|
|
140
|
+
if (!route.id || typeof route.id !== "object") route.id = {};
|
|
141
|
+
});
|
|
129
142
|
|
|
130
143
|
immediateObserve(() => {
|
|
131
|
-
if (!route.aux || typeof route.aux !==
|
|
132
|
-
})
|
|
144
|
+
if (!route.aux || typeof route.aux !== "object") route.aux = {};
|
|
145
|
+
});
|
|
133
146
|
|
|
134
147
|
immediateObserve(() => {
|
|
135
|
-
let hash =
|
|
136
|
-
if (hash && !hash.startsWith(
|
|
137
|
-
route.hash = hash
|
|
138
|
-
})
|
|
148
|
+
let hash = `${route.hash || ""}`;
|
|
149
|
+
if (hash && !hash.startsWith("#")) hash = `#${hash}`;
|
|
150
|
+
route.hash = hash;
|
|
151
|
+
});
|
|
139
152
|
|
|
140
153
|
function isSamePage(path: string, state: any): boolean {
|
|
141
|
-
return
|
|
154
|
+
return (
|
|
155
|
+
location.pathname === path &&
|
|
156
|
+
JSON.stringify(history.state.id) === JSON.stringify(state.id)
|
|
157
|
+
);
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
function updateHistory() {
|
|
145
161
|
// Get and delete mode without triggering anything.
|
|
146
|
-
let mode = route.mode
|
|
162
|
+
let mode = route.mode;
|
|
147
163
|
const state = {
|
|
148
164
|
id: clone(route.id),
|
|
149
165
|
aux: clone(route.aux),
|
|
150
166
|
route: stateRoute,
|
|
151
|
-
}
|
|
152
|
-
|
|
167
|
+
};
|
|
168
|
+
|
|
153
169
|
// Construct the URL.
|
|
154
|
-
const path = route.path
|
|
170
|
+
const path = route.path;
|
|
155
171
|
|
|
156
172
|
// Change browser state, according to `mode`.
|
|
157
|
-
if (mode ===
|
|
158
|
-
route.nav =
|
|
159
|
-
if (!isSamePage(path, state) && (history.state.route?.depth||0) > 1) {
|
|
160
|
-
history.back()
|
|
161
|
-
return
|
|
173
|
+
if (mode === "back") {
|
|
174
|
+
route.nav = "back";
|
|
175
|
+
if (!isSamePage(path, state) && (history.state.route?.depth || 0) > 1) {
|
|
176
|
+
history.back();
|
|
177
|
+
return;
|
|
162
178
|
}
|
|
163
|
-
mode =
|
|
179
|
+
mode = "replace";
|
|
164
180
|
// We'll replace the state async, to give the history.go the time to take affect first.
|
|
165
181
|
//setTimeout(() => history.replaceState(state, '', url), 0)
|
|
166
182
|
}
|
|
167
183
|
|
|
168
|
-
if (mode) route.mode = undefined
|
|
169
|
-
const search = new URLSearchParams(route.search).toString()
|
|
170
|
-
const url = (search ? path
|
|
171
|
-
|
|
172
|
-
if (mode ===
|
|
173
|
-
stateRoute.depth
|
|
174
|
-
history.pushState(state,
|
|
175
|
-
route.nav =
|
|
176
|
-
route.depth = stateRoute.depth
|
|
184
|
+
if (mode) route.mode = undefined;
|
|
185
|
+
const search = new URLSearchParams(route.search).toString();
|
|
186
|
+
const url = (search ? `${path}?${search}` : path) + route.hash;
|
|
187
|
+
|
|
188
|
+
if (mode === "push" || (!mode && !isSamePage(path, state))) {
|
|
189
|
+
stateRoute.depth++; // stateRoute === state.route
|
|
190
|
+
history.pushState(state, "", url);
|
|
191
|
+
route.nav = "push";
|
|
192
|
+
route.depth = stateRoute.depth;
|
|
177
193
|
} else {
|
|
178
194
|
// Default to `push` when the URL changed or top-level state keys changed.
|
|
179
|
-
history.replaceState(state,
|
|
195
|
+
history.replaceState(state, "", url);
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
// This deferred-mode observer will update the URL and history based on `route` changes.
|
|
184
|
-
observe(updateHistory)
|
|
185
|
-
|
|
200
|
+
observe(updateHistory);
|
|
186
201
|
|
|
187
202
|
/**
|
|
188
203
|
* Restore and store the vertical and horizontal scroll position for
|
|
189
204
|
* the parent element to the page state.
|
|
190
|
-
*
|
|
205
|
+
*
|
|
191
206
|
* @param {string} name - A unique (within this page) name for this
|
|
192
207
|
* scrollable element. Defaults to 'main'.
|
|
193
|
-
*
|
|
208
|
+
*
|
|
194
209
|
* The scroll position will be persisted in `route.aux.scroll.<name>`.
|
|
195
210
|
*/
|
|
196
|
-
export function persistScroll(name
|
|
197
|
-
const el = getParentElement()
|
|
198
|
-
el.addEventListener(
|
|
199
|
-
clean(() => el.removeEventListener(
|
|
211
|
+
export function persistScroll(name = "main") {
|
|
212
|
+
const el = getParentElement();
|
|
213
|
+
el.addEventListener("scroll", onScroll);
|
|
214
|
+
clean(() => el.removeEventListener("scroll", onScroll));
|
|
200
215
|
|
|
201
|
-
|
|
216
|
+
const restore = unproxy(route).aux.scroll?.name;
|
|
202
217
|
if (restore) {
|
|
203
|
-
Object.assign(el, restore)
|
|
218
|
+
Object.assign(el, restore);
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
function onScroll() {
|
|
207
|
-
route.mode =
|
|
208
|
-
if (!route.aux.scroll) route.aux.scroll = {}
|
|
209
|
-
route.aux.scroll[name] = {
|
|
222
|
+
route.mode = "replace";
|
|
223
|
+
if (!route.aux.scroll) route.aux.scroll = {};
|
|
224
|
+
route.aux.scroll[name] = {
|
|
225
|
+
scrollTop: el.scrollTop,
|
|
226
|
+
scrollLeft: el.scrollLeft,
|
|
227
|
+
};
|
|
210
228
|
}
|
|
211
229
|
}
|
package/src/transitions.ts
CHANGED
|
@@ -1,56 +1,68 @@
|
|
|
1
|
-
const FADE_TIME = 400
|
|
2
|
-
const GROW_SHRINK_TRANSITION = `margin ${FADE_TIME}ms ease-out, transform ${FADE_TIME}ms ease-out
|
|
3
|
-
|
|
4
|
-
function getGrowShrinkProps(el: HTMLElement) {
|
|
5
|
-
const parentStyle: any = el.parentElement ? getComputedStyle(el.parentElement) : {}
|
|
6
|
-
const isHorizontal = parentStyle.display === 'flex' && (parentStyle.flexDirection||'').startsWith('row')
|
|
7
|
-
return isHorizontal ?
|
|
8
|
-
{marginLeft: `-${el.offsetWidth/2}px`, marginRight: `-${el.offsetWidth/2}px`, transform: "scaleX(0)"} :
|
|
9
|
-
{marginBottom: `-${el.offsetHeight/2}px`, marginTop: `-${el.offsetHeight/2}px`, transform: "scaleY(0)"}
|
|
1
|
+
const FADE_TIME = 400;
|
|
2
|
+
const GROW_SHRINK_TRANSITION = `margin ${FADE_TIME}ms ease-out, transform ${FADE_TIME}ms ease-out`;
|
|
10
3
|
|
|
4
|
+
function getGrowShrinkProps(el: HTMLElement): Partial<CSSStyleDeclaration> {
|
|
5
|
+
if (el.parentElement) {
|
|
6
|
+
const parentStyle = getComputedStyle(el.parentElement);
|
|
7
|
+
const isHorizontal =
|
|
8
|
+
parentStyle.display === "flex" &&
|
|
9
|
+
(parentStyle.flexDirection || "").startsWith("row");
|
|
10
|
+
if (isHorizontal) {
|
|
11
|
+
return {
|
|
12
|
+
marginLeft: `-${el.offsetWidth / 2}px`,
|
|
13
|
+
marginRight: `-${el.offsetWidth / 2}px`,
|
|
14
|
+
transform: "scaleX(0)",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
marginBottom: `-${el.offsetHeight / 2}px`,
|
|
20
|
+
marginTop: `-${el.offsetHeight / 2}px`,
|
|
21
|
+
transform: "scaleY(0)",
|
|
22
|
+
};
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
/** Do a grow transition for the given element. This is meant to be used as a
|
|
14
|
-
* handler for the `create` property.
|
|
15
|
-
*
|
|
16
|
-
* @param el The element to transition.
|
|
17
|
-
*
|
|
18
|
-
* The transition doesn't look great for table elements, and may have problems
|
|
19
|
-
* for other specific cases as well.
|
|
20
|
-
*/
|
|
26
|
+
* handler for the `create` property.
|
|
27
|
+
*
|
|
28
|
+
* @param el The element to transition.
|
|
29
|
+
*
|
|
30
|
+
* The transition doesn't look great for table elements, and may have problems
|
|
31
|
+
* for other specific cases as well.
|
|
32
|
+
*/
|
|
21
33
|
export async function grow(el: HTMLElement) {
|
|
22
|
-
|
|
23
|
-
Object.assign(el.style, props)
|
|
24
|
-
|
|
34
|
+
const props = getGrowShrinkProps(el);
|
|
35
|
+
Object.assign(el.style, props);
|
|
36
|
+
|
|
25
37
|
// Make sure the layouting has been performed, to cause transitions to trigger
|
|
26
|
-
el.offsetHeight
|
|
38
|
+
el.offsetHeight;
|
|
27
39
|
|
|
28
|
-
el.style.transition = GROW_SHRINK_TRANSITION
|
|
29
|
-
for(
|
|
40
|
+
el.style.transition = GROW_SHRINK_TRANSITION;
|
|
41
|
+
for (const prop in props) el.style[prop] = "";
|
|
30
42
|
setTimeout(() => {
|
|
31
43
|
// Disable transitions.
|
|
32
|
-
el.style.transition =
|
|
33
|
-
}, FADE_TIME)
|
|
44
|
+
el.style.transition = "";
|
|
45
|
+
}, FADE_TIME);
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
/** Do a shrink transition for the given element, and remove it from the DOM
|
|
37
|
-
* afterwards. This is meant to be used as a handler for the `destroy` property.
|
|
38
|
-
*
|
|
39
|
-
* @param el The element to transition and remove.
|
|
40
|
-
*
|
|
41
|
-
* The transition doesn't look great for table elements, and may have problems
|
|
42
|
-
* for other specific cases as well.
|
|
43
|
-
*/
|
|
49
|
+
* afterwards. This is meant to be used as a handler for the `destroy` property.
|
|
50
|
+
*
|
|
51
|
+
* @param el The element to transition and remove.
|
|
52
|
+
*
|
|
53
|
+
* The transition doesn't look great for table elements, and may have problems
|
|
54
|
+
* for other specific cases as well.
|
|
55
|
+
*/
|
|
44
56
|
export async function shrink(el: HTMLElement) {
|
|
45
57
|
// Get original layout info
|
|
46
|
-
const props = getGrowShrinkProps(el)
|
|
58
|
+
const props = getGrowShrinkProps(el);
|
|
47
59
|
|
|
48
60
|
// Batch starting transitions in the write phase.
|
|
49
|
-
el.style.transition = GROW_SHRINK_TRANSITION
|
|
50
|
-
Object.assign(el.style, props)
|
|
51
|
-
|
|
61
|
+
el.style.transition = GROW_SHRINK_TRANSITION;
|
|
62
|
+
Object.assign(el.style, props);
|
|
63
|
+
|
|
52
64
|
// Remove the element after the transition is done.
|
|
53
65
|
setTimeout(() => {
|
|
54
|
-
el.remove()
|
|
55
|
-
}, FADE_TIME)
|
|
66
|
+
el.remove();
|
|
67
|
+
}, FADE_TIME);
|
|
56
68
|
}
|