id-dom 0.0.2 → 0.0.4
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/README.md +296 -0
- package/dist/index.cjs +176 -75
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +176 -75
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +1 -1
- package/package.json +1 -1
- package/Readme.md +0 -249
- /package/{LICENSE.md → LICENSE} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# id-dom
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/id-dom)
|
|
4
|
+
[](https://www.npmjs.com/package/id-dom)
|
|
5
|
+
[](https://github.com/iWhatty/id-dom)
|
|
6
|
+
[](https://github.com/iWhatty/id-dom/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Deterministic DOM element getters by ID — typed, tiny, modern.**
|
|
9
|
+
|
|
10
|
+
`id-dom` is a small utility for grabbing DOM references safely **by `id`**, with predictable behavior:
|
|
11
|
+
|
|
12
|
+
* **Typed getters** like `button('saveBtn')`, `input('nameInput')`, `svg('icon')`
|
|
13
|
+
* **Strict or optional** mode (`throw` vs `null`)
|
|
14
|
+
* **Short optional alias** via `.opt`
|
|
15
|
+
* **Scoped lookups** for `document`, `ShadowRoot`, `DocumentFragment`, or an `Element`
|
|
16
|
+
* **Centralized error handling** with `onError` and optional `warn`
|
|
17
|
+
* **Zero deps**
|
|
18
|
+
|
|
19
|
+
This is deliberately **not** a selector framework. It is a tiny, ID-first primitive for safe DOM wiring.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install id-dom
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import dom from 'id-dom'
|
|
35
|
+
|
|
36
|
+
const saveBtn = dom.button('saveBtn')
|
|
37
|
+
saveBtn.addEventListener('click', save)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Optional access never throws for missing or wrong-type elements:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const debug = dom.div.optional('debugPanel')
|
|
44
|
+
debug?.append('hello')
|
|
45
|
+
|
|
46
|
+
const maybeCanvas = dom.canvas.opt('game')
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Why ID-first?
|
|
52
|
+
|
|
53
|
+
Using `getElementById` is:
|
|
54
|
+
|
|
55
|
+
* fast
|
|
56
|
+
* unambiguous
|
|
57
|
+
* easy to reason about
|
|
58
|
+
|
|
59
|
+
And with typed getters, you immediately know whether you got a `HTMLButtonElement`, `HTMLInputElement`, `SVGSVGElement`, and so on.
|
|
60
|
+
|
|
61
|
+
When scoped roots do not support `getElementById`, `id-dom` falls back to `querySelector(#id)` and safely escapes edge-case IDs.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### Default export: `dom`
|
|
68
|
+
|
|
69
|
+
The default export is a scoped instance using `document` (when available) with **strict** behavior:
|
|
70
|
+
|
|
71
|
+
* missing element → **throws**
|
|
72
|
+
* wrong type or wrong tag → **throws**
|
|
73
|
+
* invalid input → **throws**
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import dom from 'id-dom'
|
|
77
|
+
|
|
78
|
+
const name = dom.input('nameInput')
|
|
79
|
+
const submit = dom.button('submitBtn')
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `createDom(root, config?)`
|
|
85
|
+
|
|
86
|
+
Create a scoped instance that searches within a specific root:
|
|
87
|
+
|
|
88
|
+
* `document` → uses `getElementById`
|
|
89
|
+
* `ShadowRoot`, `DocumentFragment`, or `Element` → uses `querySelector(#id)` fallback
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { createDom } from 'id-dom'
|
|
93
|
+
|
|
94
|
+
const d = createDom(document, { mode: 'null', warn: true })
|
|
95
|
+
const sidebar = d.div('sidebar')
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Config
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
type DomMode = 'throw' | 'null'
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
mode?: DomMode
|
|
105
|
+
warn?: boolean
|
|
106
|
+
onError?: (err: Error, ctx: any) => void
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### `byId(id, Type, config?)`
|
|
113
|
+
|
|
114
|
+
Generic typed lookup:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { byId } from 'id-dom'
|
|
118
|
+
|
|
119
|
+
const btn = byId('saveBtn', HTMLButtonElement)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Optional variants:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const maybeBtn = byId.optional('saveBtn', HTMLButtonElement)
|
|
126
|
+
const maybeBtn2 = byId.opt('saveBtn', HTMLButtonElement)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Behavior
|
|
130
|
+
|
|
131
|
+
* valid match → returns the element
|
|
132
|
+
* missing element → throws or returns `null`
|
|
133
|
+
* wrong type → throws or returns `null`
|
|
134
|
+
* invalid `id` → throws or returns `null`
|
|
135
|
+
* invalid `Type` → throws or returns `null`
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `tag(id, tagName, config?)`
|
|
140
|
+
|
|
141
|
+
Tag-based validation when constructor checks are not the right fit:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { tag } from 'id-dom'
|
|
145
|
+
|
|
146
|
+
const main = tag('appMain', 'main')
|
|
147
|
+
const icon = tag('icon', 'svg', { root: container })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Optional variants:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const maybeMain = tag.optional('appMain', 'main')
|
|
154
|
+
const maybeMain2 = tag.opt('appMain', 'main')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Behavior
|
|
158
|
+
|
|
159
|
+
* valid tag match → returns the element
|
|
160
|
+
* missing element → throws or returns `null`
|
|
161
|
+
* wrong tag → throws or returns `null`
|
|
162
|
+
* invalid `id` → throws or returns `null`
|
|
163
|
+
* invalid `tagName` → throws or returns `null`
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Built-in Getters
|
|
168
|
+
|
|
169
|
+
### Typed getters
|
|
170
|
+
|
|
171
|
+
Available on `dom` and on any `createDom()` instance:
|
|
172
|
+
|
|
173
|
+
* `el(id)` → `HTMLElement`
|
|
174
|
+
* `input(id)` → `HTMLInputElement`
|
|
175
|
+
* `button(id)` → `HTMLButtonElement`
|
|
176
|
+
* `textarea(id)` → `HTMLTextAreaElement`
|
|
177
|
+
* `select(id)` → `HTMLSelectElement`
|
|
178
|
+
* `form(id)` → `HTMLFormElement`
|
|
179
|
+
* `div(id)` → `HTMLDivElement`
|
|
180
|
+
* `span(id)` → `HTMLSpanElement`
|
|
181
|
+
* `label(id)` → `HTMLLabelElement`
|
|
182
|
+
* `canvas(id)` → `HTMLCanvasElement`
|
|
183
|
+
* `template(id)` → `HTMLTemplateElement`
|
|
184
|
+
* `svg(id)` → `SVGSVGElement`
|
|
185
|
+
* `body(id)` → `HTMLBodyElement`
|
|
186
|
+
|
|
187
|
+
Each getter also has:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
dom.canvas.optional('game')
|
|
191
|
+
dom.canvas.opt('game')
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Common tag helpers
|
|
195
|
+
|
|
196
|
+
* `main(id)` → validates `<main>`
|
|
197
|
+
* `section(id)` → validates `<section>`
|
|
198
|
+
* `small(id)` → validates `<small>`
|
|
199
|
+
|
|
200
|
+
Each also supports `.optional` and `.opt`.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Error Handling
|
|
205
|
+
|
|
206
|
+
### Throwing mode
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import dom from 'id-dom'
|
|
210
|
+
|
|
211
|
+
dom.button('missing') // throws
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Null-returning mode
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
import { createDom } from 'id-dom'
|
|
218
|
+
|
|
219
|
+
const d = createDom(document, { mode: 'null' })
|
|
220
|
+
d.button('missing') // null
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Central reporting
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const d = createDom(document, {
|
|
227
|
+
mode: 'null',
|
|
228
|
+
onError: (err, ctx) => {
|
|
229
|
+
// sendToSentry({ err, ctx })
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Enable console warnings too:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
createDom(document, { mode: 'null', warn: true })
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Scoped Roots
|
|
243
|
+
|
|
244
|
+
### Shadow DOM
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { createDom } from 'id-dom'
|
|
248
|
+
|
|
249
|
+
const host = document.querySelector('#widget')
|
|
250
|
+
const shadow = host.attachShadow({ mode: 'open' })
|
|
251
|
+
shadow.innerHTML = `<button id="shadowBtn">Click</button>`
|
|
252
|
+
|
|
253
|
+
const d = createDom(shadow)
|
|
254
|
+
const btn = d.button('shadowBtn')
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Element root
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
const container = document.querySelector('#settings-panel')
|
|
261
|
+
const d = createDom(container)
|
|
262
|
+
const input = d.input('emailInput')
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### SVG in scoped roots
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
const container = document.querySelector('#icons')
|
|
269
|
+
const d = createDom(container)
|
|
270
|
+
const icon = d.svg('logoMark')
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Notes
|
|
276
|
+
|
|
277
|
+
* `el(id)` is specifically for `HTMLElement`, not every possible DOM `Element`.
|
|
278
|
+
* `body(id)` looks up a `<body>` **by ID**. This library stays ID-first on purpose.
|
|
279
|
+
* `tag()` can validate non-HTML tags too, such as `svg`, when used against supported scoped roots.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Browser Support
|
|
284
|
+
|
|
285
|
+
Modern browsers supporting:
|
|
286
|
+
|
|
287
|
+
* `getElementById`
|
|
288
|
+
* `querySelector`
|
|
289
|
+
|
|
290
|
+
`CSS.escape` is used when available. A safe internal fallback is included for environments such as some jsdom builds where it may be missing.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
See `LICENSE`.
|
package/dist/index.cjs
CHANGED
|
@@ -27,10 +27,22 @@ const DEFAULT_ROOT = typeof document !== "undefined" && document ? document : (
|
|
|
27
27
|
/** @type {any} */
|
|
28
28
|
null
|
|
29
29
|
);
|
|
30
|
+
const REASON = (
|
|
31
|
+
/** @type {const} */
|
|
32
|
+
{
|
|
33
|
+
INVALID_ID: "invalid-id",
|
|
34
|
+
INVALID_TYPE: "invalid-type",
|
|
35
|
+
INVALID_TAG: "invalid-tag",
|
|
36
|
+
MISSING: "missing",
|
|
37
|
+
WRONG_TYPE: "wrong-type",
|
|
38
|
+
WRONG_TAG: "wrong-tag"
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
const SAFE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
42
|
+
const NEEDS_START_ESCAPE_RE = /^(?:\d|-\d)/;
|
|
30
43
|
function normalizeConfig(cfg) {
|
|
31
44
|
return {
|
|
32
45
|
mode: cfg?.mode ?? "throw",
|
|
33
|
-
// default strict
|
|
34
46
|
warn: cfg?.warn ?? false,
|
|
35
47
|
onError: typeof cfg?.onError === "function" ? cfg.onError : null,
|
|
36
48
|
root: cfg?.root ?? DEFAULT_ROOT
|
|
@@ -42,8 +54,6 @@ function hasGetElementById(v) {
|
|
|
42
54
|
function hasQuerySelector(v) {
|
|
43
55
|
return !!v && typeof v === "object" && typeof v.querySelector === "function";
|
|
44
56
|
}
|
|
45
|
-
const SAFE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
46
|
-
const NEEDS_START_ESCAPE_RE = /^(?:\d|-\d)/;
|
|
47
57
|
function cssEscape(id) {
|
|
48
58
|
const s = String(id);
|
|
49
59
|
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
@@ -59,10 +69,13 @@ function cssEscape(id) {
|
|
|
59
69
|
cp >= 97 && cp <= 122 || // a-z
|
|
60
70
|
cp === 95 || // _
|
|
61
71
|
cp === 45;
|
|
62
|
-
const
|
|
72
|
+
const next = s.codePointAt(i + 1);
|
|
73
|
+
const startsWithDigit = cp >= 48 && cp <= 57;
|
|
74
|
+
const startsWithDashDigit = cp === 45 && s.length > 1 && next >= 48 && next <= 57;
|
|
75
|
+
const needsStartEscape = i === 0 && (startsWithDigit || startsWithDashDigit);
|
|
63
76
|
if (!needsStartEscape && (isAsciiSafe || cp >= 160)) {
|
|
64
77
|
out += ch;
|
|
65
|
-
} else if (
|
|
78
|
+
} else if (i === 0 && startsWithDashDigit) {
|
|
66
79
|
out += "\\-";
|
|
67
80
|
} else {
|
|
68
81
|
out += `\\${cp.toString(16).toUpperCase()} `;
|
|
@@ -71,16 +84,32 @@ function cssEscape(id) {
|
|
|
71
84
|
}
|
|
72
85
|
return out;
|
|
73
86
|
}
|
|
87
|
+
function isElementNode(v) {
|
|
88
|
+
if (!v || typeof v !== "object") return false;
|
|
89
|
+
if (typeof Element !== "undefined") return v instanceof Element;
|
|
90
|
+
return (
|
|
91
|
+
/** @type {any} */
|
|
92
|
+
v.nodeType === 1
|
|
93
|
+
);
|
|
94
|
+
}
|
|
74
95
|
function getById(root, id) {
|
|
75
96
|
if (!root) return null;
|
|
76
97
|
if (hasGetElementById(root)) return root.getElementById(id);
|
|
77
98
|
if (hasQuerySelector(root)) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
return el instanceof HTMLElement ? el : null;
|
|
99
|
+
const el = root.querySelector(`#${cssEscape(id)}`);
|
|
100
|
+
return isElementNode(el) ? el : null;
|
|
81
101
|
}
|
|
82
102
|
return null;
|
|
83
103
|
}
|
|
104
|
+
function isValidId(v) {
|
|
105
|
+
return typeof v === "string" && v.length > 0;
|
|
106
|
+
}
|
|
107
|
+
function isValidTagName(v) {
|
|
108
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
109
|
+
}
|
|
110
|
+
function isConstructor(v) {
|
|
111
|
+
return typeof v === "function";
|
|
112
|
+
}
|
|
84
113
|
function fmtId(id) {
|
|
85
114
|
return id.startsWith("#") ? id : `#${id}`;
|
|
86
115
|
}
|
|
@@ -95,54 +124,114 @@ function handleLookupError(err, ctx, cfg) {
|
|
|
95
124
|
cfg.onError?.(err, ctx);
|
|
96
125
|
} catch {
|
|
97
126
|
}
|
|
98
|
-
if (cfg.warn) console.warn(err);
|
|
127
|
+
if (cfg.warn) console.warn(err, ctx);
|
|
99
128
|
if (cfg.mode === "throw") throw err;
|
|
100
129
|
return null;
|
|
101
130
|
}
|
|
102
|
-
function
|
|
131
|
+
function createCtx(id, root, reason, extra) {
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
root,
|
|
135
|
+
reason,
|
|
136
|
+
...extra || {}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function resolveLookup(config, spec) {
|
|
103
140
|
const cfg = normalizeConfig(config);
|
|
104
|
-
const
|
|
141
|
+
const inputFailure = spec.validateInput(cfg);
|
|
142
|
+
if (inputFailure) {
|
|
143
|
+
return handleLookupError(inputFailure.err, inputFailure.ctx, cfg);
|
|
144
|
+
}
|
|
145
|
+
const el = getById(cfg.root, spec.id);
|
|
105
146
|
if (!el) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
const failure = spec.onMissing(cfg);
|
|
148
|
+
return handleLookupError(failure.err, failure.ctx, cfg);
|
|
149
|
+
}
|
|
150
|
+
if (!spec.matches(el, cfg)) {
|
|
151
|
+
const failure = spec.onMismatch(el, cfg);
|
|
152
|
+
return handleLookupError(failure.err, failure.ctx, cfg);
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
/** @type {T} */
|
|
156
|
+
el
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
function byId(id, Type, config) {
|
|
160
|
+
return resolveLookup(config, {
|
|
161
|
+
id,
|
|
162
|
+
validateInput(cfg) {
|
|
163
|
+
if (!isValidId(id)) {
|
|
164
|
+
return {
|
|
165
|
+
err: new Error("id-dom: invalid id (expected non-empty string)"),
|
|
166
|
+
ctx: createCtx(String(id), cfg.root, REASON.INVALID_ID, { Type })
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!isConstructor(Type)) {
|
|
170
|
+
return {
|
|
171
|
+
err: new Error(`id-dom: invalid Type for ${fmtId(id)}`),
|
|
172
|
+
ctx: createCtx(id, cfg.root, REASON.INVALID_TYPE, { Type })
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
onMissing(cfg) {
|
|
178
|
+
return {
|
|
179
|
+
err: missingElError(id, Type.name),
|
|
180
|
+
ctx: createCtx(id, cfg.root, REASON.MISSING, { Type })
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
matches(el) {
|
|
184
|
+
return el instanceof Type;
|
|
185
|
+
},
|
|
186
|
+
onMismatch(el, cfg) {
|
|
187
|
+
const got = el?.constructor?.name || typeof el;
|
|
188
|
+
return {
|
|
189
|
+
err: wrongTypeError(id, Type.name, got),
|
|
190
|
+
ctx: createCtx(id, cfg.root, REASON.WRONG_TYPE, { Type, got })
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
121
194
|
}
|
|
122
195
|
byId.optional = function byIdOptional(id, Type, config) {
|
|
123
196
|
return byId(id, Type, { ...config, mode: "null" });
|
|
124
197
|
};
|
|
125
198
|
byId.opt = byId.optional;
|
|
126
199
|
function tag(id, tagName, config) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
200
|
+
return resolveLookup(config, {
|
|
201
|
+
id,
|
|
202
|
+
validateInput(cfg) {
|
|
203
|
+
if (!isValidId(id)) {
|
|
204
|
+
return {
|
|
205
|
+
err: new Error("id-dom: invalid id (expected non-empty string)"),
|
|
206
|
+
ctx: createCtx(String(id), cfg.root, REASON.INVALID_ID, { tagName })
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (!isValidTagName(tagName)) {
|
|
210
|
+
return {
|
|
211
|
+
err: new Error(`id-dom: invalid tagName for ${fmtId(id)}`),
|
|
212
|
+
ctx: createCtx(id, cfg.root, REASON.INVALID_TAG, { tagName })
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
},
|
|
217
|
+
onMissing(cfg) {
|
|
218
|
+
return {
|
|
219
|
+
err: missingElError(id, `<${tagName}>`),
|
|
220
|
+
ctx: createCtx(id, cfg.root, REASON.MISSING, { tagName })
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
matches(el) {
|
|
224
|
+
return String(el.tagName || "").toUpperCase() === String(tagName).toUpperCase();
|
|
225
|
+
},
|
|
226
|
+
onMismatch(el, cfg) {
|
|
227
|
+
const expected = String(tagName).toUpperCase();
|
|
228
|
+
const got = String(el.tagName || "").toUpperCase();
|
|
229
|
+
return {
|
|
230
|
+
err: wrongTypeError(id, `<${expected.toLowerCase()}>`, `<${got.toLowerCase()}>`),
|
|
231
|
+
ctx: createCtx(id, cfg.root, REASON.WRONG_TAG, { tagName, got })
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
});
|
|
146
235
|
}
|
|
147
236
|
tag.optional = function tagOptional(id, tagName, config) {
|
|
148
237
|
return tag(id, tagName, { ...config, mode: "null" });
|
|
@@ -162,7 +251,8 @@ const TYPE_HELPERS = (
|
|
|
162
251
|
label: typeof HTMLLabelElement !== "undefined" ? HTMLLabelElement : null,
|
|
163
252
|
canvas: typeof HTMLCanvasElement !== "undefined" ? HTMLCanvasElement : null,
|
|
164
253
|
template: typeof HTMLTemplateElement !== "undefined" ? HTMLTemplateElement : null,
|
|
165
|
-
svg: typeof SVGSVGElement !== "undefined" ? SVGSVGElement : null
|
|
254
|
+
svg: typeof SVGSVGElement !== "undefined" ? SVGSVGElement : null,
|
|
255
|
+
body: typeof HTMLBodyElement !== "undefined" ? HTMLBodyElement : null
|
|
166
256
|
}
|
|
167
257
|
);
|
|
168
258
|
const TAG_HELPERS = (
|
|
@@ -173,42 +263,53 @@ const TAG_HELPERS = (
|
|
|
173
263
|
small: "small"
|
|
174
264
|
}
|
|
175
265
|
);
|
|
266
|
+
function attachOptional(fn, optionalFn) {
|
|
267
|
+
if (typeof fn !== "function") {
|
|
268
|
+
throw new TypeError("id-dom: attachOptional expected fn to be a function");
|
|
269
|
+
}
|
|
270
|
+
if (typeof optionalFn !== "function") {
|
|
271
|
+
throw new TypeError("id-dom: attachOptional expected optionalFn to be a function");
|
|
272
|
+
}
|
|
273
|
+
fn.optional = optionalFn;
|
|
274
|
+
fn.opt = optionalFn;
|
|
275
|
+
return fn;
|
|
276
|
+
}
|
|
277
|
+
function makeTypedHelper(Type, base, baseNull) {
|
|
278
|
+
if (!Type) {
|
|
279
|
+
throw new TypeError("id-dom: makeTypedHelper received an invalid Type");
|
|
280
|
+
}
|
|
281
|
+
return attachOptional(
|
|
282
|
+
(id) => byId(id, Type, base),
|
|
283
|
+
(id) => byId(id, Type, baseNull)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function makeTagHelper(tagName, base, baseNull) {
|
|
287
|
+
if (!tagName) {
|
|
288
|
+
throw new TypeError("id-dom: makeTagHelper received an invalid tagName");
|
|
289
|
+
}
|
|
290
|
+
return attachOptional(
|
|
291
|
+
(id) => tag(id, tagName, base),
|
|
292
|
+
(id) => tag(id, tagName, baseNull)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
176
295
|
function createDom(root, config) {
|
|
177
296
|
const base = normalizeConfig({ ...config, root });
|
|
178
297
|
const baseNull = { ...base, mode: "null" };
|
|
179
|
-
const api = {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
298
|
+
const api = {};
|
|
299
|
+
api.byId = attachOptional(
|
|
300
|
+
(id, Type) => byId(id, Type, base),
|
|
301
|
+
(id, Type) => byId(id, Type, baseNull)
|
|
302
|
+
);
|
|
303
|
+
api.tag = attachOptional(
|
|
304
|
+
(id, name) => tag(id, name, base),
|
|
305
|
+
(id, name) => tag(id, name, baseNull)
|
|
306
|
+
);
|
|
184
307
|
for (const [name, Type] of Object.entries(TYPE_HELPERS)) {
|
|
185
308
|
if (!Type) continue;
|
|
186
|
-
api[name] = (
|
|
309
|
+
api[name] = makeTypedHelper(Type, base, baseNull);
|
|
187
310
|
}
|
|
188
311
|
for (const [name, tagName] of Object.entries(TAG_HELPERS)) {
|
|
189
|
-
api[name] = (
|
|
190
|
-
}
|
|
191
|
-
api.byId.optional = (id, Type) => byId(id, Type, baseNull);
|
|
192
|
-
api.byId.opt = api.byId.optional;
|
|
193
|
-
api.tag.optional = (id, name) => tag(id, name, baseNull);
|
|
194
|
-
api.tag.opt = api.tag.optional;
|
|
195
|
-
for (const k of Object.keys(api)) {
|
|
196
|
-
const fn = api[k];
|
|
197
|
-
if (typeof fn !== "function") continue;
|
|
198
|
-
if (fn.optional) continue;
|
|
199
|
-
if (k in TAG_HELPERS) {
|
|
200
|
-
const tagName = TAG_HELPERS[k];
|
|
201
|
-
fn.optional = (id) => tag(id, tagName, baseNull);
|
|
202
|
-
fn.opt = fn.optional;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (k in TYPE_HELPERS) {
|
|
206
|
-
const Type = TYPE_HELPERS[k];
|
|
207
|
-
if (Type) {
|
|
208
|
-
fn.optional = (id) => byId(id, Type, baseNull);
|
|
209
|
-
fn.opt = fn.optional;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
312
|
+
api[name] = makeTagHelper(tagName, base, baseNull);
|
|
212
313
|
}
|
|
213
314
|
return api;
|
|
214
315
|
}
|