@escape-game-over/atlas 0.1.25 → 0.1.26
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/docs/client-scripts.md +55 -84
- package/docs/rich-text.md +9 -4
- package/package.json +5 -4
- package/src/astro/AtlasElement.astro +26 -0
- package/src/astro/ConsentElement.astro +18 -12
- package/src/astro/Image.astro +1 -2
- package/src/astro/MetaTags.astro +27 -14
- package/src/astro/RichText.astro +60 -43
- package/src/astro/client.ts +7 -8
- package/src/astro/consent.ts +10 -10
- package/src/astro/element.ts +57 -92
- package/src/astro/ref.ts +46 -0
- package/src/astro/markup.ts +0 -656
package/src/astro/element.ts
CHANGED
|
@@ -1,105 +1,91 @@
|
|
|
1
1
|
import { reportDevError } from "./dev-log.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type MarkupFields,
|
|
10
|
-
type Reserved,
|
|
11
|
-
ROOT_ATTRIBUTE,
|
|
12
|
-
} from "./markup.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What `AtlasElement` puts on every root: one name, so a project gives every
|
|
5
|
+
* root a display in a single stylesheet rule, and so `element` can say when a
|
|
6
|
+
* root was rendered by hand.
|
|
7
|
+
*/
|
|
8
|
+
export const ROOT_ATTRIBUTE = "data-atlas-root";
|
|
13
9
|
|
|
14
10
|
/** What is left to undo when the element leaves; usually nothing. */
|
|
15
11
|
// biome-ignore lint/suspicious/noConfusingVoidType: that is the distinction.
|
|
16
12
|
type Undo = void | (() => void);
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
? {
|
|
24
|
-
all<T extends Element = HTMLElement>(): T[];
|
|
25
|
-
one<T extends Element = HTMLElement>(): T | null;
|
|
26
|
-
require<T extends Element = HTMLElement>(): T;
|
|
27
|
-
}
|
|
28
|
-
: {
|
|
29
|
-
all<T extends Element = HTMLElement>(): Marked<T, F>[];
|
|
30
|
-
one<T extends Element = HTMLElement>(): Marked<T, F> | null;
|
|
31
|
-
require<T extends Element = HTMLElement>(): Marked<T, F>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** Every role of an element, bound to one instance. */
|
|
35
|
-
export type ElementRoles<R extends ComponentRoles> = {
|
|
36
|
-
readonly [K in keyof R]: Lookups<R[K]>;
|
|
37
|
-
};
|
|
14
|
+
export interface ElementContext {
|
|
15
|
+
readonly host: HTMLElement;
|
|
16
|
+
/** Aborted when the element leaves, taking its listeners with it. */
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
}
|
|
38
19
|
|
|
39
20
|
/**
|
|
40
|
-
* What an element does while it is on the page. `signal`
|
|
41
|
-
*
|
|
42
|
-
* to undo is the returned function.
|
|
21
|
+
* What an element does while it is on the page. Anything `signal` does not
|
|
22
|
+
* already undo is the returned function.
|
|
43
23
|
*/
|
|
44
|
-
export type Connect
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
export type Connect = (context: ElementContext) => Undo;
|
|
25
|
+
|
|
26
|
+
/** What a template renders the element from, with `AtlasElement`. */
|
|
27
|
+
export interface ElementTag<Tag extends string = string> {
|
|
28
|
+
readonly tag: Tag;
|
|
29
|
+
}
|
|
49
30
|
|
|
50
31
|
/**
|
|
51
|
-
* A custom element
|
|
32
|
+
* A custom element's behaviour, registered under `tag`.
|
|
52
33
|
*
|
|
53
34
|
* ```ts
|
|
54
|
-
* // faq.ts — imported by the template, and by the page's script
|
|
55
|
-
* export const faq = element("go-faq", {
|
|
56
|
-
* (host,
|
|
35
|
+
* // faq.ts — imported by the template for the tag, and by the page's script
|
|
36
|
+
* export const faq = element("go-faq", ({ host, signal }) => {
|
|
37
|
+
* const input = ref<HTMLInputElement>(host, "[data-faq-search]");
|
|
38
|
+
* …
|
|
39
|
+
* });
|
|
57
40
|
* ```
|
|
58
41
|
* ```astro
|
|
59
42
|
* <script src="./faq.ts"></script>
|
|
43
|
+
* <AtlasElement of={faq}><input data-faq-search type="search"></AtlasElement>
|
|
60
44
|
* ```
|
|
61
45
|
*
|
|
62
46
|
* Registers itself when loaded in a browser and does nothing in Node, so the
|
|
63
|
-
* template imports the same file
|
|
47
|
+
* template imports the same file.
|
|
64
48
|
*
|
|
65
49
|
* - **An element can enter the page more than once.** Moving it runs the undo
|
|
66
50
|
* and `connect` again; Astro's `ClientRouter` does so on every navigation.
|
|
67
51
|
* State that must survive belongs in a `WeakMap` keyed by `host`.
|
|
68
52
|
* - **The page's script must stay a bundled module** (a plain `<script>`), so
|
|
69
53
|
* the element has its children when it upgrades.
|
|
70
|
-
* - **A `connect` that throws leaves that one element
|
|
54
|
+
* - **Failures show in dev.** A `connect` that throws leaves that one element
|
|
55
|
+
* inert, and it, or any uncaught error on the page, lands in the dev panel.
|
|
71
56
|
* - **A tag defined twice keeps the first definition** and reports the second.
|
|
72
57
|
*/
|
|
73
|
-
export function element<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
>(
|
|
78
|
-
tag: Tag & CheckedTagName<Tag>,
|
|
79
|
-
roles: R,
|
|
80
|
-
connect: Connect<R>
|
|
81
|
-
): Component<Tag, R> {
|
|
82
|
-
// Already checked by this function's own signature.
|
|
83
|
-
const contract = component(tag as never, roles) as Component<Tag, R>;
|
|
58
|
+
export function element<const Tag extends string>(
|
|
59
|
+
tag: Tag,
|
|
60
|
+
connect: Connect
|
|
61
|
+
): ElementTag<Tag> {
|
|
84
62
|
if (typeof customElements !== "undefined") {
|
|
85
|
-
|
|
63
|
+
watchDevErrors();
|
|
64
|
+
// Defining upgrades elements already on the page synchronously; a
|
|
65
|
+
// microtask later, `const x = element(…)` is assigned first.
|
|
66
|
+
queueMicrotask(() => define(tag, connect));
|
|
86
67
|
}
|
|
87
|
-
return
|
|
68
|
+
return { tag };
|
|
88
69
|
}
|
|
89
70
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
71
|
+
let watching = false;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Puts errors thrown outside `connect` — in a listener, after an `await` — in
|
|
75
|
+
* the dev panel too. Once per page load: the listeners outlive a body swap.
|
|
76
|
+
*/
|
|
77
|
+
function watchDevErrors(): void {
|
|
78
|
+
if (!import.meta.env.DEV || watching) return;
|
|
79
|
+
watching = true;
|
|
80
|
+
window.addEventListener("error", (event) => {
|
|
81
|
+
reportDevError("page", event.error ?? event.message);
|
|
82
|
+
});
|
|
83
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
84
|
+
reportDevError("page", event.reason);
|
|
85
|
+
});
|
|
95
86
|
}
|
|
96
87
|
|
|
97
|
-
function define
|
|
98
|
-
contract: Component<string, R>,
|
|
99
|
-
roles: R,
|
|
100
|
-
connect: Connect<R>
|
|
101
|
-
): void {
|
|
102
|
-
const { tag } = contract;
|
|
88
|
+
function define(tag: string, connect: Connect): void {
|
|
103
89
|
if (customElements.get(tag) !== undefined) {
|
|
104
90
|
reportDevError(
|
|
105
91
|
tag,
|
|
@@ -108,27 +94,6 @@ function define<R extends ComponentRoles>(
|
|
|
108
94
|
return;
|
|
109
95
|
}
|
|
110
96
|
|
|
111
|
-
const bind = (host: HTMLElement): ElementRoles<R> => {
|
|
112
|
-
const bound: Record<string, unknown> = {};
|
|
113
|
-
for (const [name, fields] of Object.entries(roles)) {
|
|
114
|
-
const role = (contract as unknown as Record<string, AnyRole>)[name];
|
|
115
|
-
if (role === undefined) continue;
|
|
116
|
-
bound[name] =
|
|
117
|
-
Object.keys(fields).length === 0
|
|
118
|
-
? {
|
|
119
|
-
all: () => role.all(host).map((each) => each.element),
|
|
120
|
-
one: () => role.one(host)?.element ?? null,
|
|
121
|
-
require: () => role.require(host).element,
|
|
122
|
-
}
|
|
123
|
-
: {
|
|
124
|
-
all: () => role.all(host),
|
|
125
|
-
one: () => role.one(host),
|
|
126
|
-
require: () => role.require(host),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return bound as ElementRoles<R>;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
97
|
customElements.define(
|
|
133
98
|
tag,
|
|
134
99
|
class extends HTMLElement {
|
|
@@ -146,11 +111,11 @@ function define<R extends ComponentRoles>(
|
|
|
146
111
|
!this.hasAttribute(ROOT_ATTRIBUTE)
|
|
147
112
|
) {
|
|
148
113
|
throw new Error(
|
|
149
|
-
`<${tag}> carries no ${ROOT_ATTRIBUTE} —
|
|
114
|
+
`<${tag}> carries no ${ROOT_ATTRIBUTE} — is it rendered with <AtlasElement of={…}>?`
|
|
150
115
|
);
|
|
151
116
|
}
|
|
152
117
|
this.#undo =
|
|
153
|
-
connect(
|
|
118
|
+
connect({ host: this, signal: ac.signal }) ?? undefined;
|
|
154
119
|
} catch (error) {
|
|
155
120
|
reportDevError(this.localName, error);
|
|
156
121
|
}
|
package/src/astro/ref.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the elements a script works on, by a selector the template wrote as
|
|
3
|
+
* a plain attribute — `[data-faq-search]` — so what the script asks for is what
|
|
4
|
+
* devtools shows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Names the root in an error, for a selector that matched nothing. */
|
|
8
|
+
const where = (root: ParentNode): string =>
|
|
9
|
+
"localName" in root && typeof root.localName === "string"
|
|
10
|
+
? `<${root.localName}>`
|
|
11
|
+
: "the document";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The first match under `root`, or a thrown error naming the selector. Inside
|
|
15
|
+
* `element`'s `connect`, that error lands in the dev panel.
|
|
16
|
+
*/
|
|
17
|
+
export function ref<T extends Element = HTMLElement>(
|
|
18
|
+
root: ParentNode,
|
|
19
|
+
selector: string
|
|
20
|
+
): T {
|
|
21
|
+
const found = root.querySelector<T>(selector);
|
|
22
|
+
if (found === null) {
|
|
23
|
+
throw new Error(`${where(root)} has nothing matching ${selector}`);
|
|
24
|
+
}
|
|
25
|
+
return found;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An element's `data-<name>` value, or a thrown error naming the element and
|
|
30
|
+
* the attribute. For an optional one, read `dataset` directly.
|
|
31
|
+
*/
|
|
32
|
+
export function data(element: Element, name: string): string {
|
|
33
|
+
const value = element.getAttribute(`data-${name}`);
|
|
34
|
+
if (value === null) {
|
|
35
|
+
throw new Error(`<${element.localName}> has no data-${name}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every match under `root`. None is an answer here, not an error. */
|
|
41
|
+
export function refs<T extends Element = HTMLElement>(
|
|
42
|
+
root: ParentNode,
|
|
43
|
+
selector: string
|
|
44
|
+
): T[] {
|
|
45
|
+
return [...root.querySelectorAll<T>(selector)];
|
|
46
|
+
}
|