@inglorious/web 6.3.0 → 6.4.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/package.json +37 -13
- package/src/decorators/with-error-boundary/index.js +48 -0
- package/src/decorators/with-throttle/index.js +107 -0
- package/src/directives/unsafe-svg.js +1 -0
- package/src/mobile/index.js +17 -0
- package/src/mount.js +57 -6
- package/src/{compass → sensors/compass}/handlers.js +2 -2
- package/src/{element-size → sensors/element-size}/handlers.js +8 -8
- package/src/{geolocation → sensors/geolocation}/handlers.js +3 -3
- package/src/sensors/media-query/handlers.js +86 -0
- package/src/sensors/media-query/index.js +3 -0
- package/src/sensors/network-status/handlers.js +84 -0
- package/src/sensors/network-status/index.js +3 -0
- package/src/sensors/page-visibility/handlers.js +69 -0
- package/src/sensors/page-visibility/index.js +3 -0
- package/types/decorators/with-debounce.d.ts +7 -2
- package/types/decorators/with-error-boundary.d.ts +11 -0
- package/types/decorators/with-throttle.d.ts +24 -0
- package/types/mobile.d.ts +1 -0
- package/types/mount.d.ts +22 -0
- package/types/{compass.d.ts → sensors/compass.d.ts} +1 -1
- package/types/{element-size.d.ts → sensors/element-size.d.ts} +1 -1
- package/types/{geolocation.d.ts → sensors/geolocation.d.ts} +1 -1
- package/types/sensors/media-query.d.ts +24 -0
- package/types/sensors/network-status.d.ts +23 -0
- package/types/sensors/page-visibility.d.ts +23 -0
- /package/src/{compass → sensors/compass}/index.js +0 -0
- /package/src/{element-size → sensors/element-size}/index.js +0 -0
- /package/src/{geolocation → sensors/geolocation}/index.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"contributors": [
|
|
@@ -33,35 +33,59 @@
|
|
|
33
33
|
"types": "./types/index.d.ts",
|
|
34
34
|
"import": "./src/index.js"
|
|
35
35
|
},
|
|
36
|
-
"./compass": {
|
|
37
|
-
"types": "./types/compass.d.ts",
|
|
38
|
-
"import": "./src/compass/index.js"
|
|
39
|
-
},
|
|
40
36
|
"./decorators/with-debounce": {
|
|
41
37
|
"types": "./types/decorators/with-debounce.d.ts",
|
|
42
38
|
"import": "./src/decorators/with-debounce/index.js"
|
|
43
39
|
},
|
|
40
|
+
"./decorators/with-error-boundary": {
|
|
41
|
+
"types": "./types/decorators/with-error-boundary.d.ts",
|
|
42
|
+
"import": "./src/decorators/with-error-boundary/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./decorators/with-throttle": {
|
|
45
|
+
"types": "./types/decorators/with-throttle.d.ts",
|
|
46
|
+
"import": "./src/decorators/with-throttle/index.js"
|
|
47
|
+
},
|
|
44
48
|
"./directives/*": {
|
|
45
49
|
"types": "./types/directives/*.d.ts",
|
|
46
50
|
"import": "./src/directives/*.js"
|
|
47
51
|
},
|
|
48
|
-
"./element-size": {
|
|
49
|
-
"types": "./types/element-size.d.ts",
|
|
50
|
-
"import": "./src/element-size/index.js"
|
|
51
|
-
},
|
|
52
52
|
"./form": {
|
|
53
53
|
"types": "./types/form.d.ts",
|
|
54
54
|
"import": "./src/form/index.js"
|
|
55
55
|
},
|
|
56
|
-
"./geolocation": {
|
|
57
|
-
"types": "./types/geolocation.d.ts",
|
|
58
|
-
"import": "./src/geolocation/index.js"
|
|
59
|
-
},
|
|
60
56
|
"./hydrate": "./src/hydrate.js",
|
|
57
|
+
"./mobile": {
|
|
58
|
+
"types": "./types/mobile.d.ts",
|
|
59
|
+
"import": "./src/mobile/index.js"
|
|
60
|
+
},
|
|
61
61
|
"./router": {
|
|
62
62
|
"types": "./types/router.d.ts",
|
|
63
63
|
"import": "./src/router/index.js"
|
|
64
64
|
},
|
|
65
|
+
"./sensors/compass": {
|
|
66
|
+
"types": "./types/sensors/compass.d.ts",
|
|
67
|
+
"import": "./src/sensors/compass/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./sensors/element-size": {
|
|
70
|
+
"types": "./types/sensors/element-size.d.ts",
|
|
71
|
+
"import": "./src/sensors/element-size/index.js"
|
|
72
|
+
},
|
|
73
|
+
"./sensors/geolocation": {
|
|
74
|
+
"types": "./types/sensors/geolocation.d.ts",
|
|
75
|
+
"import": "./src/sensors/geolocation/index.js"
|
|
76
|
+
},
|
|
77
|
+
"./sensors/media-query": {
|
|
78
|
+
"types": "./types/sensors/media-query.d.ts",
|
|
79
|
+
"import": "./src/sensors/media-query/index.js"
|
|
80
|
+
},
|
|
81
|
+
"./sensors/network-status": {
|
|
82
|
+
"types": "./types/sensors/network-status.d.ts",
|
|
83
|
+
"import": "./src/sensors/network-status/index.js"
|
|
84
|
+
},
|
|
85
|
+
"./sensors/page-visibility": {
|
|
86
|
+
"types": "./types/sensors/page-visibility.d.ts",
|
|
87
|
+
"import": "./src/sensors/page-visibility/index.js"
|
|
88
|
+
},
|
|
65
89
|
"./static": {
|
|
66
90
|
"types": "./types/static.d.ts",
|
|
67
91
|
"import": "./src/static.js"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../../../types/mount").Api} Api
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a type's `render` in an error boundary. If the base render throws
|
|
7
|
+
* synchronously, the caught error is handed to `fallback` and its template is
|
|
8
|
+
* rendered instead — so one failing entity degrades to a fallback UI rather
|
|
9
|
+
* than taking down the whole app (every child is rendered through the same
|
|
10
|
+
* `render`, so there is no isolation without this).
|
|
11
|
+
*
|
|
12
|
+
* Because the wrap is deterministic, the *same* error produces the *same*
|
|
13
|
+
* fallback during SSR and during client hydration, so it never introduces a
|
|
14
|
+
* hydration mismatch.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: like React/Svelte boundaries, this only catches errors thrown *during
|
|
17
|
+
* render*. Errors thrown later — in event handlers, async work (thunks), or in
|
|
18
|
+
* lit-html directives committed after `render` returns — are not caught here.
|
|
19
|
+
* Those are the store phase, not the render phase: handle them as entity
|
|
20
|
+
* `error` state in your handlers (see e.g. the geolocation type).
|
|
21
|
+
*
|
|
22
|
+
* @param {(error: Error, entity: object, api: Api) => unknown} [fallback]
|
|
23
|
+
* Renders the fallback UI. Receives the caught error, the entity, and the
|
|
24
|
+
* render api. Defaults to logging the error and rendering nothing.
|
|
25
|
+
* @returns {(type: object) => { render: (entity: object, api: Api) => unknown }}
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Compose after the base type so the boundary wraps its render.
|
|
29
|
+
* const types = {
|
|
30
|
+
* Chart: [ChartBase, withErrorBoundary((err) => html`<p>Chart failed: ${err.message}</p>`)],
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
export function withErrorBoundary(fallback = defaultFallback) {
|
|
34
|
+
return (type) => ({
|
|
35
|
+
render(entity, api) {
|
|
36
|
+
try {
|
|
37
|
+
return type.render?.(entity, api) ?? ""
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return fallback(error, entity, api)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultFallback(error) {
|
|
46
|
+
console.error("[withErrorBoundary] render failed:", error)
|
|
47
|
+
return ""
|
|
48
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { throttle } from "@inglorious/utils/functions/functions.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps selected event handlers of a type with throttling, scoped per-entity.
|
|
5
|
+
*
|
|
6
|
+
* By default this is leading-edge only: the first call in a burst runs
|
|
7
|
+
* immediately, and further calls are dropped until `delay` has elapsed — the
|
|
8
|
+
* last call of a burst may be lost. Pass `{ hasTrailing: true }` to also invoke
|
|
9
|
+
* the handler once more after `delay`, using the most recent suppressed
|
|
10
|
+
* call's arguments, so the final state of a burst is never dropped.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: because state is managed with `mutative`, the `entity` object
|
|
13
|
+
* passed into a handler is a draft proxy that gets revoked as soon as the
|
|
14
|
+
* current update tick finishes. The leading call always runs synchronously
|
|
15
|
+
* (same tick as the triggering event), so the draft is still live there —
|
|
16
|
+
* but a trailing call (when `hasTrailing: true`) fires *later*, on its own
|
|
17
|
+
* timer, after that draft is dead. To keep one code path safe for both
|
|
18
|
+
* cases, we never close over `entity` itself across the throttle boundary —
|
|
19
|
+
* only its primitive `id`. Every time the wrapped handler actually runs, we
|
|
20
|
+
* re-fetch a live entity from the current state via `api.getEntity(entityId)`.
|
|
21
|
+
* Primitives and `payload` are safe to hold onto as-is; only draft/entity
|
|
22
|
+
* references are not.
|
|
23
|
+
*
|
|
24
|
+
* @param {number|Object} config - Either a single delay (ms) applied to all
|
|
25
|
+
* handlers listed in `handlerNames`, or a map of handlerName -> delay (ms).
|
|
26
|
+
* @param {string[]} [handlerNames] - Handler names to throttle. Required when
|
|
27
|
+
* `config` is a single number; ignored when `config` is a map (its keys
|
|
28
|
+
* are used instead).
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {boolean} [options.hasTrailing=false] - Whether throttled handlers
|
|
31
|
+
* should also fire once more after `delay`, using the last suppressed
|
|
32
|
+
* call's arguments. Applies to every handler wrapped by this call.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // leading-edge only (default): rate-limit a frequent event, last call in
|
|
36
|
+
* // a burst may be dropped
|
|
37
|
+
* const types = {
|
|
38
|
+
* ScrollTracker: [Base, withThrottle(200, ["scroll"])],
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // leading + trailing: never lose the final call of a burst
|
|
43
|
+
* const types = {
|
|
44
|
+
* ResizablePanel: [Base, withThrottle({ resize: 100 }, undefined, { hasTrailing: true })],
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export function withThrottle(
|
|
48
|
+
config,
|
|
49
|
+
handlerNames,
|
|
50
|
+
{ hasTrailing = false } = {},
|
|
51
|
+
) {
|
|
52
|
+
const delays =
|
|
53
|
+
typeof config === "number"
|
|
54
|
+
? Object.fromEntries(handlerNames.map((name) => [name, config]))
|
|
55
|
+
: config
|
|
56
|
+
|
|
57
|
+
return (type) => {
|
|
58
|
+
// one throttled-fn map per entity, so instances don't share windows
|
|
59
|
+
const throttledByEntity = new Map()
|
|
60
|
+
|
|
61
|
+
const behavior = {}
|
|
62
|
+
|
|
63
|
+
for (const handlerName of Object.keys(delays)) {
|
|
64
|
+
const originalHandler = type[handlerName]
|
|
65
|
+
if (!originalHandler) continue // nothing to wrap if the base type doesn't define it
|
|
66
|
+
|
|
67
|
+
behavior[handlerName] = (entity, payload, api) => {
|
|
68
|
+
const entityId = entity.id // capture the primitive now, while the draft is live
|
|
69
|
+
|
|
70
|
+
let perEntity = throttledByEntity.get(entityId)
|
|
71
|
+
if (!perEntity) {
|
|
72
|
+
perEntity = {}
|
|
73
|
+
throttledByEntity.set(entityId, perEntity)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!perEntity[handlerName]) {
|
|
77
|
+
perEntity[handlerName] = throttle(
|
|
78
|
+
(entityId, payload, api) => {
|
|
79
|
+
const liveEntity = api.getEntity(entityId) // fresh, live reference
|
|
80
|
+
if (!liveEntity) return // entity was destroyed before this fired
|
|
81
|
+
originalHandler(liveEntity, payload, api)
|
|
82
|
+
},
|
|
83
|
+
delays[handlerName],
|
|
84
|
+
{ hasTrailing },
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
perEntity[handlerName](entityId, payload, api)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// clean up any pending windows/trailing calls when the entity is destroyed
|
|
93
|
+
behavior.destroy = (entity, payload, api) => {
|
|
94
|
+
const entityId = entity.id // same rule applies here: capture before it's gone
|
|
95
|
+
|
|
96
|
+
const perEntity = throttledByEntity.get(entityId)
|
|
97
|
+
if (perEntity) {
|
|
98
|
+
Object.values(perEntity).forEach((t) => t.cancel())
|
|
99
|
+
throttledByEntity.delete(entityId)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type.destroy?.(entity, payload, api)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return behavior
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { unsafeSVG } from "lit-html/directives/unsafe-svg.js"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function isNative() {
|
|
2
|
+
const capacitor = globalThis.Capacitor ?? globalThis.window?.Capacitor ?? null
|
|
3
|
+
|
|
4
|
+
if (!capacitor) {
|
|
5
|
+
return false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof capacitor.isNativePlatform === "function") {
|
|
9
|
+
return capacitor.isNativePlatform()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof capacitor.getPlatform === "function") {
|
|
13
|
+
return ["android", "ios"].includes(capacitor.getPlatform())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return capacitor.isNative === true
|
|
17
|
+
}
|
package/src/mount.js
CHANGED
|
@@ -5,14 +5,47 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, render } from "lit-html"
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Root-level error boundary. Unlike the per-entity `withErrorBoundary`
|
|
10
|
+
* decorator (which isolates a single failing type's render), this catches
|
|
11
|
+
* errors thrown by the *root* render function itself — before any entity is
|
|
12
|
+
* reached (e.g. a missing router entity), or by an unguarded child.
|
|
13
|
+
*
|
|
14
|
+
* Without it, a throwing root render takes down the whole app: on the initial
|
|
15
|
+
* pass it aborts the mount, and inside the store subscription it throws out of
|
|
16
|
+
* the notify loop on every subsequent update, freezing the UI. Catching it
|
|
17
|
+
* keeps the subscription alive so one bad tick can't kill the app.
|
|
18
|
+
*
|
|
19
|
+
* @param {(api: Api) => TemplateResult | null} renderFn
|
|
20
|
+
* @param {Api} api
|
|
21
|
+
* @param {(error: unknown, api: Api) => void} onError
|
|
22
|
+
* @param {((error: unknown, api: Api) => TemplateResult | null) | undefined} fallback
|
|
23
|
+
* @returns {{ ok: true, template: TemplateResult | null } | { ok: false, fallback: TemplateResult | null }}
|
|
24
|
+
*/
|
|
25
|
+
function renderRoot(renderFn, api, onError, fallback) {
|
|
26
|
+
try {
|
|
27
|
+
return { ok: true, template: renderFn(api) }
|
|
28
|
+
} catch (error) {
|
|
29
|
+
onError(error, api)
|
|
30
|
+
return { ok: false, fallback: fallback ? fallback(error, api) : null }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Mounts a lit-html template to the DOM and subscribes to a store for re-rendering.
|
|
10
36
|
* @param {Store} store - The application state store.
|
|
11
37
|
* @param {(api: Api) => TemplateResult | null} renderFn - The root render function.
|
|
12
38
|
* @param {HTMLElement | DocumentFragment} element - The DOM element to mount the template to.
|
|
39
|
+
* @param {Object} [options] - Root error-boundary options.
|
|
40
|
+
* @param {(error: unknown, api: Api) => void} [options.onError] - Called when the
|
|
41
|
+
* root render throws. Defaults to `console.error`.
|
|
42
|
+
* @param {(error: unknown, api: Api) => TemplateResult | null} [options.fallback] -
|
|
43
|
+
* Optional fallback template rendered in place of the failed root render.
|
|
13
44
|
* @returns {() => void} An unsubscribe function
|
|
14
45
|
*/
|
|
15
|
-
export async function mount(store, renderFn, element) {
|
|
46
|
+
export async function mount(store, renderFn, element, options = {}) {
|
|
47
|
+
const { onError = defaultOnError, fallback } = options
|
|
48
|
+
|
|
16
49
|
const api = { ...store._api }
|
|
17
50
|
api.render = createRender(api)
|
|
18
51
|
|
|
@@ -21,20 +54,38 @@ export async function mount(store, renderFn, element) {
|
|
|
21
54
|
// If we need to hydrate, load the heavy lifting only now
|
|
22
55
|
if (shouldHydrate) {
|
|
23
56
|
const { hydrate } = await import("@lit-labs/ssr-client")
|
|
24
|
-
const
|
|
25
|
-
|
|
57
|
+
const result = renderRoot(renderFn, api, onError, fallback)
|
|
58
|
+
if (result.ok) {
|
|
59
|
+
hydrate(result.template, element)
|
|
60
|
+
} else if (fallback) {
|
|
61
|
+
// Can't hydrate a render that threw: replace the server markup with the
|
|
62
|
+
// fallback via a fresh client render instead.
|
|
63
|
+
render(result.fallback, element)
|
|
64
|
+
}
|
|
26
65
|
}
|
|
27
66
|
|
|
28
67
|
const unsubscribe = store.subscribe(() => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
const result = renderRoot(renderFn, api, onError, fallback)
|
|
69
|
+
if (result.ok) {
|
|
70
|
+
// Standard render is already in the main bundle (it's small)
|
|
71
|
+
render(result.template, element)
|
|
72
|
+
} else if (fallback) {
|
|
73
|
+
render(result.fallback, element)
|
|
74
|
+
}
|
|
32
75
|
})
|
|
33
76
|
|
|
34
77
|
store.notify("init")
|
|
35
78
|
return unsubscribe
|
|
36
79
|
}
|
|
37
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Default root-error handler: log without taking the app down.
|
|
83
|
+
* @param {unknown} error - The error thrown by the root render function.
|
|
84
|
+
*/
|
|
85
|
+
function defaultOnError(error) {
|
|
86
|
+
console.error("[mount] root render failed:", error)
|
|
87
|
+
}
|
|
88
|
+
|
|
38
89
|
/**
|
|
39
90
|
* Creates a render function for the mount API.
|
|
40
91
|
* @param {Api} api - The mount API.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import("
|
|
3
|
-
* @typedef {import("
|
|
2
|
+
* @typedef {import("../../../types/sensors/compass.js").CompassEntity} CompassEntity
|
|
3
|
+
* @typedef {import("../../../types/sensors/compass.js").CompassError} CompassError
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const COMPASS_TIMEOUT = 3000
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import("
|
|
3
|
-
* @typedef {import("
|
|
2
|
+
* @typedef {import("../../../types/sensors/element-size.js").ElementSizeEntity} ElementSizeEntity
|
|
3
|
+
* @typedef {import("../../../types/sensors/element-size.js").ElementSizeSize} ElementSizeSize
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
const
|
|
6
|
+
// Listeners aren't serializable, so they're kept here by id, not on the entity.
|
|
7
|
+
const listeners = new Map()
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Initializes the entity state and starts observing the target element's size.
|
|
@@ -35,7 +35,7 @@ export function destroy(entity) {
|
|
|
35
35
|
export function elementSizeWatch(entity, _, api) {
|
|
36
36
|
entity.isSupported = isSupported()
|
|
37
37
|
|
|
38
|
-
if (!entity.isSupported ||
|
|
38
|
+
if (!entity.isSupported || listeners.has(entity.id)) {
|
|
39
39
|
return
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -54,7 +54,7 @@ export function elementSizeWatch(entity, _, api) {
|
|
|
54
54
|
})
|
|
55
55
|
observer.observe(element)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
listeners.set(id, observer)
|
|
58
58
|
entity.isWatching = true
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -73,11 +73,11 @@ export function elementSizeChange(entity, size) {
|
|
|
73
73
|
* @param {ElementSizeEntity} entity - The element size entity.
|
|
74
74
|
*/
|
|
75
75
|
export function elementSizeUnwatch(entity) {
|
|
76
|
-
const observer =
|
|
76
|
+
const observer = listeners.get(entity.id)
|
|
77
77
|
|
|
78
78
|
if (observer) {
|
|
79
79
|
observer.disconnect()
|
|
80
|
-
|
|
80
|
+
listeners.delete(entity.id)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
entity.isWatching = false
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import("
|
|
3
|
-
* @typedef {import("
|
|
4
|
-
* @typedef {import("
|
|
2
|
+
* @typedef {import("../../../types/sensors/geolocation.js").GeolocationEntity} GeolocationEntity
|
|
3
|
+
* @typedef {import("../../../types/sensors/geolocation.js").GeolocationOptions} GeolocationOptions
|
|
4
|
+
* @typedef {import("../../../types/sensors/geolocation.js").GeolocationPosition} GeolocationPosition
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const NO_WATCH_ID = null
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../../../types/sensors/media-query.js").MediaQueryEntity} MediaQueryEntity
|
|
3
|
+
* @typedef {import("../../../types/sensors/media-query.js").MediaQueryMatchEvent} MediaQueryMatchEvent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const listeners = new Map()
|
|
7
|
+
|
|
8
|
+
export function create(entity, _, api) {
|
|
9
|
+
entity.isSupported = isSupported()
|
|
10
|
+
entity.matches = getMatchState(entity)
|
|
11
|
+
entity.media = entity.media ?? ""
|
|
12
|
+
entity.isWatching ??= false
|
|
13
|
+
|
|
14
|
+
mediaQueryWatch(entity, _, api)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function destroy(entity) {
|
|
18
|
+
mediaQueryUnwatch(entity)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mediaQueryChange(entity, event) {
|
|
22
|
+
entity.matches = event.matches
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mediaQueryWatch(entity, _, api) {
|
|
26
|
+
entity.isSupported = isSupported()
|
|
27
|
+
|
|
28
|
+
if (!entity.isSupported || entity.isWatching) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mql = getMediaQueryList(entity)
|
|
33
|
+
if (!mql) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Snapshot the id while the draft proxy is still live: the callback below
|
|
38
|
+
// runs after this handler returns, when Mutative has revoked the draft.
|
|
39
|
+
const id = entity.id
|
|
40
|
+
const listener = (event) => api.notify(`#${id}:mediaQueryChange`, event)
|
|
41
|
+
|
|
42
|
+
listeners.set(id, { mql, listener })
|
|
43
|
+
entity.isWatching = true
|
|
44
|
+
|
|
45
|
+
if (typeof mql.addEventListener === "function") {
|
|
46
|
+
mql.addEventListener("change", listener)
|
|
47
|
+
} else if (typeof mql.addListener === "function") {
|
|
48
|
+
mql.addListener(listener)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function mediaQueryUnwatch(entity) {
|
|
53
|
+
const registration = listeners.get(entity.id)
|
|
54
|
+
|
|
55
|
+
if (!registration) {
|
|
56
|
+
entity.isWatching = false
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { mql, listener } = registration
|
|
61
|
+
|
|
62
|
+
if (typeof mql.removeEventListener === "function") {
|
|
63
|
+
mql.removeEventListener("change", listener)
|
|
64
|
+
} else if (typeof mql.removeListener === "function") {
|
|
65
|
+
mql.removeListener(listener)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
listeners.delete(entity.id)
|
|
69
|
+
entity.isWatching = false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getMediaQueryList(entity) {
|
|
73
|
+
if (typeof globalThis.matchMedia !== "function") {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return globalThis.matchMedia(entity.media ?? "")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getMatchState(entity) {
|
|
81
|
+
return getMediaQueryList(entity)?.matches ?? false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSupported() {
|
|
85
|
+
return typeof globalThis.matchMedia === "function"
|
|
86
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../../../types/sensors/network-status.js").NetworkStatusEntity} NetworkStatusEntity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ONLINE_EVENT = "online"
|
|
6
|
+
const OFFLINE_EVENT = "offline"
|
|
7
|
+
const listeners = new Map()
|
|
8
|
+
|
|
9
|
+
export function create(entity, _, api) {
|
|
10
|
+
entity.isSupported = isSupported()
|
|
11
|
+
entity.isOnline = getOnlineState()
|
|
12
|
+
entity.isWatching ??= false
|
|
13
|
+
|
|
14
|
+
networkStatusWatch(entity, _, api)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function destroy(entity) {
|
|
18
|
+
networkStatusUnwatch(entity)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function networkStatusChange(entity, value) {
|
|
22
|
+
entity.isOnline = value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function networkStatusWatch(entity, _, api) {
|
|
26
|
+
entity.isSupported = isSupported()
|
|
27
|
+
|
|
28
|
+
if (!entity.isSupported || entity.isWatching) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const win = getWindow()
|
|
33
|
+
if (!win) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
entity.isOnline = getOnlineState()
|
|
38
|
+
|
|
39
|
+
// Snapshot the id while the draft proxy is still live: the callbacks below
|
|
40
|
+
// run after this handler returns, when Mutative has revoked the draft.
|
|
41
|
+
const id = entity.id
|
|
42
|
+
const onlineListener = () => api.notify(`#${id}:networkStatusChange`, true)
|
|
43
|
+
const offlineListener = () => api.notify(`#${id}:networkStatusChange`, false)
|
|
44
|
+
|
|
45
|
+
listeners.set(id, { onlineListener, offlineListener })
|
|
46
|
+
|
|
47
|
+
win.addEventListener(ONLINE_EVENT, onlineListener)
|
|
48
|
+
win.addEventListener(OFFLINE_EVENT, offlineListener)
|
|
49
|
+
entity.isWatching = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function networkStatusUnwatch(entity) {
|
|
53
|
+
const win = getWindow()
|
|
54
|
+
const listenerPair = listeners.get(entity.id)
|
|
55
|
+
|
|
56
|
+
if (!listenerPair) {
|
|
57
|
+
entity.isWatching = false
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (win) {
|
|
62
|
+
win.removeEventListener(ONLINE_EVENT, listenerPair.onlineListener)
|
|
63
|
+
win.removeEventListener(OFFLINE_EVENT, listenerPair.offlineListener)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
listeners.delete(entity.id)
|
|
67
|
+
entity.isWatching = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getWindow() {
|
|
71
|
+
return typeof window !== "undefined" ? window : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getOnlineState() {
|
|
75
|
+
return Boolean(globalThis.navigator?.onLine)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isSupported() {
|
|
79
|
+
return (
|
|
80
|
+
typeof window !== "undefined" &&
|
|
81
|
+
Boolean(window.addEventListener) &&
|
|
82
|
+
typeof globalThis.navigator?.onLine === "boolean"
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../../../types/sensors/page-visibility.js").PageVisibilityEntity} PageVisibilityEntity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const VISIBILITY_EVENT = "visibilitychange"
|
|
6
|
+
const listeners = new Map()
|
|
7
|
+
|
|
8
|
+
export function create(entity, _, api) {
|
|
9
|
+
entity.isSupported = isSupported()
|
|
10
|
+
entity.isVisible = getVisibilityState()
|
|
11
|
+
entity.isWatching ??= false
|
|
12
|
+
|
|
13
|
+
pageVisibilityWatch(entity, _, api)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function destroy(entity) {
|
|
17
|
+
pageVisibilityUnwatch(entity)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pageVisibilityChange(entity, value) {
|
|
21
|
+
entity.isVisible = value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pageVisibilityWatch(entity, _, api) {
|
|
25
|
+
entity.isSupported = isSupported()
|
|
26
|
+
|
|
27
|
+
if (!entity.isSupported || entity.isWatching) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const doc = getDocument()
|
|
32
|
+
if (!doc) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Snapshot the id while the draft proxy is still live: the callback below
|
|
37
|
+
// runs after this handler returns, when Mutative has revoked the draft.
|
|
38
|
+
const id = entity.id
|
|
39
|
+
const listener = () => api.notify(`#${id}:pageVisibilityChange`, !doc.hidden)
|
|
40
|
+
|
|
41
|
+
listeners.set(id, listener)
|
|
42
|
+
doc.addEventListener(VISIBILITY_EVENT, listener)
|
|
43
|
+
entity.isWatching = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function pageVisibilityUnwatch(entity) {
|
|
47
|
+
const doc = getDocument()
|
|
48
|
+
const listener = listeners.get(entity.id)
|
|
49
|
+
|
|
50
|
+
if (listener && doc) {
|
|
51
|
+
doc.removeEventListener(VISIBILITY_EVENT, listener)
|
|
52
|
+
listeners.delete(entity.id)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
entity.isWatching = false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getDocument() {
|
|
59
|
+
return typeof document !== "undefined" ? document : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getVisibilityState() {
|
|
63
|
+
const doc = getDocument()
|
|
64
|
+
return doc?.visibilityState !== "hidden"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isSupported() {
|
|
68
|
+
return Boolean(getDocument()?.visibilityState)
|
|
69
|
+
}
|
|
@@ -8,6 +8,11 @@ export interface DebounceBehavior {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export declare function withDebounce(
|
|
11
|
-
config: number
|
|
12
|
-
handlerNames
|
|
11
|
+
config: number,
|
|
12
|
+
handlerNames: string[],
|
|
13
|
+
): <T extends Record<string, any>>(type: T) => DebounceBehavior
|
|
14
|
+
|
|
15
|
+
export declare function withDebounce(
|
|
16
|
+
config: Record<string, number>,
|
|
17
|
+
handlerNames?: undefined,
|
|
13
18
|
): <T extends Record<string, any>>(type: T) => DebounceBehavior
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TemplateResult } from "lit-html"
|
|
2
|
+
|
|
3
|
+
import { Api } from "../mount"
|
|
4
|
+
|
|
5
|
+
export interface ErrorBoundaryBehavior {
|
|
6
|
+
render: (entity: any, api: Api) => TemplateResult | string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare function withErrorBoundary(
|
|
10
|
+
fallback?: (error: Error, entity: any, api: Api) => TemplateResult | string,
|
|
11
|
+
): <T extends Record<string, any>>(type: T) => ErrorBoundaryBehavior
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Api } from "../mount"
|
|
2
|
+
|
|
3
|
+
export interface ThrottleOptions {
|
|
4
|
+
hasTrailing?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ThrottleBehavior {
|
|
8
|
+
destroy?: (entity: any, payload?: any, api?: Api) => void
|
|
9
|
+
[handlerName: string]:
|
|
10
|
+
| ((entity: any, payload?: any, api?: Api) => void)
|
|
11
|
+
| undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare function withThrottle(
|
|
15
|
+
config: number,
|
|
16
|
+
handlerNames: string[],
|
|
17
|
+
options?: ThrottleOptions,
|
|
18
|
+
): <T extends Record<string, any>>(type: T) => ThrottleBehavior
|
|
19
|
+
|
|
20
|
+
export declare function withThrottle(
|
|
21
|
+
config: Record<string, number>,
|
|
22
|
+
handlerNames?: undefined,
|
|
23
|
+
options?: ThrottleOptions,
|
|
24
|
+
): <T extends Record<string, any>>(type: T) => ThrottleBehavior
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isNative(): boolean
|
package/types/mount.d.ts
CHANGED
|
@@ -28,11 +28,32 @@ export type Api<
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Root error-boundary options for `mount`.
|
|
33
|
+
*/
|
|
34
|
+
export interface MountOptions<
|
|
35
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
36
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
37
|
+
> {
|
|
38
|
+
/**
|
|
39
|
+
* Called when the root render function throws. Defaults to `console.error`.
|
|
40
|
+
*/
|
|
41
|
+
onError?: (error: unknown, api: Api<TEntity, TState>) => void
|
|
42
|
+
/**
|
|
43
|
+
* Optional fallback template rendered in place of the failed root render.
|
|
44
|
+
*/
|
|
45
|
+
fallback?: (
|
|
46
|
+
error: unknown,
|
|
47
|
+
api: Api<TEntity, TState>,
|
|
48
|
+
) => TemplateResult | null
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Mounts a lit-html template to the DOM and subscribes to a store for re-rendering.
|
|
33
53
|
* @param store The application state store.
|
|
34
54
|
* @param renderFn The root render function that receives the API and returns a template.
|
|
35
55
|
* @param element The DOM element to mount the template to.
|
|
56
|
+
* @param options Root error-boundary options (`onError`, `fallback`).
|
|
36
57
|
* @returns An unsubscribe function.
|
|
37
58
|
*/
|
|
38
59
|
export function mount<
|
|
@@ -42,4 +63,5 @@ export function mount<
|
|
|
42
63
|
store: Store<TEntity, TState>,
|
|
43
64
|
renderFn: (api: Api<TEntity, TState>) => TemplateResult | null,
|
|
44
65
|
element: HTMLElement | DocumentFragment,
|
|
66
|
+
options?: MountOptions<TEntity, TState>,
|
|
45
67
|
): () => void
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Api } from "../mount"
|
|
2
|
+
|
|
3
|
+
export interface MediaQueryEntity {
|
|
4
|
+
id: string | number
|
|
5
|
+
type?: "MediaQuery"
|
|
6
|
+
media: string
|
|
7
|
+
matches: boolean
|
|
8
|
+
isSupported: boolean
|
|
9
|
+
isWatching: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MediaQueryMatchEvent {
|
|
13
|
+
matches: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MediaQueryType {
|
|
17
|
+
create(entity: MediaQueryEntity, payload: unknown, api: Api): void
|
|
18
|
+
destroy(entity: MediaQueryEntity): void
|
|
19
|
+
mediaQueryChange(entity: MediaQueryEntity, event: MediaQueryMatchEvent): void
|
|
20
|
+
mediaQueryWatch(entity: MediaQueryEntity, payload: unknown, api: Api): void
|
|
21
|
+
mediaQueryUnwatch(entity: MediaQueryEntity): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare const MediaQuery: MediaQueryType
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Api } from "../mount"
|
|
2
|
+
|
|
3
|
+
export interface NetworkStatusEntity {
|
|
4
|
+
id: string | number
|
|
5
|
+
type?: "NetworkStatus"
|
|
6
|
+
isOnline: boolean
|
|
7
|
+
isSupported: boolean
|
|
8
|
+
isWatching: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NetworkStatusType {
|
|
12
|
+
create(entity: NetworkStatusEntity, payload: unknown, api: Api): void
|
|
13
|
+
destroy(entity: NetworkStatusEntity, payload: unknown, api: Api): void
|
|
14
|
+
networkStatusChange(entity: NetworkStatusEntity, value: boolean): void
|
|
15
|
+
networkStatusWatch(
|
|
16
|
+
entity: NetworkStatusEntity,
|
|
17
|
+
payload: unknown,
|
|
18
|
+
api: Api,
|
|
19
|
+
): void
|
|
20
|
+
networkStatusUnwatch(entity: NetworkStatusEntity): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export declare const NetworkStatus: NetworkStatusType
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Api } from "../mount"
|
|
2
|
+
|
|
3
|
+
export interface PageVisibilityEntity {
|
|
4
|
+
id: string | number
|
|
5
|
+
type?: "PageVisibility"
|
|
6
|
+
isSupported: boolean
|
|
7
|
+
isVisible: boolean
|
|
8
|
+
isWatching: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PageVisibilityType {
|
|
12
|
+
create(entity: PageVisibilityEntity, payload: unknown, api: Api): void
|
|
13
|
+
destroy(entity: PageVisibilityEntity, payload: unknown, api: Api): void
|
|
14
|
+
pageVisibilityChange(entity: PageVisibilityEntity, value: boolean): void
|
|
15
|
+
pageVisibilityWatch(
|
|
16
|
+
entity: PageVisibilityEntity,
|
|
17
|
+
payload: unknown,
|
|
18
|
+
api: Api,
|
|
19
|
+
): void
|
|
20
|
+
pageVisibilityUnwatch(entity: PageVisibilityEntity): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export declare const PageVisibility: PageVisibilityType
|
|
File without changes
|
|
File without changes
|
|
File without changes
|