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,148 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export type AnyFunction = (...args: any[]) => unknown
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Checks a condition the toolkit relies on internally. Unlike
|
|
6
|
+
* toolkit.assert, which reports invalid usage, it throws in all modes.
|
|
7
|
+
*/
|
|
8
|
+
export function invariant(
|
|
9
|
+
condition: unknown,
|
|
10
|
+
message: string,
|
|
11
|
+
): asserts condition {
|
|
12
|
+
if (!condition) {
|
|
13
|
+
throw new Error(message)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const throttle = <T extends AnyFunction>(
|
|
18
|
+
fn: T,
|
|
19
|
+
wait = 200,
|
|
20
|
+
delayFirstEvent = false,
|
|
21
|
+
) => {
|
|
22
|
+
let lastTimestamp = 0
|
|
23
|
+
let taskId: ReturnType<typeof setTimeout> | null = null
|
|
24
|
+
|
|
25
|
+
let context: unknown
|
|
26
|
+
let params: Parameters<T>
|
|
27
|
+
|
|
28
|
+
return function throttled(this: unknown, ...args: Parameters<T>) {
|
|
29
|
+
if (!taskId) {
|
|
30
|
+
const timestamp = Date.now()
|
|
31
|
+
const elapsed = timestamp - lastTimestamp
|
|
32
|
+
const scheduleTask = (delay: number) => {
|
|
33
|
+
taskId = setTimeout(() => {
|
|
34
|
+
taskId = null
|
|
35
|
+
lastTimestamp = Date.now()
|
|
36
|
+
return fn.call(context, ...params)
|
|
37
|
+
}, delay)
|
|
38
|
+
}
|
|
39
|
+
if (elapsed >= wait) {
|
|
40
|
+
lastTimestamp = timestamp
|
|
41
|
+
if (!delayFirstEvent) {
|
|
42
|
+
return fn.call(this, ...args)
|
|
43
|
+
}
|
|
44
|
+
scheduleTask(wait)
|
|
45
|
+
} else {
|
|
46
|
+
scheduleTask(wait - elapsed)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- kept for the delayed call
|
|
50
|
+
context = this
|
|
51
|
+
params = args
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const debounce = <T extends AnyFunction>(
|
|
56
|
+
fn: T,
|
|
57
|
+
wait = 200,
|
|
58
|
+
leading = false,
|
|
59
|
+
) => {
|
|
60
|
+
let taskId: ReturnType<typeof setTimeout> | null = null
|
|
61
|
+
// whether the delayed call is needed, not when only the leading call was made
|
|
62
|
+
let isTrailing = false
|
|
63
|
+
|
|
64
|
+
let context: unknown
|
|
65
|
+
let params: Parameters<T>
|
|
66
|
+
|
|
67
|
+
return function debounced(this: unknown, ...args: Parameters<T>) {
|
|
68
|
+
const isFirstInvocation = !taskId
|
|
69
|
+
if (taskId) {
|
|
70
|
+
clearTimeout(taskId)
|
|
71
|
+
}
|
|
72
|
+
isTrailing = !(isFirstInvocation && leading)
|
|
73
|
+
taskId = setTimeout(() => {
|
|
74
|
+
taskId = null
|
|
75
|
+
if (isTrailing) {
|
|
76
|
+
fn.call(context, ...params)
|
|
77
|
+
}
|
|
78
|
+
}, wait)
|
|
79
|
+
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- kept for the delayed call
|
|
81
|
+
context = this
|
|
82
|
+
params = args
|
|
83
|
+
|
|
84
|
+
if (!isTrailing) {
|
|
85
|
+
return fn.call(context, ...params)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const createUUID = () => {
|
|
91
|
+
const s4 = () =>
|
|
92
|
+
Math.floor((1 + Math.random()) * 0x10000)
|
|
93
|
+
.toString(16)
|
|
94
|
+
.substring(1)
|
|
95
|
+
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const lowerDash = (name: string) =>
|
|
99
|
+
name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
100
|
+
|
|
101
|
+
export const getAttributeName = (key: string) => {
|
|
102
|
+
if (key === 'acceptCharset' || key === 'httpEquiv') {
|
|
103
|
+
return lowerDash(key)
|
|
104
|
+
} else if (key.startsWith('aria')) {
|
|
105
|
+
return `aria-${key.slice(4).toLowerCase()}`
|
|
106
|
+
}
|
|
107
|
+
return key.toLowerCase()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const getEventName = (key: string) =>
|
|
111
|
+
key === 'onDoubleClick' ? 'dblclick' : key.slice(2).toLowerCase()
|
|
112
|
+
|
|
113
|
+
const postRender = (fn: FrameRequestCallback) => {
|
|
114
|
+
// since Chromium 64 there are some problems with animations not being
|
|
115
|
+
// triggered correctly, this hack solves the problem across all OS-es
|
|
116
|
+
|
|
117
|
+
/* eslint-disable prefer-arrow-callback */
|
|
118
|
+
requestAnimationFrame(function () {
|
|
119
|
+
requestAnimationFrame(fn)
|
|
120
|
+
})
|
|
121
|
+
/* eslint-enable prefer-arrow-callback */
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Checks if the arrays have the same items, in the same order. */
|
|
125
|
+
export const isSameList = (a: readonly unknown[], b: readonly unknown[]) =>
|
|
126
|
+
a.length === b.length && a.every((item, index) => item === b[index])
|
|
127
|
+
|
|
128
|
+
export const deepFreeze = <T>(obj: T): T => {
|
|
129
|
+
if (obj === null || typeof obj !== 'object' || Object.isFrozen(obj)) {
|
|
130
|
+
// functions are intentionally not frozen
|
|
131
|
+
return obj
|
|
132
|
+
}
|
|
133
|
+
Object.freeze(obj)
|
|
134
|
+
for (const property of Object.getOwnPropertyNames(obj)) {
|
|
135
|
+
deepFreeze((obj as Record<string, unknown>)[property])
|
|
136
|
+
}
|
|
137
|
+
return obj
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* The helpers of the public API, the others are used internally. */
|
|
141
|
+
const Utils = {
|
|
142
|
+
throttle,
|
|
143
|
+
debounce,
|
|
144
|
+
createUUID,
|
|
145
|
+
postRender,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default Utils
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ComponentDescription, NodeDescription } from './description.js'
|
|
2
|
+
import {
|
|
3
|
+
Comment,
|
|
4
|
+
type Component,
|
|
5
|
+
type ParentVirtualNode,
|
|
6
|
+
Text,
|
|
7
|
+
VirtualElement,
|
|
8
|
+
type VirtualNode,
|
|
9
|
+
WebComponent,
|
|
10
|
+
} from './nodes.js'
|
|
11
|
+
|
|
12
|
+
const VirtualDOM = {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new Virtual DOM structure from given description.
|
|
15
|
+
*/
|
|
16
|
+
createFromDescription(
|
|
17
|
+
description: NodeDescription | null,
|
|
18
|
+
parent?: VirtualNode | null,
|
|
19
|
+
context?: WebComponent | null,
|
|
20
|
+
): VirtualNode | null {
|
|
21
|
+
if (!description) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
switch (description.type) {
|
|
25
|
+
case 'component':
|
|
26
|
+
return this.createComponent(description, parent, context)
|
|
27
|
+
case 'element': {
|
|
28
|
+
const element = new VirtualElement(description, parent, context)
|
|
29
|
+
this.createChildNodes(element)
|
|
30
|
+
return element
|
|
31
|
+
}
|
|
32
|
+
case 'comment':
|
|
33
|
+
return new Comment(description, parent!)
|
|
34
|
+
case 'text':
|
|
35
|
+
return new Text(description, parent!)
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unsupported node type: ${(description as NodeDescription).type}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates the child nodes of an element or a Web Component.
|
|
45
|
+
*/
|
|
46
|
+
createChildNodes(parent: ParentVirtualNode) {
|
|
47
|
+
const { children } = parent.description
|
|
48
|
+
if (children) {
|
|
49
|
+
parent.setChildNodes(
|
|
50
|
+
children.map(description =>
|
|
51
|
+
this.createFromDescription(description, parent, parent.context)!,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new component instance from given description.
|
|
59
|
+
*/
|
|
60
|
+
createComponent(
|
|
61
|
+
description: ComponentDescription,
|
|
62
|
+
parent?: VirtualNode | null,
|
|
63
|
+
context?: WebComponent | null,
|
|
64
|
+
): Component {
|
|
65
|
+
const ComponentClass = description.component
|
|
66
|
+
if (ComponentClass.prototype instanceof WebComponent) {
|
|
67
|
+
return this.createWebComponent(
|
|
68
|
+
description,
|
|
69
|
+
parent && parent.rootNode,
|
|
70
|
+
context,
|
|
71
|
+
/*= requireCustomElement */ true,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const component = new ComponentClass(description, parent, context)
|
|
75
|
+
const nodeDescription = component.renderDescription(
|
|
76
|
+
description.props,
|
|
77
|
+
description.childrenAsTemplates,
|
|
78
|
+
)
|
|
79
|
+
component.content = this.createFromDescription(
|
|
80
|
+
nodeDescription,
|
|
81
|
+
component,
|
|
82
|
+
context,
|
|
83
|
+
)
|
|
84
|
+
return component
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a new Web Component instance from given description.
|
|
89
|
+
*/
|
|
90
|
+
createWebComponent(
|
|
91
|
+
description: ComponentDescription,
|
|
92
|
+
parent?: VirtualNode | null,
|
|
93
|
+
context?: WebComponent | null,
|
|
94
|
+
requireCustomElement = false,
|
|
95
|
+
): WebComponent {
|
|
96
|
+
try {
|
|
97
|
+
const ComponentClass = description.component as typeof WebComponent
|
|
98
|
+
if (requireCustomElement && !ComponentClass.elementName) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Root component "${
|
|
101
|
+
ComponentClass.displayName
|
|
102
|
+
}" does not define custom element name!`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const root = new ComponentClass(description, parent, context)
|
|
106
|
+
if (ComponentClass.elementName) {
|
|
107
|
+
// rendered in the light DOM of the custom element
|
|
108
|
+
this.createChildNodes(root)
|
|
109
|
+
}
|
|
110
|
+
return root
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error rendering root component:', description)
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default VirtualDOM
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ChildNodes from './child-nodes.js'
|
|
2
|
+
import type { ElementDescription } from './description.js'
|
|
3
|
+
import DOM from './dom.js'
|
|
4
|
+
import VirtualNode from './virtual-node.js'
|
|
5
|
+
import type WebComponent from './web-component.js'
|
|
6
|
+
|
|
7
|
+
class VirtualElement extends VirtualNode {
|
|
8
|
+
static NodeType = 'element'
|
|
9
|
+
|
|
10
|
+
declare description: ElementDescription
|
|
11
|
+
declare ref: HTMLElement
|
|
12
|
+
declare children?: VirtualNode[]
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
description: ElementDescription,
|
|
16
|
+
parent?: VirtualNode | null,
|
|
17
|
+
context?: WebComponent | null,
|
|
18
|
+
) {
|
|
19
|
+
super(description, parent, context)
|
|
20
|
+
this.attachDOM()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* The child nodes, as shared with Web Components. */
|
|
24
|
+
get childNodes(): VirtualNode[] | undefined {
|
|
25
|
+
return this.children
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set childNodes(nodes: VirtualNode[] | undefined) {
|
|
29
|
+
if (nodes) {
|
|
30
|
+
this.children = nodes
|
|
31
|
+
} else {
|
|
32
|
+
delete this.children
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get nodeType(): string {
|
|
37
|
+
return VirtualElement.NodeType
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isCompatible(node: VirtualNode | null | undefined) {
|
|
41
|
+
return (
|
|
42
|
+
super.isCompatible(node) &&
|
|
43
|
+
this.description.name === (node as VirtualElement).description.name
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setChildNodes(nodes: VirtualNode[]) {
|
|
48
|
+
ChildNodes.set(this, nodes)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
insertChild(child: VirtualNode, index?: number) {
|
|
52
|
+
ChildNodes.insert(this, child, index)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
replaceChild(child: VirtualNode, node: VirtualNode) {
|
|
56
|
+
ChildNodes.replace(this, child, node)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
moveChild(child: VirtualNode, from: number, to: number) {
|
|
60
|
+
ChildNodes.move(this, child, from, to)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeChild(child: VirtualNode) {
|
|
64
|
+
ChildNodes.remove(this, child)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
attachDOM() {
|
|
68
|
+
this.ref = DOM.createElement(this.description)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default VirtualElement
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type Comment from './comment.js'
|
|
2
|
+
import type Component from './component.js'
|
|
3
|
+
import type { NodeDescription } from './description.js'
|
|
4
|
+
import type Text from './text.js'
|
|
5
|
+
import type VirtualElement from './virtual-element.js'
|
|
6
|
+
import type WebComponent from './web-component.js'
|
|
7
|
+
|
|
8
|
+
/* The DOM node rendered for a virtual node. */
|
|
9
|
+
export type NodeRef = Element | CharacterData
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* An abstract parent node.
|
|
13
|
+
*/
|
|
14
|
+
abstract class VirtualNode {
|
|
15
|
+
declare description: NodeDescription
|
|
16
|
+
declare key?: string
|
|
17
|
+
declare parentNode: VirtualNode | null
|
|
18
|
+
declare context: WebComponent | null
|
|
19
|
+
|
|
20
|
+
abstract ref: NodeRef
|
|
21
|
+
abstract get nodeType(): string
|
|
22
|
+
abstract attachDOM(): void
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
description: NodeDescription,
|
|
26
|
+
parentNode: VirtualNode | null = null,
|
|
27
|
+
context: WebComponent | null = null,
|
|
28
|
+
) {
|
|
29
|
+
this.description = description
|
|
30
|
+
this.key = description.key
|
|
31
|
+
this.parentNode = parentNode
|
|
32
|
+
this.context = context
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get parentElement(): VirtualNode | null {
|
|
36
|
+
if (this.parentNode) {
|
|
37
|
+
return this.parentNode.isElement()
|
|
38
|
+
? this.parentNode
|
|
39
|
+
: this.parentNode.parentElement
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get container(): Element | VirtualNode | undefined {
|
|
45
|
+
if (this.parentNode) {
|
|
46
|
+
return this.parentNode.container
|
|
47
|
+
}
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get rootNode(): WebComponent {
|
|
52
|
+
if (this.isRoot()) {
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
if (this.parentNode) {
|
|
56
|
+
return this.parentNode.rootNode
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Inconsistent virtual DOM tree detected!')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isRoot(): this is WebComponent {
|
|
62
|
+
return this.nodeType === 'root'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isComponent(): this is Component {
|
|
66
|
+
const { nodeType } = this
|
|
67
|
+
return nodeType === 'component' || nodeType === 'root'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isElement(): this is VirtualElement {
|
|
71
|
+
return this.nodeType === 'element'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
isComment(): this is Comment {
|
|
75
|
+
return this.nodeType === 'comment'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isText(): this is Text {
|
|
79
|
+
return this.nodeType === 'text'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
isCompatible(node: VirtualNode | null | undefined) {
|
|
83
|
+
return node && this.nodeType === node.nodeType && this.key === node.key
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default VirtualNode
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import ChildNodes from './child-nodes.js'
|
|
2
|
+
import Comment from './comment.js'
|
|
3
|
+
import Component from './component.js'
|
|
4
|
+
import CreatedRoots from './created-roots.js'
|
|
5
|
+
import { createComponentElement } from './custom-element.js'
|
|
6
|
+
import { CommentDescription, type ComponentDescription } from './description.js'
|
|
7
|
+
import Dispatcher, {
|
|
8
|
+
type BoundCommands,
|
|
9
|
+
type Commands,
|
|
10
|
+
type CommandsAPI,
|
|
11
|
+
type State,
|
|
12
|
+
} from './dispatcher.js'
|
|
13
|
+
import Plugins from './plugins.js'
|
|
14
|
+
import { runtime } from './runtime.js'
|
|
15
|
+
import Template from './template.js'
|
|
16
|
+
import type VirtualNode from './virtual-node.js'
|
|
17
|
+
import type { NodeRef } from './virtual-node.js'
|
|
18
|
+
|
|
19
|
+
const CONTAINER = Symbol('container')
|
|
20
|
+
const CUSTOM_ELEMENT = Symbol('custom-element')
|
|
21
|
+
const DISPATCHER = Symbol('dispatcher')
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* A root component with its own state, commands and plugins, rendered
|
|
25
|
+
* in a custom element when elementName is defined.
|
|
26
|
+
*
|
|
27
|
+
* P: props received from the parent,
|
|
28
|
+
* S: state, available as `this.props` when rendering,
|
|
29
|
+
* C: commands API set as the static commands, changing the state.
|
|
30
|
+
*/
|
|
31
|
+
class WebComponent<
|
|
32
|
+
P extends object = object,
|
|
33
|
+
S extends object = P,
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- no commands besides the core ones
|
|
35
|
+
C extends CommandsAPI<S> = {},
|
|
36
|
+
> extends Component<S> {
|
|
37
|
+
static NodeType = 'root'
|
|
38
|
+
|
|
39
|
+
static styles: string[] = []
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* The child nodes rendered in the light DOM of the custom element,
|
|
43
|
+
* separate from the `children` templates available when rendering.
|
|
44
|
+
*/
|
|
45
|
+
declare childNodes?: VirtualNode[]
|
|
46
|
+
declare subroots: Set<WebComponent>
|
|
47
|
+
declare ready: Promise<void>
|
|
48
|
+
declare markAsReady: () => void
|
|
49
|
+
declare markAsFailed: (error: Error) => void
|
|
50
|
+
declare plugins: Plugins | null
|
|
51
|
+
declare shadow: ShadowRoot | null
|
|
52
|
+
declare state?: S
|
|
53
|
+
declare pendingDescription?: ComponentDescription
|
|
54
|
+
declare stopTracking?: () => void
|
|
55
|
+
declare elementClass?: CustomElementConstructor;
|
|
56
|
+
declare [CONTAINER]?: Element;
|
|
57
|
+
declare [CUSTOM_ELEMENT]?: HTMLElement | null;
|
|
58
|
+
declare [DISPATCHER]: Dispatcher
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
description: ComponentDescription,
|
|
62
|
+
parent: VirtualNode | null = null,
|
|
63
|
+
context: WebComponent | null = null,
|
|
64
|
+
) {
|
|
65
|
+
super(description, parent, context, /*= attachDOM */ false)
|
|
66
|
+
// before being tracked, to be destroyed when rendering it fails
|
|
67
|
+
CreatedRoots.collectRoot(this)
|
|
68
|
+
this.subroots = new Set()
|
|
69
|
+
this.dispatcher = new Dispatcher(this)
|
|
70
|
+
this.ready = new Promise((resolve, reject) => {
|
|
71
|
+
this.markAsReady = resolve
|
|
72
|
+
this.markAsFailed = reject
|
|
73
|
+
})
|
|
74
|
+
this.plugins = this.createPlugins()
|
|
75
|
+
// from the start, for the plugins to be replaced when reconfigured
|
|
76
|
+
runtime().track(this)
|
|
77
|
+
this.content = this.createPlaceholder()
|
|
78
|
+
this.shadow = null
|
|
79
|
+
this.attachDOM()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
attachDOM() {
|
|
83
|
+
if ((this.constructor as typeof WebComponent).elementName) {
|
|
84
|
+
this.ref = createComponentElement(this)
|
|
85
|
+
this.plugins!.installAll()
|
|
86
|
+
} else {
|
|
87
|
+
this.plugins!.installAll()
|
|
88
|
+
super.attachDOM()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
createPlaceholder(): VirtualNode {
|
|
93
|
+
return new Comment(
|
|
94
|
+
new CommentDescription(
|
|
95
|
+
(this.constructor as typeof WebComponent).displayName,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Triggers the initial rendering of the component in given container.
|
|
102
|
+
*/
|
|
103
|
+
async init() {
|
|
104
|
+
// props are passed from templates, which are not checked against P
|
|
105
|
+
const state = await this.getInitialState.call(
|
|
106
|
+
this.sandbox,
|
|
107
|
+
(this.description.props || {}) as P,
|
|
108
|
+
)
|
|
109
|
+
this.setState(state)
|
|
110
|
+
|
|
111
|
+
if (this.pendingDescription) {
|
|
112
|
+
const description = this.pendingDescription
|
|
113
|
+
delete this.pendingDescription
|
|
114
|
+
setTimeout(() => this.update(description))
|
|
115
|
+
}
|
|
116
|
+
this.isInitialized = true
|
|
117
|
+
this.markAsReady()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setState(state: S) {
|
|
121
|
+
if (state.constructor !== Object) {
|
|
122
|
+
throw new Error('Web Component state must be a plain object!')
|
|
123
|
+
}
|
|
124
|
+
this.dispatcher.setState(
|
|
125
|
+
Template.normalizeComponentProps(
|
|
126
|
+
state as State,
|
|
127
|
+
this.constructor as typeof WebComponent,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Triggers the component update.
|
|
134
|
+
*/
|
|
135
|
+
update(description: ComponentDescription) {
|
|
136
|
+
if (!this.isInitialized) {
|
|
137
|
+
this.pendingDescription = description
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const state = this.getUpdatedState(
|
|
141
|
+
(description.props || {}) as P,
|
|
142
|
+
this.state || ({} as S),
|
|
143
|
+
)
|
|
144
|
+
this.setState(state)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The default implementation delegating the calculation of initial state
|
|
149
|
+
* to the state manager.
|
|
150
|
+
*/
|
|
151
|
+
getInitialState(props: P): S | Promise<S> {
|
|
152
|
+
// the default state is a copy of the props
|
|
153
|
+
return { ...props } as unknown as S
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The default implementation delegating the calculation of updated state
|
|
158
|
+
* to the state manager.
|
|
159
|
+
*/
|
|
160
|
+
getUpdatedState(props: P, state: S): S {
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
...props,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get dispatcher(): Dispatcher {
|
|
168
|
+
return this[DISPATCHER]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
set dispatcher(dispatcher: Dispatcher) {
|
|
172
|
+
this[DISPATCHER] = dispatcher
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get commands(): Commands<S> & BoundCommands<C> {
|
|
176
|
+
return this.dispatcher.commands as Commands<S> & BoundCommands<C>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
createPlugins(): Plugins {
|
|
180
|
+
const plugins = new Plugins(this)
|
|
181
|
+
const inherited = this.parentNode
|
|
182
|
+
? (this.parentNode as WebComponent).plugins
|
|
183
|
+
: runtime().plugins
|
|
184
|
+
for (const plugin of inherited!) {
|
|
185
|
+
plugins.register(plugin)
|
|
186
|
+
}
|
|
187
|
+
return plugins
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async mount(container: Element): Promise<this> {
|
|
191
|
+
if ((this.constructor as typeof WebComponent).elementName) {
|
|
192
|
+
// triggers this.init() from element's connected callback
|
|
193
|
+
container.appendChild(this.ref)
|
|
194
|
+
await this.ready
|
|
195
|
+
} else {
|
|
196
|
+
this.container = container
|
|
197
|
+
await this.init()
|
|
198
|
+
}
|
|
199
|
+
return this
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getStylesheets(): string[] {
|
|
203
|
+
const stylesheets: string[] = []
|
|
204
|
+
const stylesheetProviders = [...this.plugins!].filter(plugin =>
|
|
205
|
+
plugin.isStylesheetProvider(),
|
|
206
|
+
)
|
|
207
|
+
for (const plugin of stylesheetProviders) {
|
|
208
|
+
if (typeof plugin.getStylesheets !== 'function') {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Plugin '${plugin.name}' must provide the getStylesheets() method!`,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
stylesheets.push(...plugin.getStylesheets())
|
|
214
|
+
}
|
|
215
|
+
const RootClass = this.constructor as typeof WebComponent
|
|
216
|
+
if (Array.isArray(RootClass.styles)) {
|
|
217
|
+
stylesheets.push(...RootClass.styles)
|
|
218
|
+
}
|
|
219
|
+
return stylesheets
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get ref(): NodeRef {
|
|
223
|
+
return this[CUSTOM_ELEMENT] || super.ref
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
set ref(ref: HTMLElement | null) {
|
|
227
|
+
this[CUSTOM_ELEMENT] = ref
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
set container(container: Element | undefined) {
|
|
231
|
+
this[CONTAINER] = container
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
get container(): Element | undefined {
|
|
235
|
+
return this[CONTAINER]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
get tracked(): WebComponent[] {
|
|
239
|
+
const tracked: WebComponent[] = []
|
|
240
|
+
for (const root of this.subroots) {
|
|
241
|
+
tracked.push(root, ...root.tracked)
|
|
242
|
+
}
|
|
243
|
+
return tracked
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
destroy() {
|
|
247
|
+
if (!this.plugins) {
|
|
248
|
+
// already destroyed
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
super.destroy()
|
|
252
|
+
// not tracked when destroyed before being initialized
|
|
253
|
+
this.stopTracking?.()
|
|
254
|
+
this.dispatcher.ignoreIncoming()
|
|
255
|
+
this.plugins.destroy()
|
|
256
|
+
this.plugins = null
|
|
257
|
+
this.parentNode = null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setChildNodes(nodes: VirtualNode[]) {
|
|
261
|
+
ChildNodes.set(this, nodes)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
insertChild(child: VirtualNode, index?: number) {
|
|
265
|
+
ChildNodes.insert(this, child, index)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
replaceChild(child: VirtualNode, node: VirtualNode) {
|
|
269
|
+
ChildNodes.replace(this, child, node)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
moveChild(child: VirtualNode, from: number, to: number) {
|
|
273
|
+
ChildNodes.move(this, child, from, to)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
removeChild(child: VirtualNode) {
|
|
277
|
+
ChildNodes.remove(this, child)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get nodeType(): string {
|
|
281
|
+
return WebComponent.NodeType
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default WebComponent
|