web-toolkit-x 0.0.1 → 0.71.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 +169 -6
- package/dist/release/global.d.ts +10 -0
- package/dist/release/index.d.ts +650 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +2837 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/toolkit-0.71.0.js +2840 -0
- package/dist/release/toolkit-0.71.0.js.map +1 -0
- package/package.json +75 -18
- package/src/core/bragi.ts +122 -0
- package/src/core/browser.ts +555 -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/global.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,190 @@
|
|
|
1
|
+
import type { Child, RenderResult } from './bragi.js'
|
|
2
|
+
import {
|
|
3
|
+
CommentDescription,
|
|
4
|
+
type ComponentDescription,
|
|
5
|
+
type NodeDescription,
|
|
6
|
+
type Props,
|
|
7
|
+
} from './description.js'
|
|
8
|
+
import type Dispatcher from './dispatcher.js'
|
|
9
|
+
import type { Commands, CommandsAPI } from './dispatcher.js'
|
|
10
|
+
import { runtime } from './runtime.js'
|
|
11
|
+
import Sandbox, { type ComponentSandbox } from './sandbox.js'
|
|
12
|
+
import Template from './template.js'
|
|
13
|
+
import { type AnyFunction, invariant } from './utils.js'
|
|
14
|
+
import VirtualNode, { type NodeRef } from './virtual-node.js'
|
|
15
|
+
import type WebComponent from './web-component.js'
|
|
16
|
+
|
|
17
|
+
/* A task run when a component is destroyed, e.g. disconnecting a service. */
|
|
18
|
+
export type CleanUpTask = (() => void) & { service?: unknown }
|
|
19
|
+
|
|
20
|
+
/* A service components connect to, e.g. a subclass of Service. */
|
|
21
|
+
export interface Connectable {
|
|
22
|
+
connect(listeners: Record<string, unknown>): () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* The class of any component, whatever its props. */
|
|
26
|
+
export type ComponentClass = typeof Component<object>
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Node representing Component in the virtual DOM tree.
|
|
30
|
+
* Components
|
|
31
|
+
*/
|
|
32
|
+
class Component<P extends object = object> extends VirtualNode {
|
|
33
|
+
static NodeType = 'component'
|
|
34
|
+
|
|
35
|
+
declare static elementName?: string
|
|
36
|
+
declare static defaultProps?: Props
|
|
37
|
+
declare static commands?: CommandsAPI | CommandsAPI[]
|
|
38
|
+
|
|
39
|
+
static get displayName(): string {
|
|
40
|
+
return this.name
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare description: ComponentDescription
|
|
44
|
+
declare sandbox: ComponentSandbox
|
|
45
|
+
declare cleanUpTasks: CleanUpTask[]
|
|
46
|
+
declare isInitialized: boolean
|
|
47
|
+
declare content: VirtualNode | null
|
|
48
|
+
|
|
49
|
+
/* The component props, available on the sandbox passed as `this`. */
|
|
50
|
+
declare props: P
|
|
51
|
+
|
|
52
|
+
/* The child templates, available on the sandbox passed as `this`. */
|
|
53
|
+
declare children: Child[]
|
|
54
|
+
|
|
55
|
+
onCreated?(): void
|
|
56
|
+
onAttached?(): void
|
|
57
|
+
onPropsReceived?(nextProps: P): void
|
|
58
|
+
onUpdated?(prevProps: P): void
|
|
59
|
+
onDestroyed?(): void
|
|
60
|
+
onDetached?(): void
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
description: ComponentDescription,
|
|
64
|
+
parent?: VirtualNode | null,
|
|
65
|
+
context?: WebComponent | null,
|
|
66
|
+
attachDOM = true,
|
|
67
|
+
) {
|
|
68
|
+
super(description, parent, context)
|
|
69
|
+
this.sandbox = Sandbox.create(this, COMPONENT_PROPERTIES)
|
|
70
|
+
this.cleanUpTasks = []
|
|
71
|
+
this.isInitialized = attachDOM
|
|
72
|
+
if (attachDOM) {
|
|
73
|
+
this.attachDOM()
|
|
74
|
+
}
|
|
75
|
+
// the rendered content is inserted right after instantiation
|
|
76
|
+
this.content = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sets the component content.
|
|
81
|
+
*/
|
|
82
|
+
setContent(node: VirtualNode) {
|
|
83
|
+
invariant(
|
|
84
|
+
node.parentNode === this,
|
|
85
|
+
'Specified node does not have a valid parent!',
|
|
86
|
+
)
|
|
87
|
+
this.content!.parentNode = null
|
|
88
|
+
node.parentNode = this
|
|
89
|
+
this.content!.ref.replaceWith(node.ref)
|
|
90
|
+
this.content = node
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
hasOwnMethod(method: string): boolean {
|
|
94
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
95
|
+
return (this.constructor as ComponentClass).prototype.hasOwnProperty(method)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
connectTo(service: Connectable, listeners: Record<string, unknown>) {
|
|
99
|
+
runtime().assert(
|
|
100
|
+
typeof service.connect === 'function',
|
|
101
|
+
'Services have to define the connect() method',
|
|
102
|
+
)
|
|
103
|
+
const disconnect: CleanUpTask = service.connect(listeners)
|
|
104
|
+
runtime().assert(
|
|
105
|
+
typeof disconnect === 'function',
|
|
106
|
+
'The result of the connect() method has to be a disconnect() method',
|
|
107
|
+
)
|
|
108
|
+
disconnect.service = service
|
|
109
|
+
this.cleanUpTasks.push(disconnect)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get childElement(): VirtualNode | null {
|
|
113
|
+
if (this.content) {
|
|
114
|
+
if (this.content.isElement() || this.content.isRoot()) {
|
|
115
|
+
return this.content
|
|
116
|
+
}
|
|
117
|
+
if (this.content.isComponent()) {
|
|
118
|
+
return this.content.childElement
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get placeholder(): VirtualNode | null {
|
|
125
|
+
if (this.content!.isComment()) {
|
|
126
|
+
return this.content
|
|
127
|
+
}
|
|
128
|
+
return (this.content as Component).placeholder || null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render(): RenderResult {
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Calls the render method and transforms the returned template
|
|
137
|
+
* into the normalised description of the rendered node.
|
|
138
|
+
*/
|
|
139
|
+
renderDescription(
|
|
140
|
+
props: Props = {},
|
|
141
|
+
children: unknown[] = [],
|
|
142
|
+
): NodeDescription | null {
|
|
143
|
+
this.sandbox.props = props
|
|
144
|
+
this.sandbox.children = children
|
|
145
|
+
const template = this.render.call(this.sandbox)
|
|
146
|
+
if (template) {
|
|
147
|
+
return Template.describe(template)
|
|
148
|
+
}
|
|
149
|
+
const text = (this.constructor as ComponentClass).displayName
|
|
150
|
+
return new CommentDescription(text)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* The commands of the root component, whose types are not known here. */
|
|
154
|
+
get commands(): Commands<unknown> & Record<string, AnyFunction> {
|
|
155
|
+
return this.context ? this.context.commands : this.rootNode.commands
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get dispatcher(): Dispatcher {
|
|
159
|
+
return this.context ? this.context.dispatcher : this.rootNode.dispatcher
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
destroy() {
|
|
163
|
+
for (const cleanUpTask of this.cleanUpTasks) {
|
|
164
|
+
cleanUpTask()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get nodeType(): string {
|
|
169
|
+
return Component.NodeType
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get ref(): NodeRef {
|
|
173
|
+
return this.content!.ref
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isCompatible(node: VirtualNode | null | undefined) {
|
|
177
|
+
return super.isCompatible(node) && this.constructor === node!.constructor
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
attachDOM() {
|
|
181
|
+
if (this.content) {
|
|
182
|
+
this.content.attachDOM()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The properties of the Component class hidden from the sandbox. */
|
|
188
|
+
const COMPONENT_PROPERTIES = Object.getOwnPropertyNames(Component.prototype)
|
|
189
|
+
|
|
190
|
+
export default Component
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WebComponent } from './nodes.js'
|
|
2
|
+
|
|
3
|
+
/* The roots created by the function called by destroyingRootsOnError(). */
|
|
4
|
+
let createdRoots: WebComponent[] | null = null
|
|
5
|
+
|
|
6
|
+
const CreatedRoots = {
|
|
7
|
+
/**
|
|
8
|
+
* Calls the function, destroying the roots it creates when it throws.
|
|
9
|
+
* They are neither attached nor destroyed by the lifecycle then, but
|
|
10
|
+
* would stay tracked, with the plugins installed.
|
|
11
|
+
*/
|
|
12
|
+
destroyingRootsOnError<T>(fn: () => T): T {
|
|
13
|
+
const outerRoots = createdRoots
|
|
14
|
+
const roots: WebComponent[] = []
|
|
15
|
+
createdRoots = roots
|
|
16
|
+
try {
|
|
17
|
+
const result = fn()
|
|
18
|
+
outerRoots?.push(...roots)
|
|
19
|
+
return result
|
|
20
|
+
} catch (error) {
|
|
21
|
+
for (const root of roots.reverse()) {
|
|
22
|
+
root.destroy()
|
|
23
|
+
}
|
|
24
|
+
throw error
|
|
25
|
+
} finally {
|
|
26
|
+
createdRoots = outerRoots
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Records a root being created, see destroyingRootsOnError().
|
|
32
|
+
*/
|
|
33
|
+
collectRoot(root: WebComponent) {
|
|
34
|
+
createdRoots?.push(root)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default CreatedRoots
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import DOM from './dom.js'
|
|
2
|
+
import Lifecycle, { throwErrors } from './lifecycle.js'
|
|
3
|
+
import type { WebComponent } from './nodes.js'
|
|
4
|
+
import Plugins, { type Plugin, type PluginManifest } from './plugins.js'
|
|
5
|
+
import { isSameList } from './utils.js'
|
|
6
|
+
|
|
7
|
+
const cssImports = (paths: string[]) =>
|
|
8
|
+
paths.map(path => `@import url(${path});`).join('\n')
|
|
9
|
+
|
|
10
|
+
type PluginOrManifest = Plugin | PluginManifest
|
|
11
|
+
|
|
12
|
+
export class ComponentElement extends HTMLElement {
|
|
13
|
+
declare $root: WebComponent | null
|
|
14
|
+
declare pendingDestruction?: ReturnType<typeof setTimeout>
|
|
15
|
+
declare stylesheets: string[]
|
|
16
|
+
declare styleElement: HTMLStyleElement | null
|
|
17
|
+
declare stylesheetsLoaded: Promise<void>
|
|
18
|
+
declare install: (plugin: PluginOrManifest, cascade?: boolean) => void
|
|
19
|
+
declare uninstall: (
|
|
20
|
+
plugin: PluginOrManifest | string,
|
|
21
|
+
cascade?: boolean,
|
|
22
|
+
) => void
|
|
23
|
+
|
|
24
|
+
constructor(root: WebComponent) {
|
|
25
|
+
super()
|
|
26
|
+
this.$root = root
|
|
27
|
+
this.stylesheets = []
|
|
28
|
+
this.styleElement = null
|
|
29
|
+
this.stylesheetsLoaded = Promise.resolve()
|
|
30
|
+
|
|
31
|
+
addPluginsAPI(this)
|
|
32
|
+
|
|
33
|
+
root.shadow = this.attachShadow({
|
|
34
|
+
mode: 'open',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const stylesheets = root.getStylesheets()
|
|
38
|
+
|
|
39
|
+
const init = () => {
|
|
40
|
+
root.init().catch((error: Error) => root.markAsFailed(error))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (stylesheets.length) {
|
|
44
|
+
// rejects mounting, as the errors are not thrown to the caller
|
|
45
|
+
this.loadStylesheets(stylesheets).then(init, (error: Error) =>
|
|
46
|
+
root.markAsFailed(error),
|
|
47
|
+
)
|
|
48
|
+
} else {
|
|
49
|
+
init()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replaces the stylesheets imported in the shadow root, keeping the
|
|
55
|
+
* previous ones until the new ones are loaded.
|
|
56
|
+
*/
|
|
57
|
+
loadStylesheets(stylesheets: string[]): Promise<void> {
|
|
58
|
+
const previous = this.styleElement
|
|
59
|
+
this.stylesheets = stylesheets
|
|
60
|
+
let loaded = Promise.resolve()
|
|
61
|
+
if (stylesheets.length) {
|
|
62
|
+
const style = document.createElement('style')
|
|
63
|
+
style.textContent = cssImports(stylesheets)
|
|
64
|
+
loaded = new Promise((resolve, reject) => {
|
|
65
|
+
style.onload = () => resolve()
|
|
66
|
+
style.onerror = () =>
|
|
67
|
+
reject(
|
|
68
|
+
new Error(`Error loading stylesheets: ${stylesheets.join(', ')}`),
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
if (previous) {
|
|
72
|
+
previous.after(style)
|
|
73
|
+
} else {
|
|
74
|
+
this.shadowRoot!.prepend(style)
|
|
75
|
+
}
|
|
76
|
+
this.styleElement = style
|
|
77
|
+
} else {
|
|
78
|
+
this.styleElement = null
|
|
79
|
+
}
|
|
80
|
+
if (previous) {
|
|
81
|
+
// removed once loaded, as the root may be waiting to be initialized
|
|
82
|
+
void Promise.allSettled([this.stylesheetsLoaded, loaded]).then(() =>
|
|
83
|
+
previous.remove(),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
this.stylesheetsLoaded = loaded
|
|
87
|
+
return loaded
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get isComponentElement() {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
connectedCallback() {
|
|
95
|
+
clearTimeout(this.pendingDestruction)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
disconnectedCallback() {
|
|
99
|
+
this.pendingDestruction = setTimeout(() => this.destroy(), 50)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
destroy() {
|
|
103
|
+
const root = this.$root
|
|
104
|
+
if (!root) {
|
|
105
|
+
// already destroyed, e.g. directly before the scheduled destruction
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
// the hooks of all the components are called when some of them throw
|
|
109
|
+
const errors: unknown[] = []
|
|
110
|
+
Lifecycle.collectingErrors(errors, () => {
|
|
111
|
+
Lifecycle.onComponentDestroyed(root)
|
|
112
|
+
Lifecycle.onComponentDetached(root)
|
|
113
|
+
})
|
|
114
|
+
root.ref = null
|
|
115
|
+
this.$root = null
|
|
116
|
+
throwErrors(errors, 'Errors destroying the component')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const addPluginsAPI = (element: ComponentElement) => {
|
|
121
|
+
const { Plugin } = Plugins
|
|
122
|
+
element.install = (plugin, cascade = true) => {
|
|
123
|
+
const installTo = (root: WebComponent) => {
|
|
124
|
+
if (plugin instanceof Plugin) {
|
|
125
|
+
root.plugins!.registry.add(plugin)
|
|
126
|
+
root.plugins!.install(plugin)
|
|
127
|
+
} else {
|
|
128
|
+
root.plugins!.install(plugin)
|
|
129
|
+
}
|
|
130
|
+
if (cascade) {
|
|
131
|
+
for (const subroot of root.subroots) {
|
|
132
|
+
installTo(subroot)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
installTo(element.$root!)
|
|
137
|
+
}
|
|
138
|
+
element.uninstall = (plugin, cascade = true) => {
|
|
139
|
+
const name = typeof plugin === 'string' ? plugin : plugin.name
|
|
140
|
+
const uninstallFrom = (root: WebComponent) => {
|
|
141
|
+
root.plugins!.uninstall(name)
|
|
142
|
+
if (cascade) {
|
|
143
|
+
for (const subroot of root.subroots) {
|
|
144
|
+
uninstallFrom(subroot)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
uninstallFrom(element.$root!)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Loads the stylesheets of the root in its custom element when they
|
|
154
|
+
* changed, e.g. with the plugins reconfigured.
|
|
155
|
+
*/
|
|
156
|
+
export const updateStylesheets = (root: WebComponent) => {
|
|
157
|
+
const element = root.ref as ComponentElement
|
|
158
|
+
const stylesheets = root.getStylesheets()
|
|
159
|
+
if (!isSameList(stylesheets, element.stylesheets)) {
|
|
160
|
+
element
|
|
161
|
+
.loadStylesheets(stylesheets)
|
|
162
|
+
.catch((error: unknown) => console.error(error))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates a new Custom Element instance assigned to specified Web Component.
|
|
168
|
+
*/
|
|
169
|
+
export const createComponentElement = (
|
|
170
|
+
root: WebComponent,
|
|
171
|
+
): ComponentElement => {
|
|
172
|
+
const defineCustomElementClass = (RootClass: typeof WebComponent) => {
|
|
173
|
+
let ElementClass = customElements.get(RootClass.elementName!) as
|
|
174
|
+
typeof ComponentElement | undefined
|
|
175
|
+
if (!ElementClass) {
|
|
176
|
+
ElementClass = class RootElement extends ComponentElement {}
|
|
177
|
+
customElements.define(RootClass.elementName!, ElementClass)
|
|
178
|
+
RootClass.prototype.elementClass = ElementClass
|
|
179
|
+
}
|
|
180
|
+
return ElementClass
|
|
181
|
+
}
|
|
182
|
+
const ElementClass = defineCustomElementClass(
|
|
183
|
+
root.constructor as typeof WebComponent,
|
|
184
|
+
)
|
|
185
|
+
const element = new ElementClass(root)
|
|
186
|
+
// the attributes of a root are custom, as set by the diff
|
|
187
|
+
for (const [name, value] of Object.entries(root.description.attrs ?? {})) {
|
|
188
|
+
DOM.setAttribute(element, name, value, true)
|
|
189
|
+
}
|
|
190
|
+
return element
|
|
191
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { ComponentClass } from './nodes.js'
|
|
2
|
+
|
|
3
|
+
export type Props = Record<string, unknown>
|
|
4
|
+
|
|
5
|
+
/* An event listener, possibly bound to a component by its sandbox. */
|
|
6
|
+
export type Listener = ((event: Event) => unknown) & { source?: unknown }
|
|
7
|
+
|
|
8
|
+
export type NodeDescription =
|
|
9
|
+
| ComponentDescription
|
|
10
|
+
| ElementDescription
|
|
11
|
+
| CommentDescription
|
|
12
|
+
| TextDescription
|
|
13
|
+
|
|
14
|
+
/* A normalized template, as returned by the asTemplate getters. */
|
|
15
|
+
export type NormalizedTemplate = unknown[] | string | null
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Normalized description of a template.
|
|
19
|
+
* Is used to calculate differences between nodes.
|
|
20
|
+
*/
|
|
21
|
+
abstract class Description {
|
|
22
|
+
declare static ElementDescription: typeof ElementDescription
|
|
23
|
+
declare static ComponentDescription: typeof ComponentDescription
|
|
24
|
+
declare static CommentDescription: typeof CommentDescription
|
|
25
|
+
declare static TextDescription: typeof TextDescription
|
|
26
|
+
|
|
27
|
+
declare key?: string
|
|
28
|
+
declare children?: NodeDescription[]
|
|
29
|
+
|
|
30
|
+
abstract get asTemplate(): NormalizedTemplate
|
|
31
|
+
|
|
32
|
+
get childrenAsTemplates(): NormalizedTemplate[] | undefined {
|
|
33
|
+
if (this.children) {
|
|
34
|
+
return this.children.map(child => child.asTemplate)
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isCompatible(description: Description): boolean {
|
|
40
|
+
return this.constructor === description.constructor
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Defines a normalized description of a component.
|
|
46
|
+
*
|
|
47
|
+
* Enumerable properties:
|
|
48
|
+
* - key (a unique node identifier within its parent),
|
|
49
|
+
* - component (an object with meta information)
|
|
50
|
+
* - children (an array of child nodes)
|
|
51
|
+
* - props (an object of any component rendering props)
|
|
52
|
+
*
|
|
53
|
+
* Non-enumerable properties:
|
|
54
|
+
* - asTemplate: returns component description as a normalized template
|
|
55
|
+
*/
|
|
56
|
+
class ComponentDescription extends Description {
|
|
57
|
+
declare component: ComponentClass
|
|
58
|
+
declare type: 'component'
|
|
59
|
+
declare props?: Props
|
|
60
|
+
declare attrs?: Record<string, string>
|
|
61
|
+
|
|
62
|
+
constructor(component: ComponentClass) {
|
|
63
|
+
super()
|
|
64
|
+
this.component = component
|
|
65
|
+
this.type = 'component'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isCompatible(description: Description): boolean {
|
|
69
|
+
return (
|
|
70
|
+
super.isCompatible(description) &&
|
|
71
|
+
this.component === (description as ComponentDescription).component
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get isRoot(): boolean {
|
|
76
|
+
// the node type is inherited from the Root class
|
|
77
|
+
return this.component.NodeType === 'root'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get asTemplate(): unknown[] {
|
|
81
|
+
const template: unknown[] = [this.component]
|
|
82
|
+
if (this.props) {
|
|
83
|
+
template.push(this.props)
|
|
84
|
+
}
|
|
85
|
+
if (this.children) {
|
|
86
|
+
template.push(...this.children.map(child => child.asTemplate))
|
|
87
|
+
}
|
|
88
|
+
return template
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Defines a normalized description of an element.
|
|
94
|
+
*
|
|
95
|
+
* Enumerable properties:
|
|
96
|
+
* - key (a unique node identifier within its parent),
|
|
97
|
+
* - name (a string representing tag name),
|
|
98
|
+
* - children (an array of child nodes),
|
|
99
|
+
* - props (an object) defining:
|
|
100
|
+
* - class (a class name string)
|
|
101
|
+
* - style (an object for style property to string value mapping)
|
|
102
|
+
* - listeners (an object for event name to listener mapping)
|
|
103
|
+
* - attrs (an object for normalized attribute name to value mapping)
|
|
104
|
+
* - dataset (an object representing data attributes)
|
|
105
|
+
* - properties (an object for properties set directly on DOM element)
|
|
106
|
+
*
|
|
107
|
+
* Non-enumerable properties:
|
|
108
|
+
* - asTemplate: returns element description as a normalized template
|
|
109
|
+
*/
|
|
110
|
+
class ElementDescription extends Description {
|
|
111
|
+
declare name: string
|
|
112
|
+
declare type: 'element'
|
|
113
|
+
declare class?: string
|
|
114
|
+
declare style?: Record<string, string>
|
|
115
|
+
declare attrs?: Record<string, string>
|
|
116
|
+
declare dataset?: Record<string, string>
|
|
117
|
+
declare listeners?: Record<string, Listener>
|
|
118
|
+
declare properties?: Record<string, unknown>
|
|
119
|
+
declare custom?: {
|
|
120
|
+
attrs?: Record<string, string>
|
|
121
|
+
listeners?: Record<string, Listener>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
constructor(name: string) {
|
|
125
|
+
super()
|
|
126
|
+
this.name = name
|
|
127
|
+
this.type = 'element'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isCompatible(description: Description): boolean {
|
|
131
|
+
return (
|
|
132
|
+
super.isCompatible(description) &&
|
|
133
|
+
this.name === (description as ElementDescription).name
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get asTemplate(): unknown[] {
|
|
138
|
+
const template: unknown[] = [this.name]
|
|
139
|
+
const props: Record<string, unknown> = {}
|
|
140
|
+
if (this.key) {
|
|
141
|
+
props.key = this.key
|
|
142
|
+
}
|
|
143
|
+
if (this.class) {
|
|
144
|
+
props.class = this.class
|
|
145
|
+
}
|
|
146
|
+
if (this.style) {
|
|
147
|
+
props.style = this.style
|
|
148
|
+
}
|
|
149
|
+
if (this.attrs) {
|
|
150
|
+
Object.assign(props, this.attrs)
|
|
151
|
+
}
|
|
152
|
+
if (this.dataset) {
|
|
153
|
+
props.dataset = this.dataset
|
|
154
|
+
}
|
|
155
|
+
if (this.listeners) {
|
|
156
|
+
Object.assign(props, this.listeners)
|
|
157
|
+
}
|
|
158
|
+
if (this.properties) {
|
|
159
|
+
props.properties = this.properties
|
|
160
|
+
}
|
|
161
|
+
if (this.custom?.attrs) {
|
|
162
|
+
props.attrs = this.custom.attrs
|
|
163
|
+
}
|
|
164
|
+
if (this.custom?.listeners) {
|
|
165
|
+
props.on = this.custom.listeners
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(props).length) {
|
|
168
|
+
template.push(props)
|
|
169
|
+
}
|
|
170
|
+
if (this.children) {
|
|
171
|
+
template.push(...this.children.map(child => child.asTemplate))
|
|
172
|
+
}
|
|
173
|
+
return template
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* Description of a Comment node.
|
|
179
|
+
*/
|
|
180
|
+
class CommentDescription extends Description {
|
|
181
|
+
declare text: string
|
|
182
|
+
declare type: 'comment'
|
|
183
|
+
|
|
184
|
+
constructor(text: string) {
|
|
185
|
+
super()
|
|
186
|
+
this.text = text
|
|
187
|
+
this.type = 'comment'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get asTemplate(): null {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isCompatible(description: Description): boolean {
|
|
195
|
+
return (
|
|
196
|
+
super.isCompatible(description) &&
|
|
197
|
+
this.text === (description as CommentDescription).text
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* Description of a Text node.
|
|
204
|
+
*/
|
|
205
|
+
class TextDescription extends Description {
|
|
206
|
+
declare text: string
|
|
207
|
+
declare type: 'text'
|
|
208
|
+
|
|
209
|
+
constructor(text: string) {
|
|
210
|
+
super()
|
|
211
|
+
this.text = text
|
|
212
|
+
this.type = 'text'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get asTemplate(): string {
|
|
216
|
+
return this.text
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
isCompatible(description: Description): boolean {
|
|
220
|
+
return (
|
|
221
|
+
super.isCompatible(description) &&
|
|
222
|
+
this.text === (description as TextDescription).text
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Object.assign(Description, {
|
|
228
|
+
ElementDescription,
|
|
229
|
+
ComponentDescription,
|
|
230
|
+
CommentDescription,
|
|
231
|
+
TextDescription,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
ElementDescription,
|
|
236
|
+
ComponentDescription,
|
|
237
|
+
CommentDescription,
|
|
238
|
+
TextDescription,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default Description
|