@viewfly/platform-browser 2.0.2 → 2.2.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/README.md +34 -37
- package/dist/create-app.d.ts +17 -0
- package/dist/create-portal.d.ts +35 -0
- package/dist/dom-renderer.d.ts +28 -0
- package/dist/html-renderer.d.ts +57 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +354 -0
- package/dist/index.js +362 -0
- package/{bundles/index.d.ts → dist/jsx-dom.d.ts} +73 -211
- package/package.json +25 -18
- package/bundles/index.esm.js +0 -487
- package/bundles/index.js +0 -496
- package/rollup-d.config.ts +0 -14
package/README.md
CHANGED
|
@@ -1,58 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
================================
|
|
1
|
+
# @viewfly/platform-browser
|
|
3
2
|
|
|
4
|
-
Viewfly
|
|
3
|
+
在**浏览器**中运行 Viewfly 应用的入口包:提供根应用的创建、挂载、销毁,以及子应用(`fork`)、Portal 等与 DOM 相关的能力。
|
|
4
|
+
|
|
5
|
+
使用前请已安装并配置好 **`@viewfly/core`**(含 JSX 与 `reflect-metadata` 等约定)。
|
|
6
|
+
|
|
7
|
+
---
|
|
5
8
|
|
|
6
9
|
## 安装
|
|
7
10
|
|
|
8
|
-
```
|
|
9
|
-
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @viewfly/platform-browser @viewfly/core
|
|
10
13
|
```
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
---
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
## 创建并挂载应用
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
在 HTML 中准备挂载点,例如:
|
|
17
20
|
|
|
18
|
-
```
|
|
21
|
+
```html
|
|
22
|
+
<div id="app"></div>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
在脚本中:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
19
28
|
import { createApp } from '@viewfly/platform-browser'
|
|
20
29
|
|
|
21
30
|
function App() {
|
|
22
|
-
return () =>
|
|
23
|
-
return <div>App!</div>
|
|
24
|
-
}
|
|
31
|
+
return () => <div>Hello Viewfly</div>
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
const app = createApp(<App/>)
|
|
28
|
-
app.mount(document.getElementById('app'))
|
|
34
|
+
const app = createApp(<App />)
|
|
35
|
+
app.mount(document.getElementById('app')!)
|
|
29
36
|
|
|
30
|
-
//
|
|
37
|
+
// 需要时卸载并清理
|
|
31
38
|
app.destroy()
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
---
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
具体 API 以类型定义与官网说明为准。
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// 启动子应用
|
|
48
|
-
childApp.mount(document.getElementById('modal'))
|
|
49
|
-
// 销毁子应用
|
|
50
|
-
childApp.destroy()
|
|
51
|
-
|
|
52
|
-
return () => {
|
|
53
|
-
return <div>App!</div>
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 文档
|
|
48
|
+
|
|
49
|
+
- **官方文档**:[viewfly.org](https://viewfly.org)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## License
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ViewFlyNode, Application, Config, NativeNode } from '@viewfly/core';
|
|
2
|
+
/**
|
|
3
|
+
* 创建一个 Viewfly 实例
|
|
4
|
+
* @param root 应用根节点
|
|
5
|
+
* @param autoUpdate 是否自动更新视图,默认为 true,当值为 false 时,Viewfly
|
|
6
|
+
* 只会首次渲染,直到手动调用 app 的 render() 方法,这在单元测试中非常有用,
|
|
7
|
+
* 我们无需等待 Viewfly 默认的异步调度,实现同步更新视图
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const app = createApp(<App/>, false).mount(document.getElementById('app'))
|
|
10
|
+
*
|
|
11
|
+
* // do something...
|
|
12
|
+
*
|
|
13
|
+
* app.render() // 手动更新视图
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function createApp<T extends NativeNode>(root: ViewFlyNode, autoUpdate?: boolean): Application<T>;
|
|
17
|
+
export declare function createApp<T extends NativeNode>(root: ViewFlyNode, config?: Partial<Omit<Config, 'root'>>): Application<T>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSXNode, NativeNode } from '@viewfly/core';
|
|
2
|
+
/**
|
|
3
|
+
* 用于创建脱离当前 DOM 树的子节点,常用于弹窗等
|
|
4
|
+
* @deprecated 即将弃用,请使用 @viewfly/core 模块的 Portal 组件实现
|
|
5
|
+
* @param childRender
|
|
6
|
+
* @param host
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* function App() {
|
|
10
|
+
* const number = createSignal(0)
|
|
11
|
+
*
|
|
12
|
+
* setInterval(() => {
|
|
13
|
+
* number.set(number() + 1)
|
|
14
|
+
* }, 1000)
|
|
15
|
+
*
|
|
16
|
+
* const ModalPortal = function (props) {
|
|
17
|
+
* return createPortal(() => {
|
|
18
|
+
* return <div class="modal">parent data is {props.text}</div>
|
|
19
|
+
* }, document.body)
|
|
20
|
+
* }
|
|
21
|
+
* return () => {
|
|
22
|
+
* return (
|
|
23
|
+
* <div>
|
|
24
|
+
* <div>data is {number()}</div>
|
|
25
|
+
* <ModalPortal text={number()}/>
|
|
26
|
+
* </div>
|
|
27
|
+
* )
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function createPortal<T extends NativeNode>(childRender: () => JSXNode, host: T): {
|
|
33
|
+
$portalHost: T;
|
|
34
|
+
$render: () => JSXNode;
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ElementNamespace, NativeRenderer } from '@viewfly/core';
|
|
2
|
+
export declare class DomRenderer extends NativeRenderer<HTMLElement, Text> {
|
|
3
|
+
static NAMESPACES: Record<string, string>;
|
|
4
|
+
propMap: Record<string, Record<string, string>>;
|
|
5
|
+
/**
|
|
6
|
+
* IDL 属性赋 `''` 会被转成数字 0(如 maxLength/minLength),无法表示「未设置」。
|
|
7
|
+
* 这些键在移除时应删掉对应 content attribute。
|
|
8
|
+
*/
|
|
9
|
+
private static readonly REMOVE_VIA_ATTRIBUTE;
|
|
10
|
+
createElement(name: string, namespace: ElementNamespace): HTMLElement;
|
|
11
|
+
createTextNode(textContent: string): Text;
|
|
12
|
+
appendChild(parent: HTMLElement, newChild: any): void;
|
|
13
|
+
prependChild(parent: HTMLElement, newChild: HTMLElement | Text): void;
|
|
14
|
+
insertAfter(newNode: HTMLElement | Text, ref: HTMLElement | Text): void;
|
|
15
|
+
remove(node: HTMLElement | Text): void;
|
|
16
|
+
cleanChildren(node: HTMLElement): void;
|
|
17
|
+
setProperty(node: HTMLElement, key: string, value: any, namespace: ElementNamespace): void;
|
|
18
|
+
removeProperty(node: HTMLElement, key: string, namespace: ElementNamespace): void;
|
|
19
|
+
setClass(target: HTMLElement, className: string): void;
|
|
20
|
+
setStyle(target: HTMLElement, key: string, value: any): void;
|
|
21
|
+
removeStyle(target: HTMLElement, key: string): void;
|
|
22
|
+
listen<T = any>(node: HTMLElement, type: string, callback: (ev: T) => any): void;
|
|
23
|
+
unListen(node: HTMLElement, type: string, callback: (ev: any) => any): void;
|
|
24
|
+
syncTextContent(target: Text, content: string): void;
|
|
25
|
+
getNameSpace(type: string, namespace: ElementNamespace): string | void;
|
|
26
|
+
private normalizedEventType;
|
|
27
|
+
private insertBefore;
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NativeRenderer } from '@viewfly/core';
|
|
2
|
+
export declare class VDOMNode {
|
|
3
|
+
parent: VDOMElement | null;
|
|
4
|
+
remove(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class VDOMElement extends VDOMNode {
|
|
7
|
+
name: string;
|
|
8
|
+
props: Map<string, any>;
|
|
9
|
+
children: Array<VDOMElement | VDOMText>;
|
|
10
|
+
style: Map<string, any>;
|
|
11
|
+
className: string;
|
|
12
|
+
constructor(name: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class VDOMText extends VDOMNode {
|
|
15
|
+
text: string;
|
|
16
|
+
constructor(text: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 用于生成模拟轻量 DOM 节点的渲染器
|
|
20
|
+
*/
|
|
21
|
+
export declare class HTMLRenderer extends NativeRenderer<VDOMElement, VDOMText> {
|
|
22
|
+
createElement(name: string): VDOMElement;
|
|
23
|
+
createTextNode(textContent: string): VDOMText;
|
|
24
|
+
setProperty(node: VDOMElement, key: string, value: any): void;
|
|
25
|
+
appendChild(parent: VDOMElement, newChild: VDOMElement | VDOMText): void;
|
|
26
|
+
prependChild(parent: VDOMElement, newChild: VDOMElement | VDOMText): void;
|
|
27
|
+
removeProperty(node: VDOMElement, key: string): void;
|
|
28
|
+
setStyle(target: VDOMElement, key: string, value: any): void;
|
|
29
|
+
removeStyle(target: VDOMElement, key: string): void;
|
|
30
|
+
setClass(target: VDOMElement, value: string): void;
|
|
31
|
+
listen(): void;
|
|
32
|
+
unListen(): void;
|
|
33
|
+
remove(node: VDOMElement | VDOMText): void;
|
|
34
|
+
cleanChildren(node: VDOMElement): void;
|
|
35
|
+
syncTextContent(target: VDOMText, content: string): void;
|
|
36
|
+
insertAfter(newNode: VDOMElement | VDOMText, ref: VDOMElement | VDOMText): void;
|
|
37
|
+
getNameSpace(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 轻量 DOM 转换为 HTML 字符串的转换器
|
|
41
|
+
*/
|
|
42
|
+
export declare class OutputTranslator {
|
|
43
|
+
static singleTags: string[];
|
|
44
|
+
static simpleXSSFilter: {
|
|
45
|
+
text(text: string): string;
|
|
46
|
+
attrName(text: string): string;
|
|
47
|
+
attrValue(text: string): string;
|
|
48
|
+
};
|
|
49
|
+
private singleTagTest;
|
|
50
|
+
/**
|
|
51
|
+
* 将虚拟 DOM 转换为 HTML 字符串的方法
|
|
52
|
+
* @param vDom 虚拟 DOM 节点
|
|
53
|
+
*/
|
|
54
|
+
transform(vDom: VDOMElement): string;
|
|
55
|
+
private vDomToHTMLString;
|
|
56
|
+
private replaceEmpty;
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { NativeRenderer, viewfly } from "@viewfly/core";
|
|
2
|
+
//#region src/dom-renderer.ts
|
|
3
|
+
var DomRenderer = class DomRenderer extends NativeRenderer {
|
|
4
|
+
static NAMESPACES = {
|
|
5
|
+
svg: "http://www.w3.org/2000/svg",
|
|
6
|
+
html: "http://www.w3.org/1999/xhtml",
|
|
7
|
+
xml: "http://www.w3.org/XML/1998/namespace",
|
|
8
|
+
xlink: "http://www.w3.org/1999/xlink",
|
|
9
|
+
xmlns: "http://www.w3.org/2000/xmlns/",
|
|
10
|
+
mathml: "http://www.w3.org/1998/Math/MathML"
|
|
11
|
+
};
|
|
12
|
+
propMap = {
|
|
13
|
+
INPUT: { readonly: "readOnly" },
|
|
14
|
+
TEXTAREA: { readonly: "readOnly" }
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* IDL 属性赋 `''` 会被转成数字 0(如 maxLength/minLength),无法表示「未设置」。
|
|
18
|
+
* 这些键在移除时应删掉对应 content attribute。
|
|
19
|
+
*/
|
|
20
|
+
static REMOVE_VIA_ATTRIBUTE = {
|
|
21
|
+
maxLength: "maxlength",
|
|
22
|
+
minLength: "minlength",
|
|
23
|
+
size: "size",
|
|
24
|
+
cols: "cols",
|
|
25
|
+
rows: "rows",
|
|
26
|
+
tabIndex: "tabindex"
|
|
27
|
+
};
|
|
28
|
+
createElement(name, namespace) {
|
|
29
|
+
const ns = namespace && DomRenderer.NAMESPACES[namespace];
|
|
30
|
+
if (ns) return document.createElementNS(ns, name);
|
|
31
|
+
return document.createElement(name);
|
|
32
|
+
}
|
|
33
|
+
createTextNode(textContent) {
|
|
34
|
+
return document.createTextNode(textContent);
|
|
35
|
+
}
|
|
36
|
+
appendChild(parent, newChild) {
|
|
37
|
+
parent.appendChild(newChild);
|
|
38
|
+
}
|
|
39
|
+
prependChild(parent, newChild) {
|
|
40
|
+
parent.prepend(newChild);
|
|
41
|
+
}
|
|
42
|
+
insertAfter(newNode, ref) {
|
|
43
|
+
if (ref.nextSibling) this.insertBefore(newNode, ref.nextSibling);
|
|
44
|
+
else if (ref.parentNode) this.appendChild(ref.parentNode, newNode);
|
|
45
|
+
else console.warn(`Element "${ref instanceof Text ? ref.textContent : ref.tagName}" was accidentally deleted, and viewfly is unable to update the current view`);
|
|
46
|
+
}
|
|
47
|
+
remove(node) {
|
|
48
|
+
node.remove();
|
|
49
|
+
}
|
|
50
|
+
cleanChildren(node) {
|
|
51
|
+
node.textContent = "";
|
|
52
|
+
}
|
|
53
|
+
setProperty(node, key, value, namespace) {
|
|
54
|
+
if (value == null) {
|
|
55
|
+
this.removeProperty(node, key, namespace);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (namespace) {
|
|
59
|
+
if (key.startsWith("xlink:")) {
|
|
60
|
+
const ns = key.substring(6);
|
|
61
|
+
node.setAttributeNS(ns, key, String(value));
|
|
62
|
+
} else node.setAttribute(key, String(value));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const map = this.propMap[node.tagName];
|
|
66
|
+
if (map) key = map[key] || key;
|
|
67
|
+
if (key in node) {
|
|
68
|
+
if (map && document.activeElement === node && key === "value") return;
|
|
69
|
+
node[key] = value;
|
|
70
|
+
} else node.setAttribute(key, value);
|
|
71
|
+
}
|
|
72
|
+
removeProperty(node, key, namespace) {
|
|
73
|
+
if (namespace) {
|
|
74
|
+
if (key.startsWith("xlink:")) {
|
|
75
|
+
const ns = key.substring(6);
|
|
76
|
+
node.removeAttributeNS(ns, key.substring(6));
|
|
77
|
+
} else node.removeAttribute(key);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const map = this.propMap[node.tagName];
|
|
81
|
+
const resolvedKey = map ? map[key] || key : key;
|
|
82
|
+
const attrName = DomRenderer.REMOVE_VIA_ATTRIBUTE[resolvedKey];
|
|
83
|
+
if (attrName) {
|
|
84
|
+
node.removeAttribute(attrName);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (resolvedKey in node) node[resolvedKey] = "";
|
|
88
|
+
else node.removeAttribute(key);
|
|
89
|
+
}
|
|
90
|
+
setClass(target, className) {
|
|
91
|
+
target.setAttribute("class", className);
|
|
92
|
+
}
|
|
93
|
+
setStyle(target, key, value) {
|
|
94
|
+
if (key.startsWith("--")) {
|
|
95
|
+
target.style.setProperty(key, value);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
target.style[key] = value ?? "";
|
|
99
|
+
}
|
|
100
|
+
removeStyle(target, key) {
|
|
101
|
+
if (key.startsWith("--")) {
|
|
102
|
+
target.style.removeProperty(key);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
target.style[key] = "";
|
|
106
|
+
}
|
|
107
|
+
listen(node, type, callback) {
|
|
108
|
+
const normalizedType = this.normalizedEventType(type);
|
|
109
|
+
node.addEventListener(normalizedType, callback);
|
|
110
|
+
}
|
|
111
|
+
unListen(node, type, callback) {
|
|
112
|
+
const normalizedType = this.normalizedEventType(type);
|
|
113
|
+
node.removeEventListener(normalizedType, callback);
|
|
114
|
+
}
|
|
115
|
+
syncTextContent(target, content) {
|
|
116
|
+
target.textContent = content;
|
|
117
|
+
}
|
|
118
|
+
getNameSpace(type, namespace) {
|
|
119
|
+
if (namespace === "svg") {
|
|
120
|
+
if (type === "foreignObject") return;
|
|
121
|
+
return namespace;
|
|
122
|
+
}
|
|
123
|
+
if (type === "svg") return type;
|
|
124
|
+
if (type === "math") return "mathml";
|
|
125
|
+
return namespace;
|
|
126
|
+
}
|
|
127
|
+
normalizedEventType(type) {
|
|
128
|
+
return type.substring(2).toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
insertBefore(newNode, ref) {
|
|
131
|
+
if (ref.parentNode) ref.parentNode.insertBefore(newNode, ref);
|
|
132
|
+
else console.warn(`Element "${ref instanceof Text ? ref.textContent : ref.tagName}" was accidentally deleted, and viewfly is unable to update the current view`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/create-app.ts
|
|
137
|
+
function createApp(root, config = true) {
|
|
138
|
+
const c = { autoUpdate: true };
|
|
139
|
+
if (typeof config === "boolean") c.autoUpdate = config;
|
|
140
|
+
else if (typeof config === "object") Object.assign(c, config);
|
|
141
|
+
return viewfly({
|
|
142
|
+
...c,
|
|
143
|
+
root,
|
|
144
|
+
nativeRenderer: c.nativeRenderer || new DomRenderer()
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/create-portal.ts
|
|
149
|
+
/**
|
|
150
|
+
* 用于创建脱离当前 DOM 树的子节点,常用于弹窗等
|
|
151
|
+
* @deprecated 即将弃用,请使用 @viewfly/core 模块的 Portal 组件实现
|
|
152
|
+
* @param childRender
|
|
153
|
+
* @param host
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* function App() {
|
|
157
|
+
* const number = createSignal(0)
|
|
158
|
+
*
|
|
159
|
+
* setInterval(() => {
|
|
160
|
+
* number.set(number() + 1)
|
|
161
|
+
* }, 1000)
|
|
162
|
+
*
|
|
163
|
+
* const ModalPortal = function (props) {
|
|
164
|
+
* return createPortal(() => {
|
|
165
|
+
* return <div class="modal">parent data is {props.text}</div>
|
|
166
|
+
* }, document.body)
|
|
167
|
+
* }
|
|
168
|
+
* return () => {
|
|
169
|
+
* return (
|
|
170
|
+
* <div>
|
|
171
|
+
* <div>data is {number()}</div>
|
|
172
|
+
* <ModalPortal text={number()}/>
|
|
173
|
+
* </div>
|
|
174
|
+
* )
|
|
175
|
+
* }
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
function createPortal(childRender, host) {
|
|
180
|
+
return {
|
|
181
|
+
$portalHost: host,
|
|
182
|
+
$render: childRender
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/html-renderer.ts
|
|
187
|
+
var VDOMNode = class {
|
|
188
|
+
parent = null;
|
|
189
|
+
remove() {
|
|
190
|
+
if (this.parent) {
|
|
191
|
+
const i = this.parent.children.indexOf(this);
|
|
192
|
+
if (i > -1) this.parent.children.splice(i, 1);
|
|
193
|
+
}
|
|
194
|
+
this.parent = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var VDOMElement = class extends VDOMNode {
|
|
198
|
+
props = /* @__PURE__ */ new Map();
|
|
199
|
+
children = [];
|
|
200
|
+
style = /* @__PURE__ */ new Map();
|
|
201
|
+
className = "";
|
|
202
|
+
constructor(name) {
|
|
203
|
+
super();
|
|
204
|
+
this.name = name;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var VDOMText = class extends VDOMNode {
|
|
208
|
+
constructor(text) {
|
|
209
|
+
super();
|
|
210
|
+
this.text = text;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* 用于生成模拟轻量 DOM 节点的渲染器
|
|
215
|
+
*/
|
|
216
|
+
var HTMLRenderer = class extends NativeRenderer {
|
|
217
|
+
createElement(name) {
|
|
218
|
+
return new VDOMElement(name);
|
|
219
|
+
}
|
|
220
|
+
createTextNode(textContent) {
|
|
221
|
+
return new VDOMText(textContent);
|
|
222
|
+
}
|
|
223
|
+
setProperty(node, key, value) {
|
|
224
|
+
node.props.set(key, value);
|
|
225
|
+
}
|
|
226
|
+
appendChild(parent, newChild) {
|
|
227
|
+
newChild.remove();
|
|
228
|
+
parent.children.push(newChild);
|
|
229
|
+
newChild.parent = parent;
|
|
230
|
+
}
|
|
231
|
+
prependChild(parent, newChild) {
|
|
232
|
+
newChild.remove();
|
|
233
|
+
parent.children.unshift(newChild);
|
|
234
|
+
newChild.parent = parent;
|
|
235
|
+
}
|
|
236
|
+
removeProperty(node, key) {
|
|
237
|
+
node.props.delete(key);
|
|
238
|
+
}
|
|
239
|
+
setStyle(target, key, value) {
|
|
240
|
+
target.style.set(key, value);
|
|
241
|
+
}
|
|
242
|
+
removeStyle(target, key) {
|
|
243
|
+
target.style.delete(key);
|
|
244
|
+
}
|
|
245
|
+
setClass(target, value) {
|
|
246
|
+
target.className = value;
|
|
247
|
+
}
|
|
248
|
+
listen() {}
|
|
249
|
+
unListen() {}
|
|
250
|
+
remove(node) {
|
|
251
|
+
node.remove();
|
|
252
|
+
}
|
|
253
|
+
cleanChildren(node) {
|
|
254
|
+
node.children.forEach((i) => i.parent = null);
|
|
255
|
+
node.children = [];
|
|
256
|
+
}
|
|
257
|
+
syncTextContent(target, content) {
|
|
258
|
+
target.text = content;
|
|
259
|
+
}
|
|
260
|
+
insertAfter(newNode, ref) {
|
|
261
|
+
newNode.remove();
|
|
262
|
+
const parent = ref.parent;
|
|
263
|
+
if (parent) {
|
|
264
|
+
const i = parent.children.indexOf(ref);
|
|
265
|
+
if (i > -1) {
|
|
266
|
+
newNode.parent = parent;
|
|
267
|
+
parent.children.splice(i + 1, 0, newNode);
|
|
268
|
+
}
|
|
269
|
+
} else console.warn(`Element "${ref instanceof VDOMText ? ref.text : ref.name}" was accidentally deleted, and viewfly is unable to update the current view`);
|
|
270
|
+
}
|
|
271
|
+
getNameSpace() {}
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* 轻量 DOM 转换为 HTML 字符串的转换器
|
|
275
|
+
*/
|
|
276
|
+
var OutputTranslator = class OutputTranslator {
|
|
277
|
+
static singleTags = "area,base,br,col,embed,hr,img,input,link,meta,source,track,wbr".split(",");
|
|
278
|
+
static simpleXSSFilter = {
|
|
279
|
+
text(text) {
|
|
280
|
+
return text.replace(/[><&]/g, (str) => {
|
|
281
|
+
return {
|
|
282
|
+
"<": "<",
|
|
283
|
+
">": ">",
|
|
284
|
+
"&": "&"
|
|
285
|
+
}[str];
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
attrName(text) {
|
|
289
|
+
return text.replace(/[><"'&]/g, (str) => {
|
|
290
|
+
return {
|
|
291
|
+
"<": "<",
|
|
292
|
+
">": ">",
|
|
293
|
+
"\"": """,
|
|
294
|
+
"'": "'",
|
|
295
|
+
"&": "&"
|
|
296
|
+
}[str];
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
attrValue(text) {
|
|
300
|
+
return text.replace(/["']/g, (str) => {
|
|
301
|
+
return {
|
|
302
|
+
"\"": """,
|
|
303
|
+
"'": "'"
|
|
304
|
+
}[str];
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
singleTagTest = new RegExp(`^(${OutputTranslator.singleTags.join("|")})$`, "i");
|
|
309
|
+
/**
|
|
310
|
+
* 将虚拟 DOM 转换为 HTML 字符串的方法
|
|
311
|
+
* @param vDom 虚拟 DOM 节点
|
|
312
|
+
*/
|
|
313
|
+
transform(vDom) {
|
|
314
|
+
return vDom.children.map((child) => {
|
|
315
|
+
return this.vDomToHTMLString(child);
|
|
316
|
+
}).join("");
|
|
317
|
+
}
|
|
318
|
+
vDomToHTMLString(vDom) {
|
|
319
|
+
const xssFilter = OutputTranslator.simpleXSSFilter;
|
|
320
|
+
if (vDom instanceof VDOMText) return this.replaceEmpty(xssFilter.text(vDom.text), " ");
|
|
321
|
+
const styles = Array.from(vDom.style.keys()).filter((key) => {
|
|
322
|
+
const v = vDom.style.get(key);
|
|
323
|
+
return !(v === void 0 || v === null || v === "");
|
|
324
|
+
}).map((key) => {
|
|
325
|
+
const k = key.replace(/(?=[A-Z])/g, "-").toLowerCase();
|
|
326
|
+
return xssFilter.attrValue(`${k}:${vDom.style.get(key)}`);
|
|
327
|
+
}).join(";");
|
|
328
|
+
const attrs = Array.from(vDom.props.keys()).filter((key) => key !== "ref" && vDom.props.get(key) !== false).map((k) => {
|
|
329
|
+
const key = xssFilter.attrName(k);
|
|
330
|
+
const value = vDom.props.get(k);
|
|
331
|
+
return value === true && /^\w+$/.test(key) ? `${key}` : `${key}="${xssFilter.attrValue(`${value}`)}"`;
|
|
332
|
+
});
|
|
333
|
+
if (styles) attrs.push(`style="${styles}"`);
|
|
334
|
+
if (vDom.className) attrs.push(`class="${xssFilter.attrValue(vDom.className)}"`);
|
|
335
|
+
let attrStr = attrs.join(" ");
|
|
336
|
+
attrStr = attrStr ? " " + attrStr : "";
|
|
337
|
+
if (this.singleTagTest.test(vDom.name)) return `<${vDom.name}${attrStr}>`;
|
|
338
|
+
const childHTML = vDom.children.map((child) => {
|
|
339
|
+
return this.vDomToHTMLString(child);
|
|
340
|
+
}).join("");
|
|
341
|
+
return [
|
|
342
|
+
`<${vDom.name}${attrStr}>`,
|
|
343
|
+
childHTML,
|
|
344
|
+
`</${vDom.name}>`
|
|
345
|
+
].join("");
|
|
346
|
+
}
|
|
347
|
+
replaceEmpty(s, target) {
|
|
348
|
+
return s.replace(/\s\s+/g, (str) => {
|
|
349
|
+
return " " + Array.from({ length: str.length - 1 }).fill(target).join("");
|
|
350
|
+
}).replace(/^\s|\s$/g, target);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
export { DomRenderer, HTMLRenderer, OutputTranslator, VDOMElement, VDOMNode, VDOMText, createApp, createPortal };
|