@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.
- package/LICENCE.md +288 -0
- package/dist/component.d.ts +147 -0
- package/dist/component.js +405 -0
- package/dist/component.js.map +1 -0
- package/dist/component.test.d.ts +1 -0
- package/dist/component.test.js +141 -0
- package/dist/component.test.js.map +1 -0
- package/dist/css.d.ts +14 -0
- package/dist/css.js +63 -0
- package/dist/css.js.map +1 -0
- package/dist/decorators/attribute.d.ts +26 -0
- package/dist/decorators/attribute.js +115 -0
- package/dist/decorators/attribute.js.map +1 -0
- package/dist/decorators/attribute.test.d.ts +1 -0
- package/dist/decorators/attribute.test.js +148 -0
- package/dist/decorators/attribute.test.js.map +1 -0
- package/dist/decorators/connect.d.ts +9 -0
- package/dist/decorators/connect.js +44 -0
- package/dist/decorators/connect.js.map +1 -0
- package/dist/decorators/connect.test.d.ts +1 -0
- package/dist/decorators/connect.test.js +196 -0
- package/dist/decorators/connect.test.js.map +1 -0
- package/dist/decorators/reactive.d.ts +7 -0
- package/dist/decorators/reactive.js +45 -0
- package/dist/decorators/reactive.js.map +1 -0
- package/dist/decorators/reactive.test.d.ts +1 -0
- package/dist/decorators/reactive.test.js +113 -0
- package/dist/decorators/reactive.test.js.map +1 -0
- package/dist/decorators/require.d.ts +3 -0
- package/dist/decorators/require.js +6 -0
- package/dist/decorators/require.js.map +1 -0
- package/dist/decorators/require.test.d.ts +1 -0
- package/dist/decorators/require.test.js +117 -0
- package/dist/decorators/require.test.js.map +1 -0
- package/dist/dom.d.ts +92 -0
- package/dist/dom.js +354 -0
- package/dist/dom.js.map +1 -0
- package/dist/emitter.d.ts +10 -0
- package/dist/emitter.js +17 -0
- package/dist/emitter.js.map +1 -0
- package/dist/event.d.ts +70 -0
- package/dist/event.js +14 -0
- package/dist/event.js.map +1 -0
- package/dist/event.test.d.ts +1 -0
- package/dist/event.test.js +20 -0
- package/dist/event.test.js.map +1 -0
- package/dist/geometry.d.ts +48 -0
- package/dist/geometry.js +117 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +54 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/slot.d.ts +31 -0
- package/dist/slot.js +67 -0
- package/dist/slot.js.map +1 -0
- package/dist/test-global.d.ts +0 -0
- package/dist/test-global.js +5 -0
- package/dist/test-global.js.map +1 -0
- package/package.json +116 -0
- package/src/component.test.ts +90 -0
- package/src/component.ts +607 -0
- package/src/css.ts +69 -0
- package/src/decorators/attribute.test.ts +77 -0
- package/src/decorators/attribute.ts +197 -0
- package/src/decorators/connect.test.ts +119 -0
- package/src/decorators/connect.ts +85 -0
- package/src/decorators/reactive.test.ts +45 -0
- package/src/decorators/reactive.ts +80 -0
- package/src/decorators/require.test.ts +57 -0
- package/src/decorators/require.ts +11 -0
- package/src/dom.ts +456 -0
- package/src/emitter.ts +32 -0
- package/src/event.test.ts +22 -0
- package/src/event.ts +74 -0
- package/src/geometry.ts +147 -0
- package/src/index.ts +12 -0
- package/src/scheduler.ts +58 -0
- package/src/slot.ts +95 -0
- package/src/test-global.ts +5 -0
package/src/component.ts
ADDED
|
@@ -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
|
+
}
|