stateshape 0.2.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 +21 -0
- package/README.md +166 -0
- package/dist/index.cjs +517 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.mjs +506 -0
- package/index.ts +25 -0
- package/package.json +42 -0
- package/src/EventEmitter.ts +72 -0
- package/src/PersistentState.ts +99 -0
- package/src/Route.ts +223 -0
- package/src/State.ts +101 -0
- package/src/URLState.ts +143 -0
- package/src/isState.ts +20 -0
- package/src/types/EventCallback.ts +1 -0
- package/src/types/EventCallbackMap.ts +5 -0
- package/src/types/LinkElement.ts +1 -0
- package/src/types/LocationObject.ts +11 -0
- package/src/types/LocationPattern.ts +6 -0
- package/src/types/LocationValue.ts +6 -0
- package/src/types/MatchHandler.ts +6 -0
- package/src/types/MatchState.ts +22 -0
- package/src/types/NavigationOptions.ts +10 -0
- package/src/types/PersistentStorage.ts +4 -0
- package/src/types/URLComponents.ts +4 -0
- package/src/types/URLConfig.ts +5 -0
- package/src/types/URLData.ts +10 -0
- package/src/types/URLSchema.ts +4 -0
- package/src/utils/compileURL.ts +27 -0
- package/src/utils/getNavigationOptions.ts +15 -0
- package/src/utils/isLocationObject.ts +11 -0
- package/src/utils/isRouteEvent.ts +11 -0
- package/src/utils/matchURL.ts +99 -0
- package/tsconfig.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Alexander Tkačenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# stateshape
|
|
2
|
+
|
|
3
|
+
Vanilla TS/JS state management for sharing data across decoupled parts of the code and routing. Routing is essentially shared state management, too, with the shared data being the URL.
|
|
4
|
+
|
|
5
|
+
This package exposes the following classes:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
EventEmitter ──► State ──► PersistentState
|
|
9
|
+
│
|
|
10
|
+
└────► URLState ──► Route
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Roughly, their purpose boils down to the following:
|
|
14
|
+
|
|
15
|
+
- `EventEmitter` is for triggering actions without tightly coupling the interacting components
|
|
16
|
+
- `State` is `EventEmitter` that stores data and emits an event when the data gets updated, it's for dynamic data sharing without tight coupling
|
|
17
|
+
- `PersistentState` is `State` that syncs its data to the browser storage and restores it on page reload
|
|
18
|
+
- `URLState` is `State` that stores the URL + syncs with the browser's URL in a SPA fashion
|
|
19
|
+
- `Route` is `URLState` + native-like APIs for SPA navigation and an API for URL matching
|
|
20
|
+
|
|
21
|
+
Contents: [State](#state) · [PersistentState](#persistentstate) · [Route](#route) · [Annotated examples](#annotated-examples) · [Integrations](#integrations)
|
|
22
|
+
|
|
23
|
+
## `State`
|
|
24
|
+
|
|
25
|
+
A thin data container for dynamic data sharing without tight coupling.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { State } from "stateshape";
|
|
29
|
+
|
|
30
|
+
const counterState = new State(42);
|
|
31
|
+
|
|
32
|
+
document.querySelector("button").addEventListener("click", () => {
|
|
33
|
+
counterState.setValue((value) => value + 1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
counterState.on("set", ({ current }) => {
|
|
37
|
+
document.querySelector("output").textContent = String(current);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In this example, a button changes a counter value and an `<output>` element shows the updating value. Both elements are only aware of the shared counter state, but not of each other.
|
|
42
|
+
|
|
43
|
+
A `"set"` event callback is called each time the state value changes and immediately when the callback is added. Subscribe to the `"update"` event to have the callback respond only to the subsequent state changes without the immediate invocation.
|
|
44
|
+
|
|
45
|
+
## `PersistentState`
|
|
46
|
+
|
|
47
|
+
A variety of `State` that syncs its data to the browser storage and restores it on page reload. Otherwise, almost identical to `State` in usage.
|
|
48
|
+
|
|
49
|
+
```diff
|
|
50
|
+
- import { State } from "stateshape";
|
|
51
|
+
+ import { PersistentState } from "stateshape";
|
|
52
|
+
|
|
53
|
+
- const counterState = new State(42);
|
|
54
|
+
+ const counterState = new PersistentState(42, { key: "counter" });
|
|
55
|
+
|
|
56
|
+
document.querySelector("button").addEventListener("click", () => {
|
|
57
|
+
counterState.setValue((value) => value + 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
counterState.on("set", ({ current }) => {
|
|
61
|
+
document.querySelector("output").textContent = String(current);
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
By default, `PersistentState` stores its data at the specified `key` in `localStorage` and transforms the data with `JSON.stringify()` and `JSON.parse()`. Switch to `sessionStorage` by setting `options.session` to `true` in `new PersistentState(value, options)`. Set custom `serialize()` and `deserialize()` in `options` to override the default data transforms used with the browser storage. Alternatively, use custom `{ read(), write()? }` as `options` to set up custom interaction with an external storage.
|
|
66
|
+
|
|
67
|
+
Instances of `PersistentState` automatically sync their values with the browser storage when created and updated. At other times, call `.emit("sync")` on a `PersistentState` instance to sync its value from the browser storage when needed.
|
|
68
|
+
|
|
69
|
+
## `Route`
|
|
70
|
+
|
|
71
|
+
Stores the URL, exposes a native-like API for SPA navigation and an API for URL matching.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { Route } from "stateshape";
|
|
75
|
+
|
|
76
|
+
const route = new Route();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Navigate to other URLs in a SPA fashion similarly to the native APIs:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
route.href = "/intro";
|
|
83
|
+
route.assign("/intro");
|
|
84
|
+
route.replace("/intro");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or in a more fine-grained manner:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
route.navigate({ href: "/intro", history: "replace", scroll: "off" });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Check the current URL value like a regular `string` with `route.href`:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
route.href === "/intro";
|
|
97
|
+
route.href.startsWith("/sections/");
|
|
98
|
+
/^\/sections\/\d+\/?/.test(route.href);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or, alternatively, with `route.at(url, x, y)` which is similar to the ternary conditional operator `atURL ? x : y`:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
document.querySelector("header").className = route.at("/", "full", "compact");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `route.at(url, x, y)` with dynamic values that require values from the URL pattern's capturing groups:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
document.querySelector("h1").textContent = route.at(
|
|
111
|
+
/^\/sections\/(?<id>\d+)\/?/,
|
|
112
|
+
({ params }) => `Section ${params.id}`,
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Enable SPA navigation with HTML links inside the specified container (or the entire `document`) without any changes to the HTML:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
route.observe(document);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Tweak the links' navigation behavior by adding a relevant combination of the optional `data-` attributes (corresponding to the `route.navigate()` options):
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<a href="/intro">Intro</a>
|
|
126
|
+
<a href="/intro" data-history="replace">Intro</a>
|
|
127
|
+
<a href="/intro" data-scroll="off">Intro</a>
|
|
128
|
+
<a href="/intro" data-spa="off">Intro</a>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Define what should be done when the URL changes:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
route.on("navigationcomplete", ({ href }) => {
|
|
135
|
+
renderContent();
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Define what should be done before the URL changes (in a way effectively similar to routing middleware):
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
route.on("navigationstart", ({ href }) => {
|
|
143
|
+
if (hasUnsavedInput)
|
|
144
|
+
return false; // Quit the navigation, prevent the current URL change
|
|
145
|
+
|
|
146
|
+
if (href === "/") {
|
|
147
|
+
route.href = "/intro"; // SPA redirection
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Annotated examples
|
|
154
|
+
|
|
155
|
+
- [Shared state](https://codesandbox.io/p/sandbox/lqt3z2?file=%252Fsrc%252Findex.ts), counter app, State
|
|
156
|
+
- [Shared form input state](https://codesandbox.io/p/sandbox/4q7f99?file=%252Fsrc%252Findex.ts), simple form, State
|
|
157
|
+
- [Persistent shared state](https://codesandbox.io/p/sandbox/c9gt3r?file=%252Fsrc%252Findex.ts), counter app, PersistentState
|
|
158
|
+
- [URL-based rendering](https://codesandbox.io/p/sandbox/kt6m5l?file=%252Fsrc%252Findex.ts), Route
|
|
159
|
+
- [Type-safe URL-based rendering](https://codesandbox.io/p/sandbox/qg7qg3?file=%2Fsrc%2Findex.ts), Route, url-shape, zod
|
|
160
|
+
- [SPA redirection](https://codesandbox.io/p/sandbox/rpl3gh?file=%252Fsrc%252Findex.ts), Route
|
|
161
|
+
|
|
162
|
+
Find also the code of these examples in the repo's [`tests`](https://github.com/axtk/stateshape/tree/main/tests) directory.
|
|
163
|
+
|
|
164
|
+
## Integrations
|
|
165
|
+
|
|
166
|
+
[`react-stateshape`](https://www.npmjs.com/package/react-stateshape)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
let quasiurl = require("quasiurl");
|
|
2
|
+
|
|
3
|
+
var EventEmitter = class {
|
|
4
|
+
_callbacks = {};
|
|
5
|
+
_active = true;
|
|
6
|
+
/**
|
|
7
|
+
* Adds an event handler.
|
|
8
|
+
*
|
|
9
|
+
* Returns an unsubscription function. Once it's invoked, the given
|
|
10
|
+
* `callback` is removed and no longer called in response to the event.
|
|
11
|
+
*/
|
|
12
|
+
on(event, callback) {
|
|
13
|
+
(this._callbacks[event] ??= /* @__PURE__ */ new Set()).add(callback);
|
|
14
|
+
return () => this.off(event, callback);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Adds a one-time event handler: once the event is emitted, the callback
|
|
18
|
+
* is called and immediately removed.
|
|
19
|
+
*/
|
|
20
|
+
once(event, callback) {
|
|
21
|
+
let oneTimeCallback = (payload) => {
|
|
22
|
+
this.off(event, oneTimeCallback);
|
|
23
|
+
callback(payload);
|
|
24
|
+
};
|
|
25
|
+
return this.on(event, oneTimeCallback);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Removes the specified `callback` from the handlers of the given event,
|
|
29
|
+
* and removes all handlers of the given event if `callback` is not
|
|
30
|
+
* specified.
|
|
31
|
+
*/
|
|
32
|
+
off(event, callback) {
|
|
33
|
+
if (callback === void 0) delete this._callbacks[event];
|
|
34
|
+
else this._callbacks[event]?.delete(callback);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Emits the specified event. Returns `false` if at least one event callback
|
|
38
|
+
* returns `false`, effectively interrupting the callback call chain.
|
|
39
|
+
* Otherwise returns `true`.
|
|
40
|
+
*/
|
|
41
|
+
emit(event, payload) {
|
|
42
|
+
let callbacks = this._callbacks[event];
|
|
43
|
+
if (this._active && callbacks?.size) {
|
|
44
|
+
for (let callback of callbacks) if (callback(payload) === false) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
get active() {
|
|
49
|
+
return this._active;
|
|
50
|
+
}
|
|
51
|
+
start() {
|
|
52
|
+
if (!this._active) {
|
|
53
|
+
this._active = true;
|
|
54
|
+
this.emit("start");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
stop() {
|
|
58
|
+
if (this._active) {
|
|
59
|
+
this._active = false;
|
|
60
|
+
this.emit("stop");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Serves as an alternative to `instanceof State` which can lead to a false
|
|
67
|
+
* negative when `State` comes from a transitive dependency.
|
|
68
|
+
*/
|
|
69
|
+
function isState(x) {
|
|
70
|
+
return x !== null && typeof x === "object" && "on" in x && typeof x.on === "function" && "emit" in x && typeof x.emit === "function" && "setValue" in x && typeof x.setValue === "function";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isImmediatelyInvokedEvent$1(event) {
|
|
74
|
+
return event === "set";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Data container allowing for subscription to its updates.
|
|
78
|
+
*/
|
|
79
|
+
var State = class extends EventEmitter {
|
|
80
|
+
_value;
|
|
81
|
+
_revision = -1;
|
|
82
|
+
_active = false;
|
|
83
|
+
_queue = [];
|
|
84
|
+
constructor(value, options) {
|
|
85
|
+
super();
|
|
86
|
+
this._value = value;
|
|
87
|
+
this._init();
|
|
88
|
+
if (options?.autoStart !== false) this.start();
|
|
89
|
+
}
|
|
90
|
+
_init() {}
|
|
91
|
+
_call(callback) {
|
|
92
|
+
if (this._active) callback();
|
|
93
|
+
else this._queue.push(callback);
|
|
94
|
+
}
|
|
95
|
+
on(event, callback) {
|
|
96
|
+
if (isImmediatelyInvokedEvent$1(event)) this._call(() => {
|
|
97
|
+
let current = this.getValue();
|
|
98
|
+
callback({
|
|
99
|
+
current,
|
|
100
|
+
previous: current
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return super.on(event, callback);
|
|
104
|
+
}
|
|
105
|
+
getValue() {
|
|
106
|
+
return this._value;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Updates the state value.
|
|
110
|
+
*
|
|
111
|
+
* @param update - A new value or an update function `(value) => nextValue`
|
|
112
|
+
* that returns a new state value based on the current state value.
|
|
113
|
+
*/
|
|
114
|
+
setValue(update) {
|
|
115
|
+
if (this._active) this._assignValue(this._resolveValue(update));
|
|
116
|
+
}
|
|
117
|
+
_resolveValue(update) {
|
|
118
|
+
return update instanceof Function ? update(this._value) : update;
|
|
119
|
+
}
|
|
120
|
+
_assignValue(value) {
|
|
121
|
+
let previous = this._value;
|
|
122
|
+
let current = value;
|
|
123
|
+
this._value = current;
|
|
124
|
+
this._revision = Math.random();
|
|
125
|
+
this.emit("update", {
|
|
126
|
+
previous,
|
|
127
|
+
current
|
|
128
|
+
});
|
|
129
|
+
this.emit("set", {
|
|
130
|
+
previous,
|
|
131
|
+
current
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
get revision() {
|
|
135
|
+
return this._revision;
|
|
136
|
+
}
|
|
137
|
+
start() {
|
|
138
|
+
super.start();
|
|
139
|
+
for (let callback of this._queue) callback();
|
|
140
|
+
this._queue = [];
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function getStorage(session = false) {
|
|
145
|
+
if (typeof window !== "undefined") return session ? window.sessionStorage : window.localStorage;
|
|
146
|
+
}
|
|
147
|
+
function getStorageEntry({ key, session, serialize = JSON.stringify, deserialize = JSON.parse }) {
|
|
148
|
+
let storage = getStorage(session);
|
|
149
|
+
if (!storage) return {
|
|
150
|
+
read: () => null,
|
|
151
|
+
write: () => {}
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
read() {
|
|
155
|
+
try {
|
|
156
|
+
let serializedValue = storage.getItem(key);
|
|
157
|
+
if (serializedValue !== null) return deserialize(serializedValue);
|
|
158
|
+
} catch {}
|
|
159
|
+
return null;
|
|
160
|
+
},
|
|
161
|
+
write(value) {
|
|
162
|
+
try {
|
|
163
|
+
storage.setItem(key, serialize(value));
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A container for data persistent across page reloads.
|
|
170
|
+
*/
|
|
171
|
+
var PersistentState = class extends State {
|
|
172
|
+
/**
|
|
173
|
+
* @param value - Initial state value.
|
|
174
|
+
* @param options - Either of the following:
|
|
175
|
+
* - A set of browser storage settings: `key` points to the target browser
|
|
176
|
+
* storage key where the state value should be saved; `session` set to `true`
|
|
177
|
+
* signals to use `sessionStorage` instead of `localStorage`, with the latter
|
|
178
|
+
* being the default; the optional `serialize` and `deserialize` define the
|
|
179
|
+
* way the state value is saved to and restored from the browser storage
|
|
180
|
+
* entry (default: `JSON.stringify` and `JSON.parse` respectively).
|
|
181
|
+
* - A storage singleton with a `read` and an optional `write` method
|
|
182
|
+
* (synchronous or asynchronous).
|
|
183
|
+
*/
|
|
184
|
+
constructor(value, options) {
|
|
185
|
+
super(value, { autoStart: false });
|
|
186
|
+
let { read, write } = "read" in options ? options : getStorageEntry(options);
|
|
187
|
+
let update = (value) => {
|
|
188
|
+
if (value === null) write?.(this.getValue());
|
|
189
|
+
else this.setValue(value);
|
|
190
|
+
};
|
|
191
|
+
let sync = () => {
|
|
192
|
+
let value = read();
|
|
193
|
+
if (value instanceof Promise) value.then(update);
|
|
194
|
+
else update(value);
|
|
195
|
+
};
|
|
196
|
+
if (write) this.on("update", ({ current }) => {
|
|
197
|
+
write(current);
|
|
198
|
+
});
|
|
199
|
+
this.on("sync", sync);
|
|
200
|
+
this.on("start", sync);
|
|
201
|
+
if (options?.autoStart !== false) this.start();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function isImmediatelyInvokedEvent(event) {
|
|
206
|
+
return event === "navigationstart" || event === "navigationcomplete";
|
|
207
|
+
}
|
|
208
|
+
var URLState = class extends State {
|
|
209
|
+
constructor(href = null, options) {
|
|
210
|
+
super(href ?? "", options);
|
|
211
|
+
}
|
|
212
|
+
_init() {
|
|
213
|
+
super._init();
|
|
214
|
+
if (typeof window === "undefined") return;
|
|
215
|
+
let handleURLChange = () => {
|
|
216
|
+
this.setValue(window.location.href, { source: "popstate" });
|
|
217
|
+
};
|
|
218
|
+
this.on("start", () => {
|
|
219
|
+
window.addEventListener("popstate", handleURLChange);
|
|
220
|
+
});
|
|
221
|
+
this.on("stop", () => {
|
|
222
|
+
window.removeEventListener("popstate", handleURLChange);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
on(event, callback, invokeImmediately) {
|
|
226
|
+
if (isImmediatelyInvokedEvent(event) && invokeImmediately !== false) this._call(() => {
|
|
227
|
+
callback({ href: this.getValue() });
|
|
228
|
+
});
|
|
229
|
+
return super.on(event, callback);
|
|
230
|
+
}
|
|
231
|
+
getValue() {
|
|
232
|
+
return this.toValue(this._value);
|
|
233
|
+
}
|
|
234
|
+
setValue(update, options) {
|
|
235
|
+
if (!this._active) return;
|
|
236
|
+
let href = this.toValue(this._resolveValue(update));
|
|
237
|
+
let extendedOptions = {
|
|
238
|
+
...options,
|
|
239
|
+
href,
|
|
240
|
+
referrer: this.getValue()
|
|
241
|
+
};
|
|
242
|
+
if (this.emit("navigationstart", extendedOptions) && this._transition(extendedOptions) !== false) {
|
|
243
|
+
this._assignValue(href);
|
|
244
|
+
this.emit("navigation", extendedOptions);
|
|
245
|
+
if (this.emit("navigationcomplete", extendedOptions)) this._complete(extendedOptions);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
_transition(options) {
|
|
249
|
+
if (typeof window === "undefined" || options?.href === void 0 || options?.source === "popstate") return;
|
|
250
|
+
let { href, target, spa, history } = options;
|
|
251
|
+
if (target && target !== "_self") {
|
|
252
|
+
window.open(href, target);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
let url = new quasiurl.QuasiURL(href);
|
|
256
|
+
if (spa === "off" || !window.history || url.origin !== "" && url.origin !== window.location.origin) {
|
|
257
|
+
window.location[history === "replace" ? "replace" : "assign"](href);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
window.history[history === "replace" ? "replaceState" : "pushState"]({}, "", href);
|
|
261
|
+
}
|
|
262
|
+
_complete(options) {
|
|
263
|
+
if (typeof window === "undefined" || !options || options.scroll === "off") return;
|
|
264
|
+
let { href, target } = options;
|
|
265
|
+
if (href === void 0 || target && target !== "_self") return;
|
|
266
|
+
let { hash } = new quasiurl.QuasiURL(href);
|
|
267
|
+
requestAnimationFrame(() => {
|
|
268
|
+
let targetElement = hash === "" ? null : document.querySelector(`${hash}, a[name="${hash.slice(1)}"]`);
|
|
269
|
+
if (targetElement) targetElement.scrollIntoView();
|
|
270
|
+
else window.scrollTo(0, 0);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
toValue(x) {
|
|
274
|
+
if (typeof window === "undefined") return x;
|
|
275
|
+
let url = new quasiurl.QuasiURL(x || window.location.href);
|
|
276
|
+
if (url.origin === window.location.origin) url.origin = "";
|
|
277
|
+
return url.href;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
function isLocationObject(x) {
|
|
282
|
+
return x !== null && typeof x === "object" && "exec" in x && "compile" in x && "_schema" in x;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function compileURL(urlPattern, data) {
|
|
286
|
+
if (isLocationObject(urlPattern)) return urlPattern.compile(data);
|
|
287
|
+
let url = new quasiurl.QuasiURL(urlPattern ?? "");
|
|
288
|
+
let query = data?.query;
|
|
289
|
+
if (query) url.search = new URLSearchParams(Object.entries(query).reduce((p, [k, v]) => {
|
|
290
|
+
if (v !== null && v !== void 0) p[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
291
|
+
return p;
|
|
292
|
+
}, {}));
|
|
293
|
+
return url.href;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getNavigationOptions(element) {
|
|
297
|
+
let { id, spa, history, scroll } = element.dataset;
|
|
298
|
+
return {
|
|
299
|
+
href: element.getAttribute("href"),
|
|
300
|
+
target: element.getAttribute("target"),
|
|
301
|
+
spa,
|
|
302
|
+
history,
|
|
303
|
+
scroll,
|
|
304
|
+
id
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isRouteEvent(event) {
|
|
309
|
+
return event !== null && typeof event === "object" && (!("button" in event) || event.button === 0) && (!("ctrlKey" in event) || !event.ctrlKey) && (!("shiftKey" in event) || !event.shiftKey) && (!("altKey" in event) || !event.altKey) && (!("metaKey" in event) || !event.metaKey);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function toObject(x) {
|
|
313
|
+
return x.reduce((p, v, k) => {
|
|
314
|
+
p[String(k)] = v;
|
|
315
|
+
return p;
|
|
316
|
+
}, {});
|
|
317
|
+
}
|
|
318
|
+
function matchPattern(pattern, href) {
|
|
319
|
+
let query = Object.fromEntries(new URLSearchParams(new quasiurl.QuasiURL(href).search));
|
|
320
|
+
if (typeof pattern === "string") return {
|
|
321
|
+
ok: pattern === "*" || pattern === href,
|
|
322
|
+
href,
|
|
323
|
+
params: {},
|
|
324
|
+
query
|
|
325
|
+
};
|
|
326
|
+
if (pattern instanceof RegExp) {
|
|
327
|
+
let matches = pattern.exec(href);
|
|
328
|
+
return {
|
|
329
|
+
ok: matches !== null,
|
|
330
|
+
href,
|
|
331
|
+
params: matches ? {
|
|
332
|
+
...toObject(Array.from(matches).slice(1)),
|
|
333
|
+
...matches.groups
|
|
334
|
+
} : {},
|
|
335
|
+
query
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (isLocationObject(pattern)) {
|
|
339
|
+
let result = pattern.exec(href);
|
|
340
|
+
if (result === null) return {
|
|
341
|
+
ok: false,
|
|
342
|
+
href,
|
|
343
|
+
params: {},
|
|
344
|
+
query: {}
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
href,
|
|
349
|
+
params: result.params ?? {},
|
|
350
|
+
query: result.query ?? {}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
href,
|
|
356
|
+
params: {},
|
|
357
|
+
query: {}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function matchURL(pattern, href) {
|
|
361
|
+
if (Array.isArray(pattern)) {
|
|
362
|
+
for (let p of pattern) {
|
|
363
|
+
let result = matchPattern(p, href);
|
|
364
|
+
if (result.ok) return result;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
href,
|
|
369
|
+
params: {},
|
|
370
|
+
query: {}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return matchPattern(pattern, href);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let isElementCollection = (x) => Array.isArray(x) || x instanceof NodeList || x instanceof HTMLCollection;
|
|
377
|
+
let isLinkElement = (x) => x instanceof HTMLAnchorElement || x instanceof HTMLAreaElement;
|
|
378
|
+
var Route = class extends URLState {
|
|
379
|
+
_clicks = /* @__PURE__ */ new Set();
|
|
380
|
+
constructor(href = null, options) {
|
|
381
|
+
super(String(href ?? ""), options);
|
|
382
|
+
}
|
|
383
|
+
_init() {
|
|
384
|
+
super._init();
|
|
385
|
+
if (typeof window === "undefined") return;
|
|
386
|
+
let handleClick = (event) => {
|
|
387
|
+
for (let callback of this._clicks) callback(event);
|
|
388
|
+
};
|
|
389
|
+
this.on("start", () => {
|
|
390
|
+
document.addEventListener("click", handleClick);
|
|
391
|
+
});
|
|
392
|
+
this.on("stop", () => {
|
|
393
|
+
document.removeEventListener("click", handleClick);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
observe(container, elements = "a, area") {
|
|
397
|
+
let handleClick = (event) => {
|
|
398
|
+
if (!this._active || event.defaultPrevented || !isRouteEvent(event)) return;
|
|
399
|
+
let resolvedContainer = typeof container === "function" ? container() : container;
|
|
400
|
+
if (!resolvedContainer) return;
|
|
401
|
+
let element = null;
|
|
402
|
+
let targetElements = isElementCollection(elements) ? Array.from(elements) : [elements];
|
|
403
|
+
for (let targetElement of targetElements) {
|
|
404
|
+
let target = null;
|
|
405
|
+
if (typeof targetElement === "string") target = event.target instanceof HTMLElement ? event.target.closest(targetElement) : null;
|
|
406
|
+
else target = targetElement;
|
|
407
|
+
if (isLinkElement(target) && resolvedContainer.contains(target)) {
|
|
408
|
+
element = target;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (element) {
|
|
413
|
+
event.preventDefault();
|
|
414
|
+
this.navigate(getNavigationOptions(element));
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
this._clicks.add(handleClick);
|
|
418
|
+
return () => {
|
|
419
|
+
this._clicks.delete(handleClick);
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
navigate(options) {
|
|
423
|
+
if (!options?.href) return;
|
|
424
|
+
let { href, referrer, ...params } = options;
|
|
425
|
+
let transformedOptions = {
|
|
426
|
+
href: String(href),
|
|
427
|
+
referrer: referrer && String(referrer),
|
|
428
|
+
...params
|
|
429
|
+
};
|
|
430
|
+
this.setValue(transformedOptions.href, transformedOptions);
|
|
431
|
+
}
|
|
432
|
+
assign(url) {
|
|
433
|
+
this.navigate({ href: url });
|
|
434
|
+
}
|
|
435
|
+
replace(url) {
|
|
436
|
+
this.navigate({
|
|
437
|
+
href: url,
|
|
438
|
+
history: "replace"
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
reload() {
|
|
442
|
+
this.assign(this.getValue());
|
|
443
|
+
}
|
|
444
|
+
go(delta) {
|
|
445
|
+
if (typeof window !== "undefined" && window.history) window.history.go(delta);
|
|
446
|
+
}
|
|
447
|
+
back() {
|
|
448
|
+
this.go(-1);
|
|
449
|
+
}
|
|
450
|
+
forward() {
|
|
451
|
+
this.go(1);
|
|
452
|
+
}
|
|
453
|
+
get href() {
|
|
454
|
+
return this.getValue();
|
|
455
|
+
}
|
|
456
|
+
set href(value) {
|
|
457
|
+
this.assign(value);
|
|
458
|
+
}
|
|
459
|
+
get pathname() {
|
|
460
|
+
return new quasiurl.QuasiURL(this.href).pathname;
|
|
461
|
+
}
|
|
462
|
+
set pathname(value) {
|
|
463
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
464
|
+
url.pathname = String(value);
|
|
465
|
+
this.assign(url.href);
|
|
466
|
+
}
|
|
467
|
+
get search() {
|
|
468
|
+
return new quasiurl.QuasiURL(this.href).search;
|
|
469
|
+
}
|
|
470
|
+
set search(value) {
|
|
471
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
472
|
+
url.search = value;
|
|
473
|
+
this.assign(url.href);
|
|
474
|
+
}
|
|
475
|
+
get hash() {
|
|
476
|
+
return new quasiurl.QuasiURL(this.href).hash;
|
|
477
|
+
}
|
|
478
|
+
set hash(value) {
|
|
479
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
480
|
+
url.hash = value;
|
|
481
|
+
this.assign(url.href);
|
|
482
|
+
}
|
|
483
|
+
toString() {
|
|
484
|
+
return this.href;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Matches the current location against `urlPattern`.
|
|
488
|
+
*/
|
|
489
|
+
match(urlPattern) {
|
|
490
|
+
return matchURL(urlPattern, this.href);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Compiles `urlPattern` to a URL string by filling out the parameters
|
|
494
|
+
* based on `data`.
|
|
495
|
+
*/
|
|
496
|
+
compile(urlPattern, data) {
|
|
497
|
+
return compileURL(urlPattern, data);
|
|
498
|
+
}
|
|
499
|
+
at(urlPattern, matchOutput, mismatchOutput) {
|
|
500
|
+
let result = this.match(urlPattern);
|
|
501
|
+
if (!result.ok) return typeof mismatchOutput === "function" ? mismatchOutput(result) : mismatchOutput;
|
|
502
|
+
return typeof matchOutput === "function" ? matchOutput(result) : matchOutput;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
exports.EventEmitter = EventEmitter;
|
|
507
|
+
exports.PersistentState = PersistentState;
|
|
508
|
+
exports.Route = Route;
|
|
509
|
+
exports.State = State;
|
|
510
|
+
exports.URLState = URLState;
|
|
511
|
+
exports.compileURL = compileURL;
|
|
512
|
+
exports.getNavigationOptions = getNavigationOptions;
|
|
513
|
+
exports.getStorageEntry = getStorageEntry;
|
|
514
|
+
exports.isLocationObject = isLocationObject;
|
|
515
|
+
exports.isRouteEvent = isRouteEvent;
|
|
516
|
+
exports.isState = isState;
|
|
517
|
+
exports.matchURL = matchURL;
|