web-toolkit-x 0.0.1 → 0.70.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/AUTHORS +3 -0
- package/LICENSE +1 -1
- package/NOTICE +7 -0
- package/README.md +162 -6
- package/dist/release/index.d.ts +650 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +2831 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/toolkit-0.70.0.js +2834 -0
- package/dist/release/toolkit-0.70.0.js.map +1 -0
- package/package.json +71 -18
- package/src/core/bragi.ts +122 -0
- package/src/core/browser.ts +538 -0
- package/src/core/child-nodes.ts +77 -0
- package/src/core/comment.ts +24 -0
- package/src/core/component.ts +190 -0
- package/src/core/created-roots.ts +38 -0
- package/src/core/custom-element.ts +191 -0
- package/src/core/description.ts +241 -0
- package/src/core/diff.ts +590 -0
- package/src/core/dispatcher.ts +221 -0
- package/src/core/dom.ts +124 -0
- package/src/core/draft.ts +609 -0
- package/src/core/lifecycle.ts +301 -0
- package/src/core/nodes.ts +13 -0
- package/src/core/patch.ts +412 -0
- package/src/core/plugins.ts +231 -0
- package/src/core/reconciler.ts +185 -0
- package/src/core/renderer.ts +75 -0
- package/src/core/runtime.ts +36 -0
- package/src/core/sandbox.ts +128 -0
- package/src/core/service.ts +39 -0
- package/src/core/template.ts +472 -0
- package/src/core/text.ts +24 -0
- package/src/core/toolkit.ts +220 -0
- package/src/core/utils.ts +148 -0
- package/src/core/virtual-dom.ts +118 -0
- package/src/core/virtual-element.ts +72 -0
- package/src/core/virtual-node.ts +87 -0
- package/src/core/web-component.ts +285 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +62 -0
- package/src/plugins/logger.ts +38 -0
- package/src/release.ts +3 -0
- package/index.js +0 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import type { Template as BragiTemplate } from './bragi.js'
|
|
2
|
+
import Browser from './browser.js'
|
|
3
|
+
import {
|
|
4
|
+
ComponentDescription,
|
|
5
|
+
ElementDescription,
|
|
6
|
+
type Listener,
|
|
7
|
+
type NodeDescription,
|
|
8
|
+
type Props,
|
|
9
|
+
TextDescription,
|
|
10
|
+
} from './description.js'
|
|
11
|
+
import type { ComponentClass } from './nodes.js'
|
|
12
|
+
import { runtime } from './runtime.js'
|
|
13
|
+
import { deepFreeze, lowerDash } from './utils.js'
|
|
14
|
+
|
|
15
|
+
export type ItemType =
|
|
16
|
+
| 'component'
|
|
17
|
+
| 'function'
|
|
18
|
+
| 'null'
|
|
19
|
+
| 'node'
|
|
20
|
+
| 'props'
|
|
21
|
+
| 'unknown'
|
|
22
|
+
| 'string'
|
|
23
|
+
| 'number'
|
|
24
|
+
| 'bigint'
|
|
25
|
+
| 'boolean'
|
|
26
|
+
| 'symbol'
|
|
27
|
+
| 'undefined'
|
|
28
|
+
|
|
29
|
+
const isDefined = <T>(value: T): value is NonNullable<T> =>
|
|
30
|
+
value !== undefined && value !== null
|
|
31
|
+
const isFalsy = (template: unknown) => template === null || template === false
|
|
32
|
+
const isNotEmpty = (object: object) => Boolean(Object.keys(object).length)
|
|
33
|
+
|
|
34
|
+
const Template = {
|
|
35
|
+
/**
|
|
36
|
+
* Creates a normalized Description of given template.
|
|
37
|
+
*/
|
|
38
|
+
describe(template: unknown): NodeDescription | null {
|
|
39
|
+
if (isFalsy(template)) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(template) && template.length) {
|
|
44
|
+
let description!: ComponentDescription | ElementDescription
|
|
45
|
+
for (const [item, type, index] of (template as unknown[]).map(
|
|
46
|
+
(item, index): [unknown, ItemType, number] => [
|
|
47
|
+
item,
|
|
48
|
+
this.getItemType(item),
|
|
49
|
+
index,
|
|
50
|
+
],
|
|
51
|
+
)) {
|
|
52
|
+
if (index === 0) {
|
|
53
|
+
switch (type) {
|
|
54
|
+
case 'string':
|
|
55
|
+
description = new ElementDescription(item as string)
|
|
56
|
+
break
|
|
57
|
+
case 'component':
|
|
58
|
+
case 'function':
|
|
59
|
+
description = new ComponentDescription(
|
|
60
|
+
runtime().resolveComponentClass(item, type),
|
|
61
|
+
)
|
|
62
|
+
break
|
|
63
|
+
default:
|
|
64
|
+
console.error(
|
|
65
|
+
'Invalid node type:',
|
|
66
|
+
item,
|
|
67
|
+
`(${type}) at index: ${index}, template:`,
|
|
68
|
+
template,
|
|
69
|
+
)
|
|
70
|
+
throw new Error(`Invalid node type specified: ${type}`)
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (index === 1 && type === 'props') {
|
|
75
|
+
if (description.type === 'component') {
|
|
76
|
+
this.assignPropsToComponent(item as Props, description)
|
|
77
|
+
} else if (description.type === 'element') {
|
|
78
|
+
this.assignPropsToElement(item as Props, description)
|
|
79
|
+
}
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (isFalsy(item)) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (type === 'string' || type === 'number' || item === true) {
|
|
86
|
+
description.children = description.children || []
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- only strings, numbers and true
|
|
88
|
+
description.children.push(new TextDescription(String(item)))
|
|
89
|
+
continue
|
|
90
|
+
} else if (type === 'node') {
|
|
91
|
+
description.children = description.children || []
|
|
92
|
+
description.children.push(this.describe(item)!)
|
|
93
|
+
} else {
|
|
94
|
+
console.error(
|
|
95
|
+
'Invalid item',
|
|
96
|
+
item,
|
|
97
|
+
`at index: ${index}, template:`,
|
|
98
|
+
template,
|
|
99
|
+
)
|
|
100
|
+
throw new Error(`Invalid item specified: ${type}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (runtime().isDebug()) {
|
|
105
|
+
deepFreeze(description)
|
|
106
|
+
}
|
|
107
|
+
return description
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.error('Invalid template definition:', template)
|
|
111
|
+
throw new Error('Expecting array, null or false')
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a new props object supplemented by overridden values.
|
|
116
|
+
*/
|
|
117
|
+
normalizeProps(...overrides: Array<Props | undefined>): Props {
|
|
118
|
+
const result: Props = {}
|
|
119
|
+
for (const override of overrides) {
|
|
120
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
121
|
+
if (result[key] === undefined && value !== undefined) {
|
|
122
|
+
result[key] = value
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalizes specified element props object and returns either
|
|
131
|
+
* a non-empty object containing only supported props or null.
|
|
132
|
+
*/
|
|
133
|
+
normalizeComponentProps(
|
|
134
|
+
props: Props | undefined,
|
|
135
|
+
ComponentClass: { defaultProps?: Props },
|
|
136
|
+
): Props {
|
|
137
|
+
return this.normalizeProps(props, ComponentClass.defaultProps || {})
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
assignPropsToComponent(object: Props, description: ComponentDescription) {
|
|
141
|
+
const props = this.getComponentProps(
|
|
142
|
+
object,
|
|
143
|
+
description.component,
|
|
144
|
+
description.isRoot,
|
|
145
|
+
)
|
|
146
|
+
if (props) {
|
|
147
|
+
description.props = props
|
|
148
|
+
if (isDefined(props.key)) {
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- keys are stringified as given
|
|
150
|
+
description.key = String(props.key)
|
|
151
|
+
}
|
|
152
|
+
if (props.attrs) {
|
|
153
|
+
const attrs = this.getCustomAttributes(props.attrs as Props, true)
|
|
154
|
+
if (attrs) {
|
|
155
|
+
description.attrs = attrs
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
getComponentProps(
|
|
162
|
+
object: Props,
|
|
163
|
+
ComponentClass: ComponentClass,
|
|
164
|
+
isRoot: boolean,
|
|
165
|
+
): Props | null {
|
|
166
|
+
const props = isRoot
|
|
167
|
+
? object
|
|
168
|
+
: this.normalizeComponentProps(object, ComponentClass)
|
|
169
|
+
return isNotEmpty(props) ? props : null
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
assignPropsToElement(props: Props, description: ElementDescription) {
|
|
173
|
+
for (const [key, value] of Object.entries(props)) {
|
|
174
|
+
if (key === 'key') {
|
|
175
|
+
if (isDefined(value)) {
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- keys are stringified as given
|
|
177
|
+
description.key = String(value)
|
|
178
|
+
}
|
|
179
|
+
} else if (key === 'class') {
|
|
180
|
+
const className = this.getClassName(value)
|
|
181
|
+
if (className) {
|
|
182
|
+
description.class = className
|
|
183
|
+
}
|
|
184
|
+
} else if (key === 'style') {
|
|
185
|
+
const style = this.getStyle(value as Props)
|
|
186
|
+
if (style) {
|
|
187
|
+
description.style = style
|
|
188
|
+
}
|
|
189
|
+
} else if (key === 'dataset') {
|
|
190
|
+
const dataset = this.getDataset(value as Props)
|
|
191
|
+
if (dataset) {
|
|
192
|
+
description.dataset = dataset
|
|
193
|
+
}
|
|
194
|
+
} else if (key === 'properties') {
|
|
195
|
+
const properties = this.getProperties(value as Props)
|
|
196
|
+
if (properties) {
|
|
197
|
+
description.properties = properties
|
|
198
|
+
}
|
|
199
|
+
} else if (key === 'attrs') {
|
|
200
|
+
const customAttrs = this.getCustomAttributes(value as Props)
|
|
201
|
+
if (customAttrs) {
|
|
202
|
+
description.custom = description.custom || {}
|
|
203
|
+
description.custom.attrs = customAttrs
|
|
204
|
+
}
|
|
205
|
+
} else if (key === 'on') {
|
|
206
|
+
const customListeners = this.getCustomListeners(value as Props)
|
|
207
|
+
if (customListeners) {
|
|
208
|
+
description.custom = description.custom || {}
|
|
209
|
+
description.custom.listeners = customListeners
|
|
210
|
+
}
|
|
211
|
+
} else if (/^on[A-Z]/.test(key)) {
|
|
212
|
+
const listener = this.getListener(value, key)
|
|
213
|
+
if (listener) {
|
|
214
|
+
description.listeners = description.listeners || {}
|
|
215
|
+
description.listeners[key] = value as Listener
|
|
216
|
+
}
|
|
217
|
+
const element = description.name
|
|
218
|
+
// custom elements define their events themselves
|
|
219
|
+
if (
|
|
220
|
+
runtime().isDebug() &&
|
|
221
|
+
!element.includes('-') &&
|
|
222
|
+
!Browser.isEventSupported(key, element)
|
|
223
|
+
) {
|
|
224
|
+
console.warn(
|
|
225
|
+
`The "${key}" listener is not supported on "${element}" elements.`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const attr = this.getAttributeValue(value)
|
|
230
|
+
if (isDefined(attr)) {
|
|
231
|
+
description.attrs = description.attrs || {}
|
|
232
|
+
description.attrs[key] = attr
|
|
233
|
+
}
|
|
234
|
+
if (runtime().isDebug()) {
|
|
235
|
+
const element = description.name
|
|
236
|
+
if (attr === undefined) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`Invalid undefined value for attribute "${key}"`,
|
|
239
|
+
`on element "${element}".`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
// custom elements define their attributes themselves
|
|
243
|
+
if (
|
|
244
|
+
!element.includes('-') &&
|
|
245
|
+
!Browser.isAttributeSupported(key, element)
|
|
246
|
+
) {
|
|
247
|
+
console.warn(
|
|
248
|
+
`The "${key}" attribute is not supported on "${element}" elements.`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns the type of item used in the array representing node template.
|
|
258
|
+
*/
|
|
259
|
+
getItemType(item: unknown): ItemType {
|
|
260
|
+
const type = typeof item
|
|
261
|
+
switch (type) {
|
|
262
|
+
case 'function':
|
|
263
|
+
// the node type is inherited from the Component or Root class
|
|
264
|
+
if (
|
|
265
|
+
(item as ComponentClass).NodeType === 'component' ||
|
|
266
|
+
(item as ComponentClass).NodeType === 'root'
|
|
267
|
+
) {
|
|
268
|
+
return 'component'
|
|
269
|
+
}
|
|
270
|
+
return 'function'
|
|
271
|
+
case 'object':
|
|
272
|
+
if (item === null) {
|
|
273
|
+
return 'null'
|
|
274
|
+
} else if (Array.isArray(item)) {
|
|
275
|
+
return 'node'
|
|
276
|
+
} else if ((item as object).constructor === Object) {
|
|
277
|
+
return 'props'
|
|
278
|
+
}
|
|
279
|
+
return 'unknown'
|
|
280
|
+
default:
|
|
281
|
+
return type
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolves any object to a space separated string of class names.
|
|
287
|
+
*/
|
|
288
|
+
getClassName(value: unknown): string {
|
|
289
|
+
if (!value) {
|
|
290
|
+
return ''
|
|
291
|
+
}
|
|
292
|
+
if (typeof value === 'string') {
|
|
293
|
+
return value
|
|
294
|
+
}
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
return value
|
|
297
|
+
.map(item => this.getClassName(item))
|
|
298
|
+
.filter(className => className)
|
|
299
|
+
.join(' ')
|
|
300
|
+
}
|
|
301
|
+
if (typeof value === 'object') {
|
|
302
|
+
return Object.keys(value)
|
|
303
|
+
.filter(key => key && (value as Props)[key])
|
|
304
|
+
.join(' ')
|
|
305
|
+
}
|
|
306
|
+
throw new Error(`Invalid value: ${JSON.stringify(value)}`)
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Returns either a non-empty style object containing only understood
|
|
311
|
+
* styling rules or null.
|
|
312
|
+
*/
|
|
313
|
+
getStyle(object: Props): Record<string, string> | null {
|
|
314
|
+
runtime().assert(
|
|
315
|
+
object.constructor === Object,
|
|
316
|
+
'Style must be a plain object!',
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const style: Record<string, string> = {}
|
|
320
|
+
for (const [name, value] of Object.entries(object)) {
|
|
321
|
+
if (runtime().isDebug() && !Browser.isStyleSupported(name)) {
|
|
322
|
+
console.warn(`Unsupported style property, key: ${name}, value:`, value)
|
|
323
|
+
}
|
|
324
|
+
const string = this.getStyleProperty(value, name)
|
|
325
|
+
if (isDefined(string)) {
|
|
326
|
+
style[name] = string
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return isNotEmpty(style) ? style : null
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
getStyleProperty(value: unknown, name: string): string | null {
|
|
333
|
+
if (typeof value === 'string') {
|
|
334
|
+
return value || "''"
|
|
335
|
+
} else if ([true, false, null, undefined].includes(value as boolean)) {
|
|
336
|
+
return null
|
|
337
|
+
} else if (Array.isArray(value)) {
|
|
338
|
+
return value.join('')
|
|
339
|
+
} else if (typeof value === 'number') {
|
|
340
|
+
return String(value)
|
|
341
|
+
} else if (typeof value === 'object') {
|
|
342
|
+
if (name !== 'filter' && name !== 'transform') {
|
|
343
|
+
throw new Error(`Unknown function list: ${JSON.stringify(value)}`)
|
|
344
|
+
}
|
|
345
|
+
// the filter functions are named in camel case, e.g. dropShadow for
|
|
346
|
+
// drop-shadow, the transform functions are in CSS too, e.g. translateX
|
|
347
|
+
const functions = this.getFunctionList(
|
|
348
|
+
value as Props,
|
|
349
|
+
name === 'filter' ? lowerDash : undefined,
|
|
350
|
+
)
|
|
351
|
+
if (runtime().isDebug() && functions && !CSS.supports(name, functions)) {
|
|
352
|
+
console.warn(`Unsupported ${name} functions: ${functions}`)
|
|
353
|
+
}
|
|
354
|
+
return functions
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`Invalid style property value: ${JSON.stringify(value)}`)
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Returns a multi-property string value.
|
|
361
|
+
*/
|
|
362
|
+
getFunctionList(
|
|
363
|
+
object: Props,
|
|
364
|
+
getCSSName: (name: string) => string = name => name,
|
|
365
|
+
): string {
|
|
366
|
+
const composite: Record<string, string> = {}
|
|
367
|
+
for (const [key, value] of Object.entries(object)) {
|
|
368
|
+
const stringValue = this.getAttributeValue(value, /*= allowEmpty */ false)
|
|
369
|
+
if (isDefined(stringValue)) {
|
|
370
|
+
composite[getCSSName(key)] = stringValue
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return Object.entries(composite)
|
|
374
|
+
.map(([key, value]) => `${key}(${value})`)
|
|
375
|
+
.join(' ')
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
getListener(value: unknown, name: string): Listener | null {
|
|
379
|
+
if (typeof value === 'function') {
|
|
380
|
+
return value as Listener
|
|
381
|
+
}
|
|
382
|
+
if (value === null || value === false || value === undefined) {
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
throw new Error(`Invalid listener specified for event: ${name}`)
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolves given value to a string.
|
|
390
|
+
*/
|
|
391
|
+
getAttributeValue(
|
|
392
|
+
value: unknown,
|
|
393
|
+
allowEmpty = true,
|
|
394
|
+
): string | null | undefined {
|
|
395
|
+
if (value === true || value === '') {
|
|
396
|
+
return allowEmpty ? '' : null
|
|
397
|
+
} else if (typeof value === 'string') {
|
|
398
|
+
return value
|
|
399
|
+
} else if (value === null || value === false) {
|
|
400
|
+
return null
|
|
401
|
+
} else if (value === undefined) {
|
|
402
|
+
return undefined
|
|
403
|
+
} else if (Array.isArray(value)) {
|
|
404
|
+
return value.join('')
|
|
405
|
+
} else if (['object', 'function', 'symbol'].includes(typeof value)) {
|
|
406
|
+
throw new Error(`Invalid attribute value: ${JSON.stringify(value)}!`)
|
|
407
|
+
}
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are rejected above
|
|
409
|
+
return String(value)
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Returns either a non-empty dataset object or null.
|
|
414
|
+
*/
|
|
415
|
+
getDataset(object: Props): Record<string, string> | null {
|
|
416
|
+
const dataset: Record<string, string> = {}
|
|
417
|
+
for (const key of Object.keys(object)) {
|
|
418
|
+
const value = this.getAttributeValue(object[key])
|
|
419
|
+
if (isDefined(value)) {
|
|
420
|
+
dataset[key] = value
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return isNotEmpty(dataset) ? dataset : null
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Returns either a non-empty object containing properties set
|
|
428
|
+
* directly on a rendered DOM Element or null.
|
|
429
|
+
*/
|
|
430
|
+
getProperties(object: Props): Props | null {
|
|
431
|
+
return isNotEmpty(object) ? object : null
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
getCustomAttributes(
|
|
435
|
+
object: Props,
|
|
436
|
+
forComponent?: boolean,
|
|
437
|
+
): Record<string, string> | null {
|
|
438
|
+
runtime().assert(
|
|
439
|
+
object.constructor === Object,
|
|
440
|
+
'Expecting object for custom attributes!',
|
|
441
|
+
)
|
|
442
|
+
const attrs: Record<string, string> = {}
|
|
443
|
+
for (const [key, value] of Object.entries(object)) {
|
|
444
|
+
const attr = this.getAttributeValue(value, /*= allowEmpty */ true)
|
|
445
|
+
if (isDefined(attr)) {
|
|
446
|
+
const name = forComponent ? lowerDash(key) : key
|
|
447
|
+
attrs[name] = attr
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return isNotEmpty(attrs) ? attrs : null
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
getCustomListeners(object: Props): Record<string, Listener> | null {
|
|
454
|
+
runtime().assert(
|
|
455
|
+
object.constructor === Object,
|
|
456
|
+
'Expecting object for custom listeners!',
|
|
457
|
+
)
|
|
458
|
+
const listeners: Record<string, Listener> = {}
|
|
459
|
+
for (const [key, value] of Object.entries(object)) {
|
|
460
|
+
const listener = this.getListener(value, key)
|
|
461
|
+
if (listener) {
|
|
462
|
+
listeners[key] = listener
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return isNotEmpty(listeners) ? listeners : null
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* A Bragi template, see bragi.ts, sharing its name with the module above. */
|
|
470
|
+
type Template = BragiTemplate
|
|
471
|
+
|
|
472
|
+
export default Template
|
package/src/core/text.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TextDescription } from './description.js'
|
|
2
|
+
import VirtualNode from './virtual-node.js'
|
|
3
|
+
|
|
4
|
+
class Text extends VirtualNode {
|
|
5
|
+
static NodeType = 'text'
|
|
6
|
+
|
|
7
|
+
declare description: TextDescription
|
|
8
|
+
declare ref: globalThis.Text
|
|
9
|
+
|
|
10
|
+
constructor(description: TextDescription, parentNode?: VirtualNode) {
|
|
11
|
+
super(description, parentNode)
|
|
12
|
+
this.attachDOM()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get nodeType(): string {
|
|
16
|
+
return Text.NodeType
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
attachDOM() {
|
|
20
|
+
this.ref = document.createTextNode(this.description.text)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default Text
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { PureComponent, RenderResult } from './bragi.js'
|
|
2
|
+
import CreatedRoots from './created-roots.js'
|
|
3
|
+
import { updateStylesheets } from './custom-element.js'
|
|
4
|
+
import type { ComponentDescription, Props } from './description.js'
|
|
5
|
+
import { Component, type ComponentClass, WebComponent } from './nodes.js'
|
|
6
|
+
import Plugins, { type PluginManifest } from './plugins.js'
|
|
7
|
+
import Renderer from './renderer.js'
|
|
8
|
+
import { provideRuntime } from './runtime.js'
|
|
9
|
+
import Sandbox from './sandbox.js'
|
|
10
|
+
import Template, { type ItemType } from './template.js'
|
|
11
|
+
import { isSameList } from './utils.js'
|
|
12
|
+
import VirtualDOM from './virtual-dom.js'
|
|
13
|
+
|
|
14
|
+
export interface Settings {
|
|
15
|
+
readonly debug: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Options {
|
|
19
|
+
debug?: boolean
|
|
20
|
+
plugins?: PluginManifest[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* The state used by the core modules through the runtime, kept out of
|
|
25
|
+
* the public Toolkit instance.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/* The top-level root components, tracking their subroots. */
|
|
29
|
+
const roots = new Set<WebComponent>()
|
|
30
|
+
|
|
31
|
+
/* The settings used until Toolkit is configured. */
|
|
32
|
+
const defaultSettings: Settings = Object.freeze({ debug: false })
|
|
33
|
+
|
|
34
|
+
/* Function to Component mapping. */
|
|
35
|
+
const pureComponentClassRegistry = new Map<PureComponent, ComponentClass>()
|
|
36
|
+
|
|
37
|
+
const createPlugins = (manifests: PluginManifest[] = []): Plugins => {
|
|
38
|
+
const plugins = new Plugins(null)
|
|
39
|
+
for (const manifest of manifests) {
|
|
40
|
+
plugins.register(manifest)
|
|
41
|
+
}
|
|
42
|
+
return plugins
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* The plugins configured for all root components. */
|
|
46
|
+
let plugins = createPlugins()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns a PureComponent class rendering the template
|
|
50
|
+
* provided by the specified function.
|
|
51
|
+
*/
|
|
52
|
+
const resolvePureComponentClass = (fn: PureComponent): ComponentClass => {
|
|
53
|
+
let ComponentClass = pureComponentClassRegistry.get(fn)
|
|
54
|
+
if (ComponentClass) {
|
|
55
|
+
return ComponentClass
|
|
56
|
+
}
|
|
57
|
+
ComponentClass = class PureComponent extends Component {
|
|
58
|
+
static renderer = fn
|
|
59
|
+
|
|
60
|
+
render(): RenderResult {
|
|
61
|
+
// the props type of a pure component is not known here
|
|
62
|
+
return fn.call(this, this.props as never)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
pureComponentClassRegistry.set(fn, ComponentClass)
|
|
66
|
+
return ComponentClass
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns resolved Component class.
|
|
71
|
+
*/
|
|
72
|
+
const resolveComponentClass = (
|
|
73
|
+
component: unknown,
|
|
74
|
+
type: ItemType,
|
|
75
|
+
): ComponentClass => {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case 'component':
|
|
78
|
+
return component as ComponentClass
|
|
79
|
+
case 'function':
|
|
80
|
+
return resolvePureComponentClass(component as PureComponent)
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Unsupported component type: ${type}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const track = (root: WebComponent) => {
|
|
87
|
+
if (root.parentNode) {
|
|
88
|
+
const parentRootNode = root.parentNode.rootNode
|
|
89
|
+
parentRootNode.subroots.add(root)
|
|
90
|
+
root.stopTracking = () => {
|
|
91
|
+
parentRootNode.subroots.delete(root)
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
roots.add(root)
|
|
95
|
+
root.stopTracking = () => {
|
|
96
|
+
roots.delete(root)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class Toolkit {
|
|
102
|
+
declare settings: Settings
|
|
103
|
+
declare assert: (condition: unknown, message?: string) => void
|
|
104
|
+
|
|
105
|
+
constructor() {
|
|
106
|
+
this.settings = defaultSettings
|
|
107
|
+
this.assert = console.assert as Toolkit['assert']
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Configures Toolkit with given options, keeping the current values
|
|
112
|
+
* of the options not provided, by default no debug mode and no plugins.
|
|
113
|
+
* Changed plugins are uninstalled from the created roots, and the new
|
|
114
|
+
* ones installed in their place, with their stylesheets loaded.
|
|
115
|
+
*/
|
|
116
|
+
configure(options: Options) {
|
|
117
|
+
if (options.debug !== undefined) {
|
|
118
|
+
this.settings = Object.freeze({ debug: options.debug })
|
|
119
|
+
}
|
|
120
|
+
const manifests = options.plugins
|
|
121
|
+
const previous = [...plugins]
|
|
122
|
+
const origins = previous.map(plugin => plugin.origin)
|
|
123
|
+
if (!manifests || isSameList(origins, manifests)) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
// the methods of the remaining plugins are registered again below
|
|
127
|
+
Sandbox.clearPluginMethods()
|
|
128
|
+
plugins = createPlugins(manifests)
|
|
129
|
+
for (const root of this.tracked) {
|
|
130
|
+
for (const plugin of previous) {
|
|
131
|
+
root.plugins!.uninstall(plugin.name)
|
|
132
|
+
}
|
|
133
|
+
for (const plugin of root.plugins!) {
|
|
134
|
+
plugin.register?.()
|
|
135
|
+
}
|
|
136
|
+
for (const plugin of plugins) {
|
|
137
|
+
root.plugins!.register(plugin)
|
|
138
|
+
root.plugins!.install(plugin)
|
|
139
|
+
}
|
|
140
|
+
if (root.shadow) {
|
|
141
|
+
updateStylesheets(root)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resets Toolkit to a pristine state, with the default settings
|
|
148
|
+
* and no plugins, forgetting the created roots.
|
|
149
|
+
*/
|
|
150
|
+
reset() {
|
|
151
|
+
plugins.destroy()
|
|
152
|
+
plugins = createPlugins()
|
|
153
|
+
roots.clear()
|
|
154
|
+
this.settings = defaultSettings
|
|
155
|
+
pureComponentClassRegistry.clear()
|
|
156
|
+
Sandbox.clearPluginMethods()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* The created root components, with their subroots, until destroyed. */
|
|
160
|
+
get tracked(): WebComponent[] {
|
|
161
|
+
const tracked: WebComponent[] = []
|
|
162
|
+
for (const root of roots) {
|
|
163
|
+
tracked.push(root, ...root.tracked)
|
|
164
|
+
}
|
|
165
|
+
return tracked
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isDebug(): boolean {
|
|
169
|
+
return this.settings.debug
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
warn(...messages: unknown[]) {
|
|
173
|
+
if (this.isDebug()) {
|
|
174
|
+
console.warn(...messages)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async createRoot(
|
|
179
|
+
component: ComponentClass,
|
|
180
|
+
props: Props = {},
|
|
181
|
+
): Promise<WebComponent> {
|
|
182
|
+
const description = Template.describe([
|
|
183
|
+
component,
|
|
184
|
+
props,
|
|
185
|
+
]) as ComponentDescription
|
|
186
|
+
// not to stay tracked, with the plugins installed, when it throws
|
|
187
|
+
return CreatedRoots.destroyingRootsOnError(() =>
|
|
188
|
+
VirtualDOM.createWebComponent(description, null),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async render(
|
|
193
|
+
component: ComponentClass,
|
|
194
|
+
container: Element,
|
|
195
|
+
props: Props = {},
|
|
196
|
+
): Promise<WebComponent> {
|
|
197
|
+
const root = await this.createRoot(component, props)
|
|
198
|
+
return root.mount(container)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* The Toolkit singleton, exposed globally as toolkit. */
|
|
203
|
+
export const toolkit = new Toolkit()
|
|
204
|
+
|
|
205
|
+
// asserts, debug mode and warnings are read from the instance, which
|
|
206
|
+
// can be configured, e.g. with the assert function throwing in tests
|
|
207
|
+
provideRuntime({
|
|
208
|
+
get plugins() {
|
|
209
|
+
return plugins
|
|
210
|
+
},
|
|
211
|
+
assert: (condition, message) => toolkit.assert(condition, message),
|
|
212
|
+
isDebug: () => toolkit.isDebug(),
|
|
213
|
+
warn: (...messages) => toolkit.warn(...messages),
|
|
214
|
+
track,
|
|
215
|
+
resolveComponentClass,
|
|
216
|
+
update: (root, from, to, commands) =>
|
|
217
|
+
Renderer.update(root, from, to, commands),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
export default Toolkit
|