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/src/route.ts CHANGED
@@ -1,90 +1,99 @@
1
- import {getParentElement, runQueue, clean, proxy, observe, immediateObserve, unproxy, clone} from './aberdeen.js'
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: number = 1
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
- 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...
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: 'push' | 'replace' | 'back' | undefined
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
- let state = event?.state || {}
51
- let nav: 'load' | 'back' | 'forward' | 'push' = 'load'
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 ? 'forward' : 'back'
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 === 'back') {
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(let [k, v] of new URLSearchParams(location.search)) {
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 = 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
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
- return updateP()
98
- }
99
- path = ''+(path || '/')
100
- if (!path.startsWith('/')) path = '/'+path
101
- route.path = path
102
- route.p = path.slice(1).split('/')
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(): void {
107
- const p = route.p
116
+ function updateP() {
117
+ const p = route.p;
108
118
  if (p == null && unproxy(route).path) {
109
- return updatePath()
119
+ updatePath();
120
+ return;
110
121
  }
111
122
  if (!(p instanceof Array)) {
112
- console.error(`aberdeen route: 'p' must be a non-empty array, not ${JSON.stringify(p)}`)
113
- route.p = [''] // This will cause a recursive call this observer.
114
- } else if (p.length == 0) {
115
- route.p = [''] // This will cause a recursive call this observer.
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 = '/' + p.join('/')
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 !== 'object') 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 !== 'object') 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 !== 'object') route.aux = {}
132
- })
144
+ if (!route.aux || typeof route.aux !== "object") route.aux = {};
145
+ });
133
146
 
134
147
  immediateObserve(() => {
135
- let hash = ''+(route.hash || '')
136
- if (hash && !hash.startsWith('#')) hash = '#'+hash
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 location.pathname === path && JSON.stringify(history.state.id) === JSON.stringify(state.id)
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 === 'back') {
158
- route.nav = 'back'
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 = 'replace'
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+'?'+search : path) + route.hash
171
-
172
- if (mode === 'push' || (!mode && !isSamePage(path, state))) {
173
- stateRoute.depth++ // stateRoute === state.route
174
- history.pushState(state, '', url)
175
- route.nav = 'push'
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, '', url)
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: string = 'main') {
197
- const el = getParentElement()
198
- el.addEventListener('scroll', onScroll)
199
- clean(() => el.removeEventListener('scroll', onScroll))
211
+ export function persistScroll(name = "main") {
212
+ const el = getParentElement();
213
+ el.addEventListener("scroll", onScroll);
214
+ clean(() => el.removeEventListener("scroll", onScroll));
200
215
 
201
- let restore = unproxy(route).aux.scroll?.name
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 = 'replace'
208
- if (!route.aux.scroll) route.aux.scroll = {}
209
- route.aux.scroll[name] = {scrollTop: el.scrollTop, scrollLeft: el.scrollLeft}
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
  }
@@ -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
- let props = getGrowShrinkProps(el)
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(let prop in props) el.style[prop as any] = ''
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
  }