@bodil/dom 0.1.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.
Files changed (82) hide show
  1. package/LICENCE.md +288 -0
  2. package/dist/component.d.ts +147 -0
  3. package/dist/component.js +405 -0
  4. package/dist/component.js.map +1 -0
  5. package/dist/component.test.d.ts +1 -0
  6. package/dist/component.test.js +141 -0
  7. package/dist/component.test.js.map +1 -0
  8. package/dist/css.d.ts +14 -0
  9. package/dist/css.js +63 -0
  10. package/dist/css.js.map +1 -0
  11. package/dist/decorators/attribute.d.ts +26 -0
  12. package/dist/decorators/attribute.js +115 -0
  13. package/dist/decorators/attribute.js.map +1 -0
  14. package/dist/decorators/attribute.test.d.ts +1 -0
  15. package/dist/decorators/attribute.test.js +148 -0
  16. package/dist/decorators/attribute.test.js.map +1 -0
  17. package/dist/decorators/connect.d.ts +9 -0
  18. package/dist/decorators/connect.js +44 -0
  19. package/dist/decorators/connect.js.map +1 -0
  20. package/dist/decorators/connect.test.d.ts +1 -0
  21. package/dist/decorators/connect.test.js +196 -0
  22. package/dist/decorators/connect.test.js.map +1 -0
  23. package/dist/decorators/reactive.d.ts +7 -0
  24. package/dist/decorators/reactive.js +45 -0
  25. package/dist/decorators/reactive.js.map +1 -0
  26. package/dist/decorators/reactive.test.d.ts +1 -0
  27. package/dist/decorators/reactive.test.js +113 -0
  28. package/dist/decorators/reactive.test.js.map +1 -0
  29. package/dist/decorators/require.d.ts +3 -0
  30. package/dist/decorators/require.js +6 -0
  31. package/dist/decorators/require.js.map +1 -0
  32. package/dist/decorators/require.test.d.ts +1 -0
  33. package/dist/decorators/require.test.js +117 -0
  34. package/dist/decorators/require.test.js.map +1 -0
  35. package/dist/dom.d.ts +92 -0
  36. package/dist/dom.js +354 -0
  37. package/dist/dom.js.map +1 -0
  38. package/dist/emitter.d.ts +10 -0
  39. package/dist/emitter.js +17 -0
  40. package/dist/emitter.js.map +1 -0
  41. package/dist/event.d.ts +70 -0
  42. package/dist/event.js +14 -0
  43. package/dist/event.js.map +1 -0
  44. package/dist/event.test.d.ts +1 -0
  45. package/dist/event.test.js +20 -0
  46. package/dist/event.test.js.map +1 -0
  47. package/dist/geometry.d.ts +48 -0
  48. package/dist/geometry.js +117 -0
  49. package/dist/geometry.js.map +1 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.js +11 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/scheduler.d.ts +2 -0
  54. package/dist/scheduler.js +54 -0
  55. package/dist/scheduler.js.map +1 -0
  56. package/dist/slot.d.ts +31 -0
  57. package/dist/slot.js +67 -0
  58. package/dist/slot.js.map +1 -0
  59. package/dist/test-global.d.ts +0 -0
  60. package/dist/test-global.js +5 -0
  61. package/dist/test-global.js.map +1 -0
  62. package/package.json +116 -0
  63. package/src/component.test.ts +90 -0
  64. package/src/component.ts +607 -0
  65. package/src/css.ts +69 -0
  66. package/src/decorators/attribute.test.ts +77 -0
  67. package/src/decorators/attribute.ts +197 -0
  68. package/src/decorators/connect.test.ts +119 -0
  69. package/src/decorators/connect.ts +85 -0
  70. package/src/decorators/reactive.test.ts +45 -0
  71. package/src/decorators/reactive.ts +80 -0
  72. package/src/decorators/require.test.ts +57 -0
  73. package/src/decorators/require.ts +11 -0
  74. package/src/dom.ts +456 -0
  75. package/src/emitter.ts +32 -0
  76. package/src/event.test.ts +22 -0
  77. package/src/event.ts +74 -0
  78. package/src/geometry.ts +147 -0
  79. package/src/index.ts +12 -0
  80. package/src/scheduler.ts +58 -0
  81. package/src/slot.ts +95 -0
  82. package/src/test-global.ts +5 -0
@@ -0,0 +1,607 @@
1
+ import { assert, isIterable, isNullish, present } from "@bodil/core/assert";
2
+ import { DisposableContext, toDisposable, type Disposifiable } from "@bodil/core/disposable";
3
+ import { Signal } from "@bodil/signal";
4
+ import {
5
+ adoptStyles,
6
+ getCompatibleStyle,
7
+ nothing,
8
+ render,
9
+ unsafeCSS,
10
+ type CSSResult,
11
+ type CSSResultOrNative,
12
+ type ReactiveController,
13
+ type ReactiveControllerHost,
14
+ type RenderOptions,
15
+ type RootPart,
16
+ } from "lit";
17
+ import type { Constructor } from "type-fest";
18
+
19
+ import {
20
+ attributeConfig,
21
+ detectAttributeType,
22
+ fromAttribute,
23
+ toAttribute,
24
+ type AttributeConfig,
25
+ } from "./decorators/attribute";
26
+ import { connectedJobs } from "./decorators/connect";
27
+ import { reactiveFields, signalForObject } from "./decorators/reactive";
28
+ import { requiredProperties } from "./decorators/require";
29
+ import { childElements } from "./dom";
30
+ import { EmitterElement } from "./emitter";
31
+ import { eventListener } from "./event";
32
+ import { scheduler } from "./scheduler";
33
+ import {
34
+ findSlot,
35
+ getOrSetQuery,
36
+ getOrSetSignal,
37
+ SlotChangeController,
38
+ type QuerySlotOptions,
39
+ } from "./slot";
40
+
41
+ export type { ReactiveController, ReactiveControllerHost } from "lit";
42
+ export { attribute, type AttributeOptions } from "./decorators/attribute";
43
+ export {
44
+ connect,
45
+ connectEffect,
46
+ type ConnectFunction,
47
+ type ConnectFunctionReturnValue,
48
+ } from "./decorators/connect";
49
+ export { reactive } from "./decorators/reactive";
50
+ export { require } from "./decorators/require";
51
+ export type { QuerySlotOptions } from "./slot";
52
+ export type { CustomEventTypes } from "./emitter";
53
+
54
+ (Symbol as any).metadata ??= Symbol.for("Symbol.metadata");
55
+
56
+ export type UpdateConfig = {
57
+ viewTransition?: boolean;
58
+ };
59
+
60
+ function listsEqual<A>(as: Array<A>, bs: Array<A>): boolean {
61
+ return as.length === bs.length && as.every((a, index) => Object.is(a, bs[index]));
62
+ }
63
+
64
+ const finalised = Symbol("finalised");
65
+
66
+ export type Deps = Array<typeof HTMLElement>;
67
+
68
+ export type Declare<A extends keyof HTMLElementEventMap> = { [K in A]: never };
69
+
70
+ export type ExtendSuper<C extends Component, A extends keyof HTMLElementEventMap> = {
71
+ [K in keyof C["emits"] | A]: never;
72
+ };
73
+
74
+ export type Emits<C> = C extends Component ? keyof C["emits"] : never;
75
+
76
+ export type CSSStyleSpec = CSSResult | CSSStyleSheet | string;
77
+ export type CSSStyleSpecArray = Array<CSSStyleSpec | CSSStyleSpecArray>;
78
+ export type CSSStyleSpecDeclaration = CSSStyleSpec | CSSStyleSpecArray;
79
+
80
+ function processCSSStyleSpec(spec: CSSStyleSpec) {
81
+ return getCompatibleStyle(typeof spec === "string" ? unsafeCSS(spec) : spec);
82
+ }
83
+
84
+ export abstract class Component
85
+ extends EmitterElement
86
+ implements ReactiveControllerHost, Disposable
87
+ {
88
+ static deps: Deps = [];
89
+ static styles?: CSSStyleSpecDeclaration;
90
+ static shadowRootOptions: ShadowRootInit = { mode: "open" };
91
+ static initialisers = new Set<() => void>();
92
+
93
+ /** @ignore */
94
+ protected static attributeConfig = new Map<string, AttributeConfig>();
95
+ /** @ignore */
96
+ protected static elementStyles: Array<CSSResultOrNative> = [];
97
+ /** @ignore */
98
+ protected static requiredProperties = new Set<string | symbol>();
99
+ private static [finalised] = true;
100
+
101
+ renderOptions: RenderOptions = { host: this };
102
+ emits!: object;
103
+
104
+ readonly #controllers = new Set<ReactiveController>();
105
+ readonly #connectedContext = new DisposableContext();
106
+ #isUpdatePending = false;
107
+ #updateSignal?: Signal.Computed<void>;
108
+ readonly #updateSignalWatcher = new Signal.subtle.Watcher(() => this.requestUpdate());
109
+ #updateResult = Promise.withResolvers<boolean>();
110
+ readonly #stabilised = Promise.withResolvers<void>();
111
+ #rootPart?: RootPart;
112
+ #firstUpdate = false;
113
+ #initialised = false;
114
+ #viewTransitionRequested = false;
115
+
116
+ readonly renderRoot: ShadowRoot | HTMLElement = this.createRenderRoot();
117
+
118
+ get isUpdatePending(): boolean {
119
+ return this.#isUpdatePending;
120
+ }
121
+ get updateComplete(): Promise<boolean> {
122
+ return this.#updateResult.promise;
123
+ }
124
+ get hasStabilised(): Promise<void> {
125
+ return this.#stabilised.promise;
126
+ }
127
+
128
+ #abortController = new AbortController();
129
+ get abortSignal(): AbortSignal {
130
+ return this.#abortController.signal;
131
+ }
132
+
133
+ static addInitialiser(init: () => void) {
134
+ if (!Object.hasOwn(this, "initialisers")) {
135
+ this.initialisers = new Set();
136
+ }
137
+ this.initialisers.add(init);
138
+ }
139
+
140
+ static get observedAttributes(): Array<string> {
141
+ this.finalise();
142
+ return this.attributeConfig.keys().toArray();
143
+ }
144
+
145
+ private static finalise() {
146
+ if (!Object.hasOwn(this, finalised)) {
147
+ this[finalised] = true;
148
+ const parent = Object.getPrototypeOf(this) as typeof Component;
149
+ parent.finalise();
150
+
151
+ this.attributeConfig = new Map([
152
+ ...parent.attributeConfig,
153
+ ...((this[Symbol.metadata]?.[attributeConfig] as Map<string, AttributeConfig>) ??
154
+ []),
155
+ ]);
156
+ this.requiredProperties = new Set([
157
+ ...parent.requiredProperties,
158
+ ...((this[Symbol.metadata]?.[requiredProperties] as Set<string | symbol>) ?? []),
159
+ ]);
160
+ this.initialisers = new Set([...parent.initialisers, ...this.initialisers]);
161
+ this.elementStyles = [...parent.elementStyles];
162
+ if (Object.hasOwn(this, "styles")) {
163
+ if (Array.isArray(this.styles)) {
164
+ const sheets = new Set(
165
+ (this.styles as Array<unknown>)
166
+ .flat(Infinity)
167
+ .reverse() as Array<CSSStyleSpec>,
168
+ );
169
+ for (const sheet of sheets) {
170
+ this.elementStyles.unshift(processCSSStyleSpec(sheet));
171
+ }
172
+ } else if (this.styles !== undefined) {
173
+ this.elementStyles.push(processCSSStyleSpec(this.styles));
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ constructor() {
180
+ super();
181
+
182
+ // set up reactive fields, because decorators can't do this themselves
183
+ for (const field of (this.constructor[Symbol.metadata]?.[reactiveFields] as Set<
184
+ string | symbol
185
+ >) ?? []) {
186
+ const sig = signalForObject(this, field, () =>
187
+ Signal(undefined, { equals: Object.is }),
188
+ ) as Signal.State<unknown>;
189
+ Object.defineProperty(this, field, {
190
+ get() {
191
+ return sig.get();
192
+ },
193
+ set(value) {
194
+ sig.set(value);
195
+ },
196
+ enumerable: true,
197
+ configurable: true,
198
+ });
199
+ }
200
+
201
+ // run initialisers
202
+ for (const init of (this.constructor as typeof Component).initialisers) {
203
+ init.call(this);
204
+ }
205
+
206
+ this.#initialised = false;
207
+ }
208
+
209
+ protected connectedCallback() {
210
+ this.#connectedContext.dispose();
211
+ this.#controllers.forEach((c) => c.hostConnected?.());
212
+ this.#rootPart?.setConnected(true);
213
+
214
+ for (const [key, config] of (
215
+ this.constructor as typeof Component
216
+ ).attributeConfig.entries()) {
217
+ if (config.reflect) {
218
+ this.syncAttribute(key, config);
219
+ }
220
+ }
221
+
222
+ for (const job of connectedJobs(this)) {
223
+ const result = job.call(this);
224
+ const items = isNullish(result)
225
+ ? Iterator.from<Disposifiable>([])
226
+ : isIterable(result)
227
+ ? Iterator.from(result)
228
+ : Iterator.from([result]);
229
+ items.filter((i) => i !== undefined).forEach(this.useWhileConnected.bind(this));
230
+ }
231
+
232
+ this.useWhileConnected(
233
+ Signal.effect(() => {
234
+ if (this.hasRequiredProperties().get()) {
235
+ if (!this.#initialised) {
236
+ this.#initialised = true;
237
+ this.firstInitialised();
238
+ }
239
+ this.initialised();
240
+ this.requestUpdate();
241
+ }
242
+ }),
243
+ );
244
+
245
+ this.requestUpdate();
246
+ }
247
+
248
+ protected disconnectedCallback() {
249
+ if (this.#updateSignal !== undefined) {
250
+ this.#updateSignalWatcher.unwatch(this.#updateSignal);
251
+ this.#updateSignal = undefined;
252
+ }
253
+ this.#isUpdatePending = false;
254
+ this.#abortController.abort(
255
+ new DOMException(`${this.tagName} element disconnected`, "AbortError"),
256
+ );
257
+ this.#abortController = new AbortController();
258
+ this.#rootPart?.setConnected(false);
259
+ this.#controllers.forEach((c) => c.hostDisconnected?.());
260
+ this.#connectedContext.dispose();
261
+ }
262
+
263
+ protected attributeChangedCallback(
264
+ name: string,
265
+ old: string | null,
266
+ valueString: string | null,
267
+ ) {
268
+ if (old === valueString) {
269
+ return;
270
+ }
271
+ const attrConfig = (this.constructor as typeof Component).attributeConfig.get(name);
272
+ assert(attrConfig !== undefined);
273
+ const value = fromAttribute(
274
+ valueString,
275
+ present(
276
+ attrConfig.type,
277
+ `@attribute decorator for ${this.constructor.name}.${name} needs a type declaration, eg. @attribute({type: String})`,
278
+ ),
279
+ );
280
+ this[attrConfig.property as keyof this] = value as any;
281
+ }
282
+
283
+ private syncAttribute(name: string, config?: AttributeConfig) {
284
+ if (this.isConnected) {
285
+ config ??= (this.constructor as typeof Component).attributeConfig.get(name);
286
+ assert(config !== undefined);
287
+ if (config.reflect) {
288
+ const realValue = this[config.property as keyof this];
289
+ config.type ??= detectAttributeType(realValue);
290
+ const value = toAttribute(realValue, present(config.type));
291
+ const oldValue = this.getAttribute(name);
292
+ if (value !== oldValue) {
293
+ if (value === null) {
294
+ this.removeAttribute(name);
295
+ } else {
296
+ this.setAttribute(name, value);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ $signal<K extends keyof this & string, V extends this[K]>(prop: K): Signal<V> {
304
+ const _value = this[prop];
305
+ return signalForObject(this, prop, () => {
306
+ throw new TypeError(`Object has no reactive property ${JSON.stringify(prop)}`);
307
+ }) as Signal<V>;
308
+ }
309
+
310
+ addController(controller: ReactiveController) {
311
+ this.#controllers.add(controller);
312
+ if (this.renderRoot !== undefined && this.isConnected) {
313
+ controller.hostConnected?.();
314
+ }
315
+ }
316
+
317
+ removeController(controller: ReactiveController) {
318
+ this.#controllers.delete(controller);
319
+ }
320
+
321
+ protected createRenderRoot(): HTMLElement | ShadowRoot {
322
+ const renderRoot =
323
+ this.shadowRoot ??
324
+ this.attachShadow((this.constructor as typeof Component).shadowRootOptions);
325
+ adoptStyles(renderRoot, [...(this.constructor as typeof Component).elementStyles]);
326
+ this.renderOptions.renderBefore ??= renderRoot.firstChild;
327
+ return renderRoot;
328
+ }
329
+
330
+ requestUpdate(opts?: UpdateConfig) {
331
+ if (opts?.viewTransition === true) {
332
+ this.#viewTransitionRequested = true;
333
+ }
334
+ if (!this.isUpdatePending && this.isConnected && this.#initialised) {
335
+ this.#isUpdatePending = true;
336
+ this.#updateResult = Promise.withResolvers();
337
+ scheduler(this);
338
+ }
339
+ }
340
+
341
+ #hasRequiredProperties?: Signal.Computed<boolean>;
342
+ hasRequiredProperties(): Signal.Computed<boolean> {
343
+ if (this.#hasRequiredProperties === undefined) {
344
+ const requiredProperties = (this.constructor as typeof Component).requiredProperties;
345
+ this.#hasRequiredProperties = Signal.computed(
346
+ () => {
347
+ return requiredProperties.size === 0
348
+ ? true
349
+ : requiredProperties
350
+ .keys()
351
+ .every((key) => (this as any)[key] !== undefined);
352
+ },
353
+ // every signal update is relevant, even if the signal value doesn't
354
+ // change, because the required values probably did.
355
+ { equals: () => false },
356
+ );
357
+ }
358
+ return this.#hasRequiredProperties;
359
+ }
360
+
361
+ protected async performUpdate() {
362
+ let didFirstUpdate = false;
363
+ const runUpdate = () => {
364
+ this.#controllers.forEach((c) => c.hostUpdate?.());
365
+ this.update();
366
+ this.#controllers.forEach((c) => c.hostUpdated?.());
367
+ if (!this.#firstUpdate) {
368
+ this.#firstUpdate = true;
369
+ this.firstUpdated();
370
+ didFirstUpdate = true;
371
+ }
372
+ this.updated();
373
+ };
374
+
375
+ if (!this.isConnected || !this.isUpdatePending) {
376
+ return;
377
+ }
378
+ try {
379
+ if (
380
+ this.#viewTransitionRequested &&
381
+ typeof document.startViewTransition === "function"
382
+ ) {
383
+ this.#viewTransitionRequested = false;
384
+ await document.startViewTransition(runUpdate).finished;
385
+ } else {
386
+ runUpdate();
387
+ }
388
+ this.#updateResult.resolve(true);
389
+ this.#isUpdatePending = false;
390
+ if (didFirstUpdate) {
391
+ await this.stabilise();
392
+ this.stabilised();
393
+ this.#stabilised.resolve();
394
+ }
395
+ } catch (e) {
396
+ this.#updateResult.reject(e);
397
+ this.#isUpdatePending = false;
398
+ if (didFirstUpdate) {
399
+ this.#stabilised.reject(e);
400
+ }
401
+ }
402
+ }
403
+
404
+ protected update() {
405
+ if (this.#updateSignal !== undefined) {
406
+ this.#updateSignalWatcher.unwatch(this.#updateSignal);
407
+ }
408
+ this.#updateSignal = Signal.computed(() => {
409
+ this.renderOptions.isConnected = this.isConnected;
410
+ this.#rootPart = render(this.render(), this.renderRoot, this.renderOptions);
411
+ });
412
+ this.#updateSignalWatcher.watch(this.#updateSignal);
413
+ this.#updateSignal.get();
414
+ }
415
+
416
+ protected findChildComponents(
417
+ root: Element | ShadowRoot = this.renderRoot,
418
+ ): IteratorObject<Component> {
419
+ let all: Array<Element> = childElements(root).toArray();
420
+ const top = [...all];
421
+ for (const el of top) {
422
+ all = [...all, ...this.findChildComponents(el)];
423
+ }
424
+ return Iterator.from(all).filter((el) => el instanceof Component);
425
+ }
426
+
427
+ protected async stabilise(): Promise<void> {
428
+ // wait for children to stabilise
429
+ const children = this.findChildComponents();
430
+ // eslint-disable-next-line no-console
431
+ await Promise.all(children.map((child) => child.hasStabilised)).catch(console.error);
432
+ }
433
+
434
+ protected updated(): void {
435
+ // called when an update has completed
436
+ }
437
+
438
+ protected firstUpdated() {
439
+ // called when the first update has completed, and before
440
+ // {@link Component.updated}.
441
+ }
442
+
443
+ protected stabilised() {
444
+ // called when the component has stabilised after its first update
445
+ }
446
+
447
+ protected initialised() {
448
+ // called when a property marked `@require` has changed and every
449
+ // such property is non-`undefined`.
450
+ }
451
+
452
+ protected firstInitialised() {
453
+ // called the first time all properties marked `@require` become
454
+ // defined, and before {@link Component.initialised}.
455
+ }
456
+
457
+ protected render(): unknown {
458
+ return nothing;
459
+ }
460
+
461
+ [Symbol.dispose]() {
462
+ for (const child of this.findChildComponents()) {
463
+ child[Symbol.dispose]();
464
+ }
465
+ this.disconnectedCallback();
466
+ (this.#rootPart as any)?._$clear?.();
467
+ this.#rootPart = undefined;
468
+ this.remove();
469
+ }
470
+
471
+ useWhileConnected(disposifiable: undefined): undefined;
472
+ useWhileConnected(disposifiable: null): null;
473
+ useWhileConnected(disposifiable: Disposifiable): Disposable;
474
+ useWhileConnected(disposifiable: Disposifiable | undefined): Disposable | undefined;
475
+ useWhileConnected(
476
+ disposifiable: Disposifiable | null | undefined,
477
+ ): Disposable | null | undefined;
478
+ useWhileConnected(
479
+ disposifiable: Disposifiable | null | undefined,
480
+ ): Disposable | null | undefined {
481
+ if (isNullish(disposifiable)) {
482
+ return disposifiable;
483
+ }
484
+ const disposable = toDisposable(disposifiable);
485
+ this.#connectedContext.use(disposable);
486
+ return disposable;
487
+ }
488
+
489
+ on<K extends keyof HTMLElementEventMap>(
490
+ type: K,
491
+ callback: (event: HTMLElementEventMap[K]) => void,
492
+ options?: AddEventListenerOptions | boolean,
493
+ ): Disposable {
494
+ return eventListener(this as Component, type, callback, options);
495
+ }
496
+
497
+ /**
498
+ * If an element that is either inside this element's shadow root or is a
499
+ * child of this element (ie. slotted) currently has focus, return that
500
+ * element. Otherwise, return null.
501
+ */
502
+ getFocusedElement(): Element | null {
503
+ return this.shadowRoot?.activeElement ?? this.querySelector(":focus");
504
+ }
505
+
506
+ query<El extends keyof HTMLElementTagNameMap>(selector: El): HTMLElementTagNameMap[El] | null;
507
+ query<El extends keyof SVGElementTagNameMap>(selector: El): SVGElementTagNameMap[El] | null;
508
+ query<El extends keyof MathMLElementTagNameMap>(
509
+ selector: El,
510
+ ): MathMLElementTagNameMap[El] | null;
511
+ /** @deprecated */
512
+ query<El extends keyof HTMLElementDeprecatedTagNameMap>(
513
+ selector: El,
514
+ ): HTMLElementDeprecatedTagNameMap[El] | null;
515
+ query(selector: string): Element | null;
516
+ query(selector: string): Element | null {
517
+ return this.renderRoot.querySelector(selector);
518
+ }
519
+
520
+ queryAll<El extends keyof HTMLElementTagNameMap>(
521
+ selector: El,
522
+ ): NodeListOf<HTMLElementTagNameMap[El]>;
523
+ queryAll<El extends keyof SVGElementTagNameMap>(
524
+ selector: El,
525
+ ): NodeListOf<SVGElementTagNameMap[El]>;
526
+ queryAll<El extends keyof MathMLElementTagNameMap>(
527
+ selector: El,
528
+ ): NodeListOf<MathMLElementTagNameMap[El]>;
529
+ /** @deprecated */
530
+ queryAll<El extends keyof HTMLElementDeprecatedTagNameMap>(
531
+ selector: El,
532
+ ): NodeListOf<HTMLElementDeprecatedTagNameMap[El]>;
533
+ queryAll(selector: string): NodeListOf<Element>;
534
+ queryAll(selector: string): NodeListOf<Element> {
535
+ return this.renderRoot.querySelectorAll(selector);
536
+ }
537
+
538
+ querySlot(
539
+ options: QuerySlotOptions & { nodes: true; reactive: true },
540
+ ): Signal.Computed<Array<Node>>;
541
+ querySlot(options: QuerySlotOptions & { nodes: true }): Array<Node>;
542
+ querySlot<T>(
543
+ options: QuerySlotOptions & { reactive: true; instanceOf: Constructor<T> },
544
+ ): Signal.Computed<Array<T>>;
545
+ querySlot(options: QuerySlotOptions & { reactive: true }): Signal.Computed<Array<Element>>;
546
+ querySlot<T>(options: QuerySlotOptions & { instanceOf: Constructor<T> }): Array<T>;
547
+ querySlot<El extends keyof HTMLElementTagNameMap>(
548
+ options: QuerySlotOptions & { reactive: true; selector: El },
549
+ ): Signal.Computed<Array<HTMLElementTagNameMap[El]>>;
550
+ querySlot<El extends keyof HTMLElementTagNameMap>(
551
+ options: QuerySlotOptions & { selector: El },
552
+ ): Array<HTMLElementTagNameMap[El]>;
553
+ querySlot<El extends keyof SVGElementTagNameMap>(
554
+ options: QuerySlotOptions & { reactive: true; selector: El },
555
+ ): Signal.Computed<Array<SVGElementTagNameMap[El]>>;
556
+ querySlot<El extends keyof SVGElementTagNameMap>(
557
+ options: QuerySlotOptions & { selector: El },
558
+ ): Array<SVGElementTagNameMap[El]>;
559
+ querySlot<El extends keyof MathMLElementTagNameMap>(
560
+ options: QuerySlotOptions & { reactive: true; selector: El },
561
+ ): Signal.Computed<Array<MathMLElementTagNameMap[El]>>;
562
+ querySlot<El extends keyof MathMLElementTagNameMap>(
563
+ options: QuerySlotOptions & { selector: El },
564
+ ): Array<MathMLElementTagNameMap[El]>;
565
+ /** @deprecated */
566
+ querySlot<El extends keyof HTMLElementDeprecatedTagNameMap>(
567
+ options: QuerySlotOptions & { reactive: true; selector: El },
568
+ ): Signal.Computed<Array<HTMLElementDeprecatedTagNameMap[El]>>;
569
+ /** @deprecated */
570
+ querySlot<El extends keyof HTMLElementDeprecatedTagNameMap>(
571
+ options: QuerySlotOptions & { selector: El },
572
+ ): Array<HTMLElementDeprecatedTagNameMap[El]>;
573
+ querySlot(options?: QuerySlotOptions): Array<Element>;
574
+ querySlot(
575
+ options?: QuerySlotOptions,
576
+ ):
577
+ | Array<Node>
578
+ | Array<Element>
579
+ | Signal.Computed<Array<Node>>
580
+ | Signal.Computed<Array<Element>> {
581
+ const { slot, selector, instanceOf, nodes, reactive } = options ?? {};
582
+ const query = getOrSetQuery(this, options ?? {}, () => (slotEl: HTMLSlotElement | null) => {
583
+ const elements = isNullish(slotEl)
584
+ ? []
585
+ : nodes === true
586
+ ? slotEl.assignedNodes(options)
587
+ : slotEl.assignedElements(options);
588
+ const selectedElements =
589
+ selector !== undefined && nodes !== true
590
+ ? (elements as Array<Element>).filter((node) => node.matches(selector))
591
+ : elements;
592
+ return instanceOf !== undefined
593
+ ? selectedElements.filter((el) => el instanceof instanceOf)
594
+ : selectedElements;
595
+ });
596
+ const slotEl = findSlot(this, slot);
597
+ if (reactive !== true) {
598
+ return query(slotEl);
599
+ }
600
+ const sig = getOrSetSignal(this, query, () => {
601
+ const sig = Signal(query(slotEl), { equals: listsEqual });
602
+ this.addController(new SlotChangeController(this, slot, sig, query));
603
+ return sig.readOnly();
604
+ });
605
+ return sig;
606
+ }
607
+ }
package/src/css.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { assert } from "@bodil/core/assert";
2
+ import { None, Option, Some } from "@bodil/opt";
3
+
4
+ export type Pixels = number;
5
+
6
+ /**
7
+ * Get a computed CSS property for an element.
8
+ */
9
+ export function getComputedProperty(element: Element, property: string): Option<string> {
10
+ return Option.from(window.getComputedStyle(element)?.getPropertyValue(property));
11
+ }
12
+
13
+ const NUMBER_MATCHER = /^(?<number>[+-]?\d+(?:[.]\d+)?(?:e\d+)?)(?<unit>[a-zA-Z]+)$/;
14
+
15
+ function parseWithUnit(value: string): Option<{ number: number; unit: string }> {
16
+ const match = NUMBER_MATCHER.exec(value.trim());
17
+ if (match?.groups === undefined) {
18
+ return None;
19
+ }
20
+ assert(match.groups.number !== undefined);
21
+ assert(match.groups.unit !== undefined);
22
+ const number = Number.parseFloat(match.groups.number);
23
+ if (Number.isNaN(number)) {
24
+ return None;
25
+ }
26
+ return Some({ number, unit: match.groups.unit });
27
+ }
28
+
29
+ /**
30
+ * Parse a CSS size value with units into pixels.
31
+ */
32
+ export function parseSize(value: string): Option<Pixels> {
33
+ return parseWithUnit(value).chain(({ number, unit }) => {
34
+ switch (unit) {
35
+ case "px":
36
+ return Some(number);
37
+ case "cm":
38
+ return Some((number * 96) / 2.54);
39
+ case "mm":
40
+ return Some((number * 96) / 0.254);
41
+ case "Q":
42
+ return Some((number * 96) / 0.0635);
43
+ case "in":
44
+ return Some(number * 96);
45
+ case "pc":
46
+ return Some((number * 96) / 6);
47
+ case "pt":
48
+ return Some((number * 96) / 72);
49
+ default:
50
+ return None;
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Parse a CSS time value with units into milliseconds.
57
+ */
58
+ export function parseTime(value: string): Option<number> {
59
+ return parseWithUnit(value).chain(({ number, unit }) => {
60
+ switch (unit) {
61
+ case "ms":
62
+ return Some(number);
63
+ case "s":
64
+ return Some(number * 1000);
65
+ default:
66
+ return None;
67
+ }
68
+ });
69
+ }