@escape-game-over/atlas 0.1.18 → 0.1.20
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 +33 -3
- package/package.json +2 -1
- package/src/astro/client.ts +23 -0
- package/src/astro/dom.ts +8 -26
- package/src/astro/element.ts +43 -147
- package/src/astro/filters-view.ts +46 -147
- package/src/astro/filters.ts +88 -248
- package/src/astro/markup.ts +217 -148
package/docs/client-scripts.md
CHANGED
|
@@ -54,6 +54,13 @@ from the package root is the same question at build time: false, and the banner
|
|
|
54
54
|
never reaches the HTML. Gate the render on that and keep the runtime check for
|
|
55
55
|
the banner that is rendered. See NOT-BUILT.md on where the halves divide.
|
|
56
56
|
|
|
57
|
+
**A script imports one path: `@escape-game-over/atlas/client`**, which re-exports
|
|
58
|
+
every module above. It is browser-only — `element.ts` evaluates
|
|
59
|
+
`class AtlasElement extends HTMLElement` as it loads, which throws in Node — so
|
|
60
|
+
frontmatter and `astro.config.ts` cannot use it. A component's contract is the
|
|
61
|
+
one thing both halves need, and it stays on `./astro/markup`, which touches
|
|
62
|
+
nothing.
|
|
63
|
+
|
|
57
64
|
## Lifetimes are the recurring bug
|
|
58
65
|
|
|
59
66
|
Every module here that binds a listener hands back the undo, and the undo is
|
|
@@ -408,9 +415,24 @@ const input = search.require<HTMLInputElement>(this).element;
|
|
|
408
415
|
a list that quietly loses a row is the failure this exists to prevent.
|
|
409
416
|
|
|
410
417
|
The kinds are data, not builders, for the reason `filters` gives:
|
|
411
|
-
`text`, `number`, `list`, `choice` and `flag`,
|
|
412
|
-
`optional`. A `flag` is off by being absent, never `"false"`, so a
|
|
413
|
-
select it with a bare attribute.
|
|
418
|
+
`text`, `number`, `list`, `template`, `choice` and `flag`, all but `flag`
|
|
419
|
+
optionally `optional`. A `flag` is off by being absent, never `"false"`, so a
|
|
420
|
+
stylesheet can select it with a bare attribute.
|
|
421
|
+
|
|
422
|
+
A `template` is the one that keeps a sentence out of the script. It is written
|
|
423
|
+
as the translated string with its placeholders left in, and read back as the
|
|
424
|
+
function that fills them:
|
|
425
|
+
|
|
426
|
+
```astro
|
|
427
|
+
<p {...faq.line.attrs({ sentence: t("faq.matched", { count: "{count}" }) })}></p>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
line.element.textContent = line.values.sentence({ count: String(hits.size) });
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
So no script spells `"{count}"`, and a sentence that never says one of its own
|
|
435
|
+
placeholders fails the build rather than shipping a brace to a reader.
|
|
414
436
|
|
|
415
437
|
`write` sets fields back onto an element, for state a stylesheet reads — a
|
|
416
438
|
dropdown that is blocked — so the script uses the names the template did.
|
|
@@ -470,6 +492,14 @@ faq.define(Faq);
|
|
|
470
492
|
before they are read, so a broken nested copy cannot fail the outer lookup.
|
|
471
493
|
- **The tag is written once.** `faq.tag` is what the template renders and
|
|
472
494
|
`faq.define` registers the class under it.
|
|
495
|
+
- **The names are checked in the editor.** A tag without a hyphen, or a role
|
|
496
|
+
that is not camelCase, is a compile error where it is written — the rules are
|
|
497
|
+
types, character by character, as in `i18n/placeholders.ts`. Nothing is
|
|
498
|
+
validated at run time, because a name that reached the browser malformed would
|
|
499
|
+
already have been refused there: by `setAttribute`, by `querySelectorAll`, or
|
|
500
|
+
by `customElements.define`.
|
|
501
|
+
- **Most roles carry nothing**, and say so: `search: marker` rather than
|
|
502
|
+
`search: {}`, which reads like options somebody forgot to fill in.
|
|
473
503
|
|
|
474
504
|
`tag` and `define` are the component's own keys, so no role may take them. A role
|
|
475
505
|
that lives outside every instance, such as a footer button that reopens a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./astro/images": "./src/astro/images.ts",
|
|
18
18
|
"./astro/background-video": "./src/astro/background-video.ts",
|
|
19
19
|
"./astro/carousel": "./src/astro/carousel.ts",
|
|
20
|
+
"./client": "./src/astro/client.ts",
|
|
20
21
|
"./astro/consent": "./src/astro/consent.ts",
|
|
21
22
|
"./astro/dev-log": "./src/astro/dev-log.ts",
|
|
22
23
|
"./astro/dom": "./src/astro/dom.ts",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything a client script needs, from one import.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { AtlasElement, filters, searchBox } from "@escape-game-over/atlas/client";
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* **Browser only.** `element.ts` evaluates `class AtlasElement extends
|
|
9
|
+
* HTMLElement` as this module loads, which throws in Node — so frontmatter and
|
|
10
|
+
* `astro.config.ts` must not import it. A component's contract is the one thing
|
|
11
|
+
* both halves need, and it stays on `astro/markup`, which touches nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export * from "./background-video.ts";
|
|
15
|
+
export * from "./carousel.ts";
|
|
16
|
+
export * from "./consent.ts";
|
|
17
|
+
export * from "./dev-log.ts";
|
|
18
|
+
export * from "./dom.ts";
|
|
19
|
+
export * from "./element.ts";
|
|
20
|
+
export * from "./filters.ts";
|
|
21
|
+
export * from "./filters-view.ts";
|
|
22
|
+
export * from "./markup.ts";
|
|
23
|
+
export * from "./youtube.ts";
|
package/src/astro/dom.ts
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scoped element lookups,
|
|
3
|
-
*
|
|
4
|
-
* `querySelector` returns `Element`, so anything that wants to set `hidden`,
|
|
5
|
-
* read `dataset` or call `focus` has to say `<HTMLElement>` at every call — long
|
|
6
|
-
* enough that the line wraps, on lookups that are otherwise trivial. And
|
|
7
|
-
* `querySelectorAll` returns a `NodeList`, which needs spreading before it will
|
|
8
|
-
* `map` or `entries`. Both are noise, and both hide the one thing worth reading:
|
|
9
|
-
* what is being looked for.
|
|
2
|
+
* Scoped element lookups, typed.
|
|
10
3
|
*
|
|
11
4
|
* ```ts
|
|
12
5
|
* const { one, all } = within(root);
|
|
13
|
-
* const track = one("[data-carousel-track]");
|
|
14
6
|
* const dots = all<HTMLButtonElement>("[data-carousel-dot]");
|
|
15
7
|
* ```
|
|
16
8
|
*
|
|
17
|
-
* The root is bound once,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* exactly once per script — to find the roots.
|
|
9
|
+
* The root is bound once, so a script cannot reach a second copy of itself
|
|
10
|
+
* elsewhere on the page. `document` belongs in one place per script: finding
|
|
11
|
+
* the roots.
|
|
21
12
|
*
|
|
22
13
|
* **A selector with a combinator still matches against the whole document.**
|
|
23
|
-
* `within(form).one("form p")` can match a `<p>`
|
|
24
|
-
* the
|
|
25
|
-
* attribute, a class — are unaffected, which is what these are for. Use
|
|
26
|
-
* `:scope` if a combinator is ever genuinely needed.
|
|
14
|
+
* `within(form).one("form p")` can match a `<p>` under a different form; only
|
|
15
|
+
* the final filter is scoped. Use `:scope` if a combinator is needed.
|
|
27
16
|
*/
|
|
28
17
|
|
|
29
|
-
/**
|
|
30
|
-
* Constrained to `Element` but defaulting to `HTMLElement`.
|
|
31
|
-
*
|
|
32
|
-
* The default is what almost every lookup wants — `hidden`, `dataset` and
|
|
33
|
-
* `focus` all live on `HTMLElement`, and having to name it at each call is the
|
|
34
|
-
* noise this exists to remove. The wider constraint is for the rest: inline
|
|
35
|
-
* `<svg>` and `<use>` are `SVGElement`, which is an `Element` and not an
|
|
36
|
-
* `HTMLElement`, so a narrower bound would refuse a perfectly ordinary lookup.
|
|
37
|
-
*/
|
|
38
18
|
export interface Within {
|
|
39
19
|
/** The first match inside the root, or `null`. */
|
|
40
20
|
one<T extends Element = HTMLElement>(selector: string): T | null;
|
|
@@ -42,6 +22,8 @@ export interface Within {
|
|
|
42
22
|
all<T extends Element = HTMLElement>(selector: string): T[];
|
|
43
23
|
}
|
|
44
24
|
|
|
25
|
+
// `HTMLElement` by default because `hidden`, `dataset` and `focus` live there;
|
|
26
|
+
// the bound stays `Element` so inline `<svg>` lookups are not refused.
|
|
45
27
|
export function within(root: ParentNode): Within {
|
|
46
28
|
return {
|
|
47
29
|
one: <T extends Element = HTMLElement>(selector: string) =>
|
package/src/astro/element.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { reportDevError } from "./dev-log.ts";
|
|
2
|
-
import { within } from "./dom.ts";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* A base for custom elements
|
|
6
|
-
*
|
|
7
|
-
* In `astro/` because it touches the DOM, which the core is type-checked
|
|
8
|
-
* without — this, `consent.ts`, `carousel.ts` and `dom.ts` are the folder
|
|
9
|
-
* allowed it.
|
|
4
|
+
* A base for custom elements: two lifetimes, and one abort signal.
|
|
10
5
|
*
|
|
11
6
|
* ```ts
|
|
12
7
|
* class Thing extends AtlasElement {
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* protected connect(): void { // every time it enters the document
|
|
17
|
-
* this.require("form").addEventListener("submit", this.#send, {
|
|
8
|
+
* protected setup(): void { … } // once, ever: what the element is
|
|
9
|
+
* protected connect(): void { // every connection: bind with signal
|
|
10
|
+
* this.querySelector("form")?.addEventListener("submit", send, {
|
|
18
11
|
* signal: this.signal,
|
|
19
12
|
* });
|
|
20
13
|
* }
|
|
@@ -22,75 +15,49 @@ import { within } from "./dom.ts";
|
|
|
22
15
|
* customElements.define("atlas-thing", Thing);
|
|
23
16
|
* ```
|
|
24
17
|
*
|
|
25
|
-
* **
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
18
|
+
* - **An element is constructed once and connected many times.** Moving it in
|
|
19
|
+
* the DOM runs `disconnectedCallback` then `connectedCallback` again, so
|
|
20
|
+
* state belongs in `setup` and listeners in `connect`. Anything registered
|
|
21
|
+
* with `signal` is dropped on disconnect and rebound on the next connect.
|
|
22
|
+
* - **Override `setup`, `connect` and `disconnect`, never the callbacks.**
|
|
23
|
+
* Overriding `connectedCallback` skips the controller and leaks every
|
|
24
|
+
* listener; that mistake throws in development, below.
|
|
25
|
+
* - **The defining script must stay a deferred module.** Astro emits
|
|
26
|
+
* `<script src>` as `type="module"`, so the element has its children when it
|
|
27
|
+
* is upgraded. `is:inline` runs it too early and every lookup finds nothing.
|
|
28
|
+
* - **`attributeChangedCallback` runs before `connectedCallback`**, where
|
|
29
|
+
* `signal` is not readable yet. Record what changed and act on it in
|
|
30
|
+
* `connect`.
|
|
31
|
+
* - **`disconnect` is not a destructor.** A move fires it and connects again a
|
|
32
|
+
* moment later, so it is connection teardown and nothing else.
|
|
39
33
|
*
|
|
40
|
-
*
|
|
41
|
-
* emits `<script>` and `<script src="./…">` as `type="module"`, which runs after
|
|
42
|
-
* the document is parsed, so an element has its children by the time it is
|
|
43
|
-
* upgraded. `is:inline` opts out and runs the script where it sits — usually
|
|
44
|
-
* above the element it wires — and every lookup then finds nothing. The
|
|
45
|
-
* protection comes from the bundling, not from custom elements.
|
|
46
|
-
*
|
|
47
|
-
* **`attributeChangedCallback` runs before `connectedCallback`** for attributes
|
|
48
|
-
* present in the initial markup, so `signal` is not available there and reading
|
|
49
|
-
* it throws. A subclass with `observedAttributes` should record what changed and
|
|
50
|
-
* act on it in `connect`.
|
|
51
|
-
*
|
|
52
|
-
* What is deliberately *not* modelled is destruction. `disconnect` fires on
|
|
53
|
-
* every move, so it is connection teardown and nothing else — it is not the
|
|
54
|
-
* place to flush state or release something genuinely scarce, because the
|
|
55
|
-
* element may well be back a microtask later.
|
|
34
|
+
* See docs/client-scripts.md.
|
|
56
35
|
*/
|
|
57
36
|
export abstract class AtlasElement extends HTMLElement {
|
|
58
37
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* an `AbortController` is single-use: a controller that outlived the first
|
|
63
|
-
* connection would come back already aborted, `addEventListener` with an
|
|
64
|
-
* aborted signal silently adds nothing, and the element would return looking
|
|
65
|
-
* perfectly normal and be inert forever.
|
|
38
|
+
* This connection's listeners, and only this connection's. Remade on every
|
|
39
|
+
* connect: an `AbortController` is single-use, and a reused one comes back
|
|
40
|
+
* already aborted, leaving the element inert but normal-looking.
|
|
66
41
|
*/
|
|
67
42
|
#ac?: AbortController;
|
|
68
43
|
|
|
69
|
-
/** Whether `setup` has run. See the two-lifetimes note above. */
|
|
70
44
|
#ready = false;
|
|
71
45
|
|
|
72
46
|
constructor() {
|
|
73
47
|
super();
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// method below is dropped from the bundle. An optional chain would be
|
|
77
|
-
// replaced as `import.meta.env` instead — an object literal, whose
|
|
78
|
-
// `.DEV` a minifier has to fold rather than simply delete.
|
|
48
|
+
// The bare expression Vite substitutes, so this collapses to
|
|
49
|
+
// `if (false)` and the method below leaves the bundle.
|
|
79
50
|
if (import.meta.env.DEV) this.#assertHooks();
|
|
80
51
|
}
|
|
81
52
|
|
|
82
53
|
/**
|
|
83
|
-
* Refuses a subclass that overrode the wrong lifecycle method.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* inherited one is not — so walking up to this class's prototype finds it.
|
|
87
|
-
* The loop rather than a single check, because an intermediate base class
|
|
88
|
-
* could be the one that got it wrong.
|
|
54
|
+
* Refuses a subclass that overrode the wrong lifecycle method. An override
|
|
55
|
+
* is an *own* property of the subclass prototype; the loop covers an
|
|
56
|
+
* intermediate base class that got it wrong.
|
|
89
57
|
*/
|
|
90
58
|
#assertHooks(): void {
|
|
91
|
-
// A tuple array
|
|
92
|
-
// back to `string
|
|
93
|
-
// where a typo would matter.
|
|
59
|
+
// A tuple array, not an object: `Object.entries` would widen the keys
|
|
60
|
+
// back to `string` and stop checking the pairing.
|
|
94
61
|
const wrong = [
|
|
95
62
|
["connectedCallback", "connect"],
|
|
96
63
|
["disconnectedCallback", "disconnect"],
|
|
@@ -106,11 +73,8 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
106
73
|
const error = new Error(
|
|
107
74
|
`override "${hook}", not "${callback}" — see AtlasElement`
|
|
108
75
|
);
|
|
109
|
-
// Reported before throwing: this runs during upgrade,
|
|
110
|
-
//
|
|
111
|
-
// throw would surface only as an uncaught error in the
|
|
112
|
-
// console — invisible, for the one mistake this whole
|
|
113
|
-
// apparatus exists to catch.
|
|
76
|
+
// Reported before throwing: this runs during upgrade, where
|
|
77
|
+
// a throw would only surface as an uncaught console error.
|
|
114
78
|
reportDevError(this.constructor.name, error);
|
|
115
79
|
throw error;
|
|
116
80
|
}
|
|
@@ -120,10 +84,7 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
120
84
|
|
|
121
85
|
/**
|
|
122
86
|
* Pass to `addEventListener`, observers, anything that should stop when the
|
|
123
|
-
* element leaves the document.
|
|
124
|
-
*
|
|
125
|
-
* Throws when read outside a connection, which is a programming error
|
|
126
|
-
* rather than a state to handle: there is nothing sensible to return.
|
|
87
|
+
* element leaves the document. Throws outside a connection.
|
|
127
88
|
*/
|
|
128
89
|
protected get signal(): AbortSignal {
|
|
129
90
|
if (!this.#ac) {
|
|
@@ -135,25 +96,17 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
135
96
|
}
|
|
136
97
|
|
|
137
98
|
connectedCallback(): void {
|
|
138
|
-
// Insurance
|
|
139
|
-
// live controller — but if it ever did, overwriting one would orphan
|
|
140
|
-
// every listener it owned, silently, which is the failure this class
|
|
141
|
-
// exists to make impossible.
|
|
99
|
+
// Insurance: overwriting a live controller would orphan its listeners.
|
|
142
100
|
this.#ac?.abort();
|
|
143
101
|
this.#ac = new AbortController();
|
|
144
102
|
|
|
145
|
-
// Caught
|
|
146
|
-
//
|
|
147
|
-
// so the blast radius is already one. That is what lets `require`
|
|
148
|
-
// throw instead of returning something every call site has to check.
|
|
103
|
+
// Caught so one broken element logs and sits inert rather than taking
|
|
104
|
+
// its siblings with it — which is what lets `require` throw.
|
|
149
105
|
try {
|
|
150
106
|
if (!this.#ready) {
|
|
151
107
|
this.setup();
|
|
152
|
-
// Only once it returned
|
|
153
|
-
//
|
|
154
|
-
// would run `connect` against state that was never initialized
|
|
155
|
-
// — a component that renders perfectly and does nothing, which
|
|
156
|
-
// is the failure this class exists to make impossible.
|
|
108
|
+
// Only once it returned, so a `setup` that threw is retried
|
|
109
|
+
// rather than leaving `connect` to run against nothing.
|
|
157
110
|
this.#ready = true;
|
|
158
111
|
}
|
|
159
112
|
this.connect();
|
|
@@ -163,18 +116,13 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
163
116
|
}
|
|
164
117
|
|
|
165
118
|
disconnectedCallback(): void {
|
|
166
|
-
// Idempotent, so `adoptedCallback` can delegate here
|
|
167
|
-
// subclass's teardown twice.
|
|
119
|
+
// Idempotent, so `adoptedCallback` can delegate here.
|
|
168
120
|
if (!this.#ac) return;
|
|
169
121
|
this.#ac.abort();
|
|
170
122
|
|
|
171
|
-
// Caught for the same reason as `connect`, and it matters more: a
|
|
172
|
-
// throw here escapes into whatever is swapping the DOM — a router, a
|
|
173
|
-
// view transition — rather than staying in the component.
|
|
174
123
|
try {
|
|
175
124
|
// Before `#ac` is cleared, so `this.signal` is readable and already
|
|
176
|
-
// aborted: an async continuation can check
|
|
177
|
-
// bail rather than finishing against a detached element.
|
|
125
|
+
// aborted: an async continuation can check it and bail.
|
|
178
126
|
this.disconnect();
|
|
179
127
|
} catch (error) {
|
|
180
128
|
this.#report(error);
|
|
@@ -183,13 +131,7 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
183
131
|
this.#ac = undefined;
|
|
184
132
|
}
|
|
185
133
|
|
|
186
|
-
/**
|
|
187
|
-
* Fires when the element moves to another document.
|
|
188
|
-
*
|
|
189
|
-
* Delegates, because the controller belongs to the connection in the old
|
|
190
|
-
* document and nothing else would tear it down. Exotic — `adoptNode` and
|
|
191
|
-
* iframe work — and one line either way.
|
|
192
|
-
*/
|
|
134
|
+
/** Moving to another document ends the old document's connection. */
|
|
193
135
|
adoptedCallback(): void {
|
|
194
136
|
this.disconnectedCallback();
|
|
195
137
|
}
|
|
@@ -202,58 +144,12 @@ export abstract class AtlasElement extends HTMLElement {
|
|
|
202
144
|
console.error(`${this.localName}:`, error);
|
|
203
145
|
}
|
|
204
146
|
|
|
205
|
-
/**
|
|
206
|
-
* Runs once per element, before its first `connect`.
|
|
207
|
-
*
|
|
208
|
-
* Where state belongs. A move re-runs `connect` but never this, so anything
|
|
209
|
-
* initialised here survives one — which is the difference between a reader
|
|
210
|
-
* coming back to the slide they left and coming back to the first one.
|
|
211
|
-
*/
|
|
147
|
+
/** Runs once per element, before its first `connect`. State belongs here. */
|
|
212
148
|
protected setup(): void {}
|
|
213
149
|
|
|
214
150
|
/** Runs on every connect, with `signal` and the children both available. */
|
|
215
151
|
protected abstract connect(): void;
|
|
216
152
|
|
|
217
|
-
/**
|
|
218
|
-
* Runs on every disconnect, once the signal has been aborted.
|
|
219
|
-
*
|
|
220
|
-
* `signal` is still readable here and reports `aborted` — which is what an
|
|
221
|
-
* async continuation should check before touching a now-detached element.
|
|
222
|
-
* What it is *not* is a destructor: a move fires this and then `connect`
|
|
223
|
-
* again a moment later, so it is connection teardown and nothing else.
|
|
224
|
-
* Flushing state or releasing something scarce does not belong here.
|
|
225
|
-
*
|
|
226
|
-
* Empty by default: anything registered with `signal` is already gone, and
|
|
227
|
-
* most elements have nothing else to undo.
|
|
228
|
-
*/
|
|
153
|
+
/** Runs on every disconnect, once the signal has been aborted. */
|
|
229
154
|
protected disconnect(): void {}
|
|
230
|
-
|
|
231
|
-
/** The first match inside this element, or `null`. */
|
|
232
|
-
protected one<T extends Element = HTMLElement>(selector: string): T | null {
|
|
233
|
-
return within(this).one<T>(selector);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Every match inside this element, as an array. */
|
|
237
|
-
protected all<T extends Element = HTMLElement>(selector: string): T[] {
|
|
238
|
-
return within(this).all<T>(selector);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* The first match, or a thrown error naming what was missing.
|
|
243
|
-
*
|
|
244
|
-
* Throws rather than returning `T | null`, because a nullable return puts an
|
|
245
|
-
* `?.` at every call site — and that optional chain swallows the failure
|
|
246
|
-
* just as thoroughly as the missing element did, which is the thing worth
|
|
247
|
-
* avoiding. `connectedCallback` catches it, so one element with broken
|
|
248
|
-
* markup logs and stops while every other element on the page is untouched.
|
|
249
|
-
*/
|
|
250
|
-
protected require<T extends Element = HTMLElement>(selector: string): T {
|
|
251
|
-
const found = this.one<T>(selector);
|
|
252
|
-
if (!found) {
|
|
253
|
-
throw new Error(
|
|
254
|
-
`nothing matched "${selector}" — is the script still a deferred module?`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return found;
|
|
258
|
-
}
|
|
259
155
|
}
|