@fulate/vue 1.0.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 +103 -0
- package/dist/app.d.ts +14 -0
- package/dist/context.d.ts +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +510 -0
- package/dist/renderer.d.ts +5 -0
- package/dist/transition.d.ts +74 -0
- package/dist/vue-shape.d.ts +44 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# `@fulate/vue`
|
|
2
|
+
|
|
3
|
+
> 文档角色:`GUIDE`。权威来源:Vue adapter 源码 TSDoc 与测试;Core 场景和生命周期规则见
|
|
4
|
+
> [`@fulate/core` 指南](../core/docs/README.md),跨包 Editor 规则见 [`docs/specs/`](../../docs/README.md)。
|
|
5
|
+
|
|
6
|
+
Vue adapter 把 Vue renderer 接到一个 Fulate `Root`。它不复制 Core 场景状态,也不创建第二个 Root
|
|
7
|
+
frame runtime;每个挂载的应用只拥有自己的 content/overlay Layer 和 Vue 生命周期。
|
|
8
|
+
|
|
9
|
+
## 创建与挂载
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createApp } from "@fulate/vue";
|
|
13
|
+
|
|
14
|
+
const app = createApp(App);
|
|
15
|
+
app.mount(container); // 自动创建并拥有 Root
|
|
16
|
+
// 或:app.mount(root); // 使用已有 Root,Root 仍由调用方拥有
|
|
17
|
+
|
|
18
|
+
app.unmount();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
传入 `HTMLElement`、选择器或已有 `Root`。自动创建的 Root 由 app 释放;传入已有 Root 时,app 只
|
|
22
|
+
释放自己创建的 Layer 和 renderer。一个 Root 只能承载一个 Fulate Vue app;应用不可在 unmount 后
|
|
23
|
+
重新挂载。
|
|
24
|
+
|
|
25
|
+
## VueShape 与自定义元素
|
|
26
|
+
|
|
27
|
+
`VueShape` 在元素进入活动 Root 时挂载 Vue component,离开时卸载。它支持两种互斥的 props 模式。
|
|
28
|
+
|
|
29
|
+
低层 manual 模式通过 `setComponentProps()` 采用 whole-value props,同一引用是 noop。组件可通过
|
|
30
|
+
`registerVueComponent(name, component)` 注册,以便基础 clipboard/file adapter 按名称恢复:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { VueShape, registerVueComponent } from "@fulate/vue";
|
|
34
|
+
|
|
35
|
+
registerVueComponent("Badge", Badge);
|
|
36
|
+
const badge = new VueShape({ width: 120, height: 32 }, { label: "Ready" })
|
|
37
|
+
.bindComponent(Badge);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
mapped 模式使用 `VueShape<P, O>` 和第三个构造参数安装 typed Properties,并在 `bindComponent()`
|
|
41
|
+
时注册一次 properties resolver。resolver 读取包含 configured/runtime 的最终 effective Properties,
|
|
42
|
+
每次返回完整 props;Fulate 通过同一 `setComponentProps()` 路径整体替换:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
class KeyValueShape extends VueShape<KeyValueProperties, KeyValueOption> {
|
|
46
|
+
override readonly type = "key-value";
|
|
47
|
+
|
|
48
|
+
constructor(options: KeyValueOption = {}) {
|
|
49
|
+
super(options, {}, new KeyValueProperties(options));
|
|
50
|
+
this.bindComponent(FKeyValue, (current) => ({
|
|
51
|
+
keyText: current.runtimeKeyText ?? current.keyText,
|
|
52
|
+
valueText: current.runtimeValue ?? current.valueText
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
初始绑定、configured/runtime 更新与清除、属性快照恢复都经过该映射。mapped props 是派生缓存,不写入
|
|
59
|
+
JSON、Clipboard 或 History;mapped 业务类型由其具体 type factory 恢复。manual 模式仍保存组件名称和
|
|
60
|
+
手动 component props,`fromVueToFulate()` 与基础 `vueClipboardPlugin` 的行为不变。
|
|
61
|
+
|
|
62
|
+
每个 `VueShape` 都拥有独立 Vue app。组件可在 `setup()` 中直接通过 `useFulateHost()` 取得当前宿主;
|
|
63
|
+
派生类型可作为泛型参数保留具体类型:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { useFulateHost } from "@fulate/vue";
|
|
67
|
+
|
|
68
|
+
const shape = useFulateHost<KeyValueShape>();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
该注入只存在于 `VueShape` 创建的独立 app;外层 Editor Vue app 的 `provide()` 不会自动穿透。Root、
|
|
72
|
+
overlay 和 size 上下文仍由每个独立 app 分别提供。
|
|
73
|
+
|
|
74
|
+
自定义 Canvas 元素通过 `registerElement("f-widget", Widget)` 注册后,才能在 Vue template 中使用
|
|
75
|
+
`<f-widget />`。未知元素会在 renderer 创建时自然抛错,不另设兼容路径。
|
|
76
|
+
|
|
77
|
+
## Root 与 overlay
|
|
78
|
+
|
|
79
|
+
`createApp()` 会建立内容层和 overlay 层,并通过 `FULATE_ROOT_KEY`、`FULATE_OVERLAY_KEY` 注入给
|
|
80
|
+
Vue 组件。手工管理 Root 时,可用 `registerVueOverlay(root, layer)` 为独立 `VueShape` 提供浮层层;
|
|
81
|
+
返回的 cleanup 必须在场景销毁时调用。
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const unregister = registerVueOverlay(root, overlayLayer);
|
|
85
|
+
// ... mount VueShape / use overlay composables
|
|
86
|
+
unregister();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
组件尺寸可通过 `useVueShapeSize()` 读取。过渡动画使用 `FTransition`,其动画仍通过元素的正常
|
|
90
|
+
`animate()` 与 Root frame 路径完成。`registerVueOverlay()` 和组件 overlay 定位的 cleanup 绑定
|
|
91
|
+
所属 Root 生命周期;`useOverlayPosition()` 的异步结果只在 task、引用和挂载状态仍有效时安装。
|
|
92
|
+
|
|
93
|
+
## Clipboard
|
|
94
|
+
|
|
95
|
+
`vueClipboardPlugin` 是按 `Select` 实例显式安装的 clipboard adapter:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const select = new Select().useClipboard(vueClipboardPlugin);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
基础 manual `VueShape` 的复制 payload 保存组件名称、props 和必要元素数据,组件必须在恢复前完成
|
|
102
|
+
注册。mapped 自定义类型不保存组件名称或派生 props,由业务 type factory 恢复。该 adapter 不注册
|
|
103
|
+
全局 Select 行为,也不读取旧格式。
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Root } from "@fulate/core";
|
|
2
|
+
import { baseCreateApp } from "./renderer";
|
|
3
|
+
import type { Component } from "@vue/runtime-core";
|
|
4
|
+
export interface FulateAppMountOptions {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
type BaseApp = ReturnType<typeof baseCreateApp>;
|
|
9
|
+
export type FulateApp = Omit<BaseApp, "mount"> & {
|
|
10
|
+
readonly root: Root | null;
|
|
11
|
+
mount(rootOrEl: Root | HTMLElement | string, options?: FulateAppMountOptions): ReturnType<BaseApp["mount"]>;
|
|
12
|
+
};
|
|
13
|
+
export declare function createApp(rootComponent: Component, rootProps?: Record<string, unknown>): FulateApp;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Layer, Root } from "@fulate/core";
|
|
2
|
+
import { type InjectionKey } from "@vue/runtime-core";
|
|
3
|
+
import type { VueShape } from "./vue-shape";
|
|
4
|
+
export declare const FULATE_ROOT_KEY = "__fulate_root";
|
|
5
|
+
export declare const FULATE_OVERLAY_KEY = "__fulate_overlay";
|
|
6
|
+
export declare const VUE_SHAPE_SIZE_KEY = "__vueShapeSize";
|
|
7
|
+
export declare const FULATE_HOST_KEY: InjectionKey<VueShape>;
|
|
8
|
+
/** 读取当前独立 VueShape app 提供的 Fulate 宿主。 */
|
|
9
|
+
export declare function useFulateHost<T extends VueShape = VueShape>(): T;
|
|
10
|
+
export interface FulateRootLayers {
|
|
11
|
+
content: Layer;
|
|
12
|
+
overlay: Layer;
|
|
13
|
+
}
|
|
14
|
+
export declare function setFulateRootLayers(root: Root, layers: FulateRootLayers): void;
|
|
15
|
+
export declare function getFulateRootLayers(root: Root): FulateRootLayers;
|
|
16
|
+
export declare function getFulateOverlay(root: Root): Layer<import("@fulate/core").LayerProperties, import("@fulate/core").LayerOption>;
|
|
17
|
+
export declare function registerVueOverlay(root: Root, overlay: Layer): () => void;
|
|
18
|
+
export declare function clearFulateRootLayers(root: Root, layers: FulateRootLayers): void;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { registerElement, getRegisteredElement } from "./renderer";
|
|
2
|
+
export type { ElementConstructor } from "./renderer";
|
|
3
|
+
export { createApp } from "./app";
|
|
4
|
+
export type { FulateApp, FulateAppMountOptions } from "./app";
|
|
5
|
+
export { FULATE_HOST_KEY, FULATE_OVERLAY_KEY, FULATE_ROOT_KEY, VUE_SHAPE_SIZE_KEY, registerVueOverlay, useFulateHost } from "./context";
|
|
6
|
+
export { FTransition } from "./transition";
|
|
7
|
+
export { VueShape, fromVueToFulate, registerVueComponent, getVueComponent, vueClipboardPlugin } from "./vue-shape";
|
|
8
|
+
export type { VueComponentPropsResolver, VueShapeJSON } from "./vue-shape";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { BaseTransition, createRenderer, defineComponent, getCurrentInstance, h, inject, markRaw, mergeProps, onBeforeUnmount, reactive, shallowRef } from "@vue/runtime-core";
|
|
2
|
+
import { Element, Layer, Root, Shape } from "@fulate/core";
|
|
3
|
+
import { isFunction, isString, isUndefined } from "lodash-es";
|
|
4
|
+
import { Circle, Group, Image, Line, LineTree, Polygon, Rectangle, RectangleProperties, RectangleTransform, RippleOverlay, ScrollView, Text, Triangle, Workspace } from "@fulate/ui";
|
|
5
|
+
//#region packages/vue/src/renderer.ts
|
|
6
|
+
var elementConstructors = new Map([
|
|
7
|
+
["f-element", Element],
|
|
8
|
+
["f-shape", Shape],
|
|
9
|
+
["f-rectangle", Rectangle],
|
|
10
|
+
["f-circle", Circle],
|
|
11
|
+
["f-triangle", Triangle],
|
|
12
|
+
["f-polygon", Polygon],
|
|
13
|
+
["f-text", Text],
|
|
14
|
+
["f-image", Image],
|
|
15
|
+
["f-workspace", Workspace],
|
|
16
|
+
["f-group", Group],
|
|
17
|
+
["f-scrollview", ScrollView],
|
|
18
|
+
["f-scroll-view", ScrollView],
|
|
19
|
+
["f-line", Line],
|
|
20
|
+
["f-line-tree", LineTree],
|
|
21
|
+
["f-ripple", RippleOverlay]
|
|
22
|
+
]);
|
|
23
|
+
function registerElement(type, Constructor) {
|
|
24
|
+
elementConstructors.set(type, Constructor);
|
|
25
|
+
}
|
|
26
|
+
function getRegisteredElement(type) {
|
|
27
|
+
return elementConstructors.get(type);
|
|
28
|
+
}
|
|
29
|
+
function isEventProp(key) {
|
|
30
|
+
return /^on[A-Z]/.test(key);
|
|
31
|
+
}
|
|
32
|
+
function toEventName(key) {
|
|
33
|
+
return key.slice(2).toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
function forEachEventHandler(value, callback) {
|
|
36
|
+
if (Array.isArray(value)) value.forEach((item) => forEachEventHandler(item, callback));
|
|
37
|
+
else if (isFunction(value)) callback(value);
|
|
38
|
+
}
|
|
39
|
+
function createPlaceholder() {
|
|
40
|
+
const el = new Element({
|
|
41
|
+
width: 0,
|
|
42
|
+
height: 0,
|
|
43
|
+
silent: true,
|
|
44
|
+
pickable: false
|
|
45
|
+
});
|
|
46
|
+
placeholders.add(el);
|
|
47
|
+
return el;
|
|
48
|
+
}
|
|
49
|
+
var placeholders = /* @__PURE__ */ new WeakSet();
|
|
50
|
+
var { render, createApp: baseCreateApp } = createRenderer({
|
|
51
|
+
createElement(type) {
|
|
52
|
+
const Ctor = getRegisteredElement(type);
|
|
53
|
+
if (!Ctor) throw new Error(`[fulate-vue] Unknown element "${type}". Register it before mounting the app.`);
|
|
54
|
+
return markRaw(new Ctor());
|
|
55
|
+
},
|
|
56
|
+
insert(child, parent, anchor) {
|
|
57
|
+
if (!parent) return;
|
|
58
|
+
if (anchor) parent.insertBefore(child, anchor);
|
|
59
|
+
else parent.append(child);
|
|
60
|
+
},
|
|
61
|
+
remove(child) {
|
|
62
|
+
child.unmount();
|
|
63
|
+
},
|
|
64
|
+
patchProp(el, key, prevValue, nextValue) {
|
|
65
|
+
if (placeholders.has(el)) return;
|
|
66
|
+
if (isEventProp(key)) {
|
|
67
|
+
const eventName = toEventName(key);
|
|
68
|
+
forEachEventHandler(prevValue, (handler) => el.removeEventListener(eventName, handler));
|
|
69
|
+
forEachEventHandler(nextValue, (handler) => el.addEventListener(eventName, handler));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
el.setOptions({ [key]: nextValue });
|
|
73
|
+
},
|
|
74
|
+
createText() {
|
|
75
|
+
return createPlaceholder();
|
|
76
|
+
},
|
|
77
|
+
createComment() {
|
|
78
|
+
return createPlaceholder();
|
|
79
|
+
},
|
|
80
|
+
setText() {},
|
|
81
|
+
setElementText(el, text) {
|
|
82
|
+
if (placeholders.has(el)) return;
|
|
83
|
+
if (el.type === "text") el.setOptions({ text });
|
|
84
|
+
},
|
|
85
|
+
parentNode(node) {
|
|
86
|
+
return node?.parent ?? null;
|
|
87
|
+
},
|
|
88
|
+
nextSibling(node) {
|
|
89
|
+
return node?.nextSibling ?? null;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region packages/vue/src/context.ts
|
|
94
|
+
var FULATE_ROOT_KEY = "__fulate_root";
|
|
95
|
+
var FULATE_OVERLAY_KEY = "__fulate_overlay";
|
|
96
|
+
var VUE_SHAPE_SIZE_KEY = "__vueShapeSize";
|
|
97
|
+
var FULATE_HOST_KEY = Symbol("fulate-host");
|
|
98
|
+
/** 读取当前独立 VueShape app 提供的 Fulate 宿主。 */
|
|
99
|
+
function useFulateHost() {
|
|
100
|
+
return inject(FULATE_HOST_KEY);
|
|
101
|
+
}
|
|
102
|
+
var rootLayers = /* @__PURE__ */ new WeakMap();
|
|
103
|
+
var standaloneOverlays = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
function setFulateRootLayers(root, layers) {
|
|
105
|
+
rootLayers.set(root, layers);
|
|
106
|
+
}
|
|
107
|
+
function getFulateRootLayers(root) {
|
|
108
|
+
return rootLayers.get(root);
|
|
109
|
+
}
|
|
110
|
+
function getFulateOverlay(root) {
|
|
111
|
+
return rootLayers.get(root)?.overlay ?? standaloneOverlays.get(root);
|
|
112
|
+
}
|
|
113
|
+
function registerVueOverlay(root, overlay) {
|
|
114
|
+
if (root.isUnmounted) throw new Error("[fulate-vue] cannot register an overlay on an unmounted Root");
|
|
115
|
+
standaloneOverlays.set(root, overlay);
|
|
116
|
+
let removeRootListener = () => {};
|
|
117
|
+
const unregister = () => {
|
|
118
|
+
if (standaloneOverlays.get(root) === overlay) standaloneOverlays.delete(root);
|
|
119
|
+
removeRootListener();
|
|
120
|
+
};
|
|
121
|
+
removeRootListener = root.addEventListener("unmounted", unregister);
|
|
122
|
+
return unregister;
|
|
123
|
+
}
|
|
124
|
+
function clearFulateRootLayers(root, layers) {
|
|
125
|
+
if (rootLayers.get(root) === layers) rootLayers.delete(root);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region packages/vue/src/app.ts
|
|
129
|
+
function createApp(rootComponent, rootProps) {
|
|
130
|
+
const app = baseCreateApp(rootComponent, rootProps);
|
|
131
|
+
const originalMount = app.mount.bind(app);
|
|
132
|
+
const originalUnmount = app.unmount.bind(app);
|
|
133
|
+
let mounted = null;
|
|
134
|
+
let disposing = false;
|
|
135
|
+
let hasUnmounted = false;
|
|
136
|
+
let rendererMounted = false;
|
|
137
|
+
function resolveRoot(rootOrEl, options) {
|
|
138
|
+
if (rootOrEl instanceof Root) return {
|
|
139
|
+
root: rootOrEl,
|
|
140
|
+
ownsRoot: false
|
|
141
|
+
};
|
|
142
|
+
const element = isString(rootOrEl) ? document.querySelector(rootOrEl) : rootOrEl;
|
|
143
|
+
if (!(element instanceof HTMLElement)) throw new TypeError("[fulate-vue] mount target must be a Root or HTMLElement");
|
|
144
|
+
return {
|
|
145
|
+
root: new Root(element, options),
|
|
146
|
+
ownsRoot: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function dispose(fromRoot = false) {
|
|
150
|
+
if (!mounted || disposing) return;
|
|
151
|
+
disposing = true;
|
|
152
|
+
const state = mounted;
|
|
153
|
+
mounted = null;
|
|
154
|
+
hasUnmounted = true;
|
|
155
|
+
state.removeRootListener();
|
|
156
|
+
try {
|
|
157
|
+
try {
|
|
158
|
+
if (rendererMounted) {
|
|
159
|
+
originalUnmount();
|
|
160
|
+
rendererMounted = false;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
if (!state.content.isUnmounted) state.content.unmount();
|
|
164
|
+
if (!state.overlay.isUnmounted) state.overlay.unmount();
|
|
165
|
+
clearFulateRootLayers(state.root, state);
|
|
166
|
+
if (state.ownsRoot && !fromRoot && !state.root.isUnmounted) state.root.unmount();
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
disposing = false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const mount = (rootOrEl, options) => {
|
|
173
|
+
if (mounted) throw new Error("[fulate-vue] app is already mounted");
|
|
174
|
+
if (hasUnmounted) throw new Error("[fulate-vue] app cannot be remounted after unmount");
|
|
175
|
+
const { root, ownsRoot } = resolveRoot(rootOrEl, options);
|
|
176
|
+
if (root.isUnmounted) throw new Error("[fulate-vue] cannot mount into an unmounted Root");
|
|
177
|
+
if (getFulateRootLayers(root)) throw new Error("[fulate-vue] Root already hosts a Fulate Vue app");
|
|
178
|
+
const content = new Layer({
|
|
179
|
+
zIndex: 1,
|
|
180
|
+
enableDirtyRect: true
|
|
181
|
+
});
|
|
182
|
+
const overlay = new Layer({
|
|
183
|
+
zIndex: 100,
|
|
184
|
+
enableDirtyRect: true
|
|
185
|
+
});
|
|
186
|
+
root.append(content);
|
|
187
|
+
root.append(overlay);
|
|
188
|
+
app.provide(FULATE_OVERLAY_KEY, overlay);
|
|
189
|
+
app.provide(FULATE_ROOT_KEY, root);
|
|
190
|
+
mounted = {
|
|
191
|
+
root,
|
|
192
|
+
ownsRoot,
|
|
193
|
+
content,
|
|
194
|
+
overlay,
|
|
195
|
+
removeRootListener: root.addEventListener("unmounted", () => {
|
|
196
|
+
dispose(true);
|
|
197
|
+
})
|
|
198
|
+
};
|
|
199
|
+
setFulateRootLayers(root, mounted);
|
|
200
|
+
try {
|
|
201
|
+
const instance = originalMount(content);
|
|
202
|
+
rendererMounted = true;
|
|
203
|
+
if (!root.isMounted) root.mount();
|
|
204
|
+
return instance;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
dispose();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
app.mount = mount;
|
|
211
|
+
app.unmount = (() => dispose());
|
|
212
|
+
Object.defineProperty(app, "root", {
|
|
213
|
+
configurable: false,
|
|
214
|
+
enumerable: true,
|
|
215
|
+
get: () => mounted?.root ?? null
|
|
216
|
+
});
|
|
217
|
+
return app;
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region packages/vue/src/transition.ts
|
|
221
|
+
var FTransition = defineComponent({
|
|
222
|
+
name: "FTransition",
|
|
223
|
+
props: {
|
|
224
|
+
appear: {
|
|
225
|
+
type: Boolean,
|
|
226
|
+
default: false
|
|
227
|
+
},
|
|
228
|
+
mode: {
|
|
229
|
+
type: String,
|
|
230
|
+
default: void 0
|
|
231
|
+
},
|
|
232
|
+
enterFrom: {
|
|
233
|
+
type: Object,
|
|
234
|
+
default: void 0
|
|
235
|
+
},
|
|
236
|
+
enterTo: {
|
|
237
|
+
type: Object,
|
|
238
|
+
default: void 0
|
|
239
|
+
},
|
|
240
|
+
leaveTo: {
|
|
241
|
+
type: Object,
|
|
242
|
+
default: void 0
|
|
243
|
+
},
|
|
244
|
+
duration: {
|
|
245
|
+
type: Number,
|
|
246
|
+
default: 300
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
emits: [
|
|
250
|
+
"beforeEnter",
|
|
251
|
+
"enter",
|
|
252
|
+
"afterEnter",
|
|
253
|
+
"enterCancelled",
|
|
254
|
+
"beforeLeave",
|
|
255
|
+
"leave",
|
|
256
|
+
"afterLeave",
|
|
257
|
+
"leaveCancelled",
|
|
258
|
+
"beforeAppear",
|
|
259
|
+
"appear",
|
|
260
|
+
"afterAppear",
|
|
261
|
+
"appearCancelled"
|
|
262
|
+
],
|
|
263
|
+
setup(props, { emit, slots }) {
|
|
264
|
+
const instance = getCurrentInstance();
|
|
265
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
266
|
+
function hasListener(name) {
|
|
267
|
+
const vnodeProps = instance.vnode.props;
|
|
268
|
+
return Boolean(vnodeProps?.[name] || vnodeProps?.[`${name}Once`]);
|
|
269
|
+
}
|
|
270
|
+
function cancelRun(element) {
|
|
271
|
+
const run = activeRuns.get(element);
|
|
272
|
+
if (!run) return null;
|
|
273
|
+
activeRuns.delete(element);
|
|
274
|
+
run.cancel();
|
|
275
|
+
return run.phase;
|
|
276
|
+
}
|
|
277
|
+
function cancelAllRuns() {
|
|
278
|
+
for (const run of activeRuns.values()) run.cancel();
|
|
279
|
+
activeRuns.clear();
|
|
280
|
+
}
|
|
281
|
+
function prepareEnter(element) {
|
|
282
|
+
const { enterFrom, enterTo } = props;
|
|
283
|
+
if (!enterFrom || !enterTo) return;
|
|
284
|
+
const initial = {};
|
|
285
|
+
for (const key of Object.keys(enterTo)) if (Object.hasOwn(enterFrom, key)) initial[key] = enterFrom[key];
|
|
286
|
+
element.setOptions(initial);
|
|
287
|
+
}
|
|
288
|
+
function animate(element, phase, target, done) {
|
|
289
|
+
cancelRun(element);
|
|
290
|
+
if (!target) {
|
|
291
|
+
done();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
let cancelled = false;
|
|
295
|
+
let removeActivatedListener = null;
|
|
296
|
+
let tween = null;
|
|
297
|
+
const run = {
|
|
298
|
+
phase,
|
|
299
|
+
cancel() {
|
|
300
|
+
cancelled = true;
|
|
301
|
+
removeActivatedListener?.();
|
|
302
|
+
removeActivatedListener = null;
|
|
303
|
+
tween?.stop();
|
|
304
|
+
tween = null;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
activeRuns.set(element, run);
|
|
308
|
+
const complete = () => {
|
|
309
|
+
if (cancelled || activeRuns.get(element) !== run) return;
|
|
310
|
+
activeRuns.delete(element);
|
|
311
|
+
done();
|
|
312
|
+
};
|
|
313
|
+
const start = () => {
|
|
314
|
+
removeActivatedListener = null;
|
|
315
|
+
if (cancelled) return;
|
|
316
|
+
tween = element.animate(target, {
|
|
317
|
+
duration: props.duration,
|
|
318
|
+
onComplete: complete
|
|
319
|
+
}).tween;
|
|
320
|
+
};
|
|
321
|
+
if (element.isActive) start();
|
|
322
|
+
else removeActivatedListener = element.addEventListener("activated", start, { once: true });
|
|
323
|
+
}
|
|
324
|
+
onBeforeUnmount(cancelAllRuns);
|
|
325
|
+
return () => {
|
|
326
|
+
const baseProps = {
|
|
327
|
+
appear: props.appear,
|
|
328
|
+
mode: props.mode,
|
|
329
|
+
onBeforeEnter(element) {
|
|
330
|
+
cancelRun(element);
|
|
331
|
+
prepareEnter(element);
|
|
332
|
+
emit("beforeEnter", element);
|
|
333
|
+
},
|
|
334
|
+
onEnter(element, done) {
|
|
335
|
+
emit("enter", element);
|
|
336
|
+
animate(element, "enter", props.enterTo, done);
|
|
337
|
+
},
|
|
338
|
+
onAfterEnter(element) {
|
|
339
|
+
cancelRun(element);
|
|
340
|
+
emit("afterEnter", element);
|
|
341
|
+
},
|
|
342
|
+
onEnterCancelled(element) {
|
|
343
|
+
cancelRun(element);
|
|
344
|
+
emit("enterCancelled", element);
|
|
345
|
+
},
|
|
346
|
+
onBeforeLeave(element) {
|
|
347
|
+
cancelRun(element);
|
|
348
|
+
emit("beforeLeave", element);
|
|
349
|
+
},
|
|
350
|
+
onLeave(element, done) {
|
|
351
|
+
emit("leave", element);
|
|
352
|
+
animate(element, "leave", props.leaveTo, done);
|
|
353
|
+
},
|
|
354
|
+
onAfterLeave(element) {
|
|
355
|
+
if (cancelRun(element) === "leave") {
|
|
356
|
+
emit("leaveCancelled", element);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
emit("afterLeave", element);
|
|
360
|
+
},
|
|
361
|
+
onLeaveCancelled(element) {
|
|
362
|
+
cancelRun(element);
|
|
363
|
+
emit("leaveCancelled", element);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
if (hasListener("onBeforeAppear")) baseProps.onBeforeAppear = (element) => {
|
|
367
|
+
cancelRun(element);
|
|
368
|
+
prepareEnter(element);
|
|
369
|
+
emit("beforeAppear", element);
|
|
370
|
+
};
|
|
371
|
+
if (hasListener("onAppear")) baseProps.onAppear = (element, done) => {
|
|
372
|
+
emit("appear", element);
|
|
373
|
+
animate(element, "enter", props.enterTo, done);
|
|
374
|
+
};
|
|
375
|
+
if (hasListener("onAfterAppear")) baseProps.onAfterAppear = (element) => {
|
|
376
|
+
cancelRun(element);
|
|
377
|
+
emit("afterAppear", element);
|
|
378
|
+
};
|
|
379
|
+
if (hasListener("onAppearCancelled")) baseProps.onAppearCancelled = (element) => {
|
|
380
|
+
cancelRun(element);
|
|
381
|
+
emit("appearCancelled", element);
|
|
382
|
+
};
|
|
383
|
+
return h(BaseTransition, baseProps, slots.default);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region packages/vue/src/vue-shape.ts
|
|
389
|
+
var vueCompRegistry = /* @__PURE__ */ new Map();
|
|
390
|
+
function registerVueComponent(name, comp) {
|
|
391
|
+
vueCompRegistry.set(name, comp);
|
|
392
|
+
}
|
|
393
|
+
function getVueComponent(name) {
|
|
394
|
+
return vueCompRegistry.get(name);
|
|
395
|
+
}
|
|
396
|
+
var VueShape = class extends Rectangle {
|
|
397
|
+
type = "vue-component";
|
|
398
|
+
comp = null;
|
|
399
|
+
compName = "";
|
|
400
|
+
componentProps;
|
|
401
|
+
componentPropsResolver;
|
|
402
|
+
componentPropsRef = null;
|
|
403
|
+
sizeRef = null;
|
|
404
|
+
app = null;
|
|
405
|
+
constructor(options = {}, componentProps = {}, properties = new RectangleProperties(options), Transform = RectangleTransform) {
|
|
406
|
+
super(options, properties, Transform);
|
|
407
|
+
this.componentProps = componentProps;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 绑定一次 Vue component;提供 resolver 时,Properties 成为 component props 唯一真源。
|
|
411
|
+
*/
|
|
412
|
+
bindComponent(comp, resolveComponentProps) {
|
|
413
|
+
this.comp = comp;
|
|
414
|
+
this.componentPropsResolver = resolveComponentProps;
|
|
415
|
+
this.compName = comp.name ?? "";
|
|
416
|
+
if (this.compName) vueCompRegistry.set(this.compName, comp);
|
|
417
|
+
this.syncComponentProps();
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
onActivate(root, layer) {
|
|
421
|
+
super.onActivate(root, layer);
|
|
422
|
+
if (this.comp) this.mountVue();
|
|
423
|
+
}
|
|
424
|
+
onDeactivate(root, layer) {
|
|
425
|
+
this.unmountVue();
|
|
426
|
+
super.onDeactivate(root, layer);
|
|
427
|
+
}
|
|
428
|
+
onUnmount() {
|
|
429
|
+
this.unmountVue();
|
|
430
|
+
super.onUnmount();
|
|
431
|
+
}
|
|
432
|
+
mountVue() {
|
|
433
|
+
if (this.app) return;
|
|
434
|
+
this.componentPropsRef = shallowRef(this.componentProps);
|
|
435
|
+
this.sizeRef = reactive({
|
|
436
|
+
width: this.width ?? 0,
|
|
437
|
+
height: this.height ?? 0
|
|
438
|
+
});
|
|
439
|
+
const componentPropsRef = this.componentPropsRef;
|
|
440
|
+
const sizeRef = this.sizeRef;
|
|
441
|
+
const Comp = this.comp;
|
|
442
|
+
this.app = baseCreateApp(defineComponent({ setup() {
|
|
443
|
+
return () => h(Comp, mergeProps(componentPropsRef.value, sizeRef));
|
|
444
|
+
} }));
|
|
445
|
+
this.app.provide(FULATE_HOST_KEY, markRaw(this));
|
|
446
|
+
const root = this.activeRoot;
|
|
447
|
+
if (root) {
|
|
448
|
+
this.app.provide(FULATE_ROOT_KEY, root);
|
|
449
|
+
const overlay = getFulateOverlay(root);
|
|
450
|
+
if (overlay) this.app.provide(FULATE_OVERLAY_KEY, overlay);
|
|
451
|
+
}
|
|
452
|
+
this.app.provide(VUE_SHAPE_SIZE_KEY, this.sizeRef);
|
|
453
|
+
this.app.mount(markRaw(this));
|
|
454
|
+
}
|
|
455
|
+
unmountVue() {
|
|
456
|
+
if (this.app) {
|
|
457
|
+
this.app.unmount();
|
|
458
|
+
this.app = null;
|
|
459
|
+
this.componentPropsRef = null;
|
|
460
|
+
this.sizeRef = null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** 采用完整 component props whole-value;不合并字段,同一引用为 noop。 */
|
|
464
|
+
setComponentProps(componentProps) {
|
|
465
|
+
if (this.componentProps === componentProps) return this;
|
|
466
|
+
this.componentProps = componentProps;
|
|
467
|
+
if (this.componentPropsRef) this.componentPropsRef.value = componentProps;
|
|
468
|
+
return this;
|
|
469
|
+
}
|
|
470
|
+
onPropertiesChanged(current, changes) {
|
|
471
|
+
super.onPropertiesChanged(current, changes);
|
|
472
|
+
if (changes.has("width") || changes.has("height")) this.syncSize();
|
|
473
|
+
this.syncComponentProps(current);
|
|
474
|
+
}
|
|
475
|
+
syncSize() {
|
|
476
|
+
if (this.sizeRef) {
|
|
477
|
+
if (!isUndefined(this.width)) this.sizeRef.width = this.width;
|
|
478
|
+
if (!isUndefined(this.height)) this.sizeRef.height = this.height;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
syncComponentProps(properties = this.properties) {
|
|
482
|
+
if (this.componentPropsResolver) this.setComponentProps(this.componentPropsResolver(properties));
|
|
483
|
+
}
|
|
484
|
+
/** manual 模式保存 component metadata;mapped 模式只保存 Properties。 */
|
|
485
|
+
toJSON() {
|
|
486
|
+
const json = super.toJSON();
|
|
487
|
+
delete json.children;
|
|
488
|
+
if (this.componentPropsResolver) return json;
|
|
489
|
+
return {
|
|
490
|
+
...json,
|
|
491
|
+
component: this.compName,
|
|
492
|
+
componentProps: structuredClone(this.componentProps)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var vueClipboardPlugin = {
|
|
497
|
+
type: "vue-component",
|
|
498
|
+
deserialize(data) {
|
|
499
|
+
const { type: _type, children: _children, component, componentProps, ...shapeProps } = data;
|
|
500
|
+
const comp = vueCompRegistry.get(component);
|
|
501
|
+
if (!comp) return;
|
|
502
|
+
return fromVueToFulate(comp, shapeProps, componentProps);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
/** 从彼此独立的 Rectangle options 和 Vue component props 创建 VueShape。 */
|
|
506
|
+
function fromVueToFulate(comp, options = {}, componentProps = {}) {
|
|
507
|
+
return markRaw(new VueShape(options, componentProps)).bindComponent(comp);
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
export { FTransition, FULATE_HOST_KEY, FULATE_OVERLAY_KEY, FULATE_ROOT_KEY, VUE_SHAPE_SIZE_KEY, VueShape, createApp, fromVueToFulate, getRegisteredElement, getVueComponent, registerElement, registerVueComponent, registerVueOverlay, useFulateHost, vueClipboardPlugin };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Element } from "@fulate/core";
|
|
2
|
+
export type ElementConstructor<T extends Element = Element> = new (options?: any) => T;
|
|
3
|
+
export declare function registerElement(type: string, Constructor: ElementConstructor): void;
|
|
4
|
+
export declare function getRegisteredElement(type: string): ElementConstructor<Element<import("packages/core/src").ElementProperties, import("packages/core/src").BaseElementOption<any>, Partial<Omit<import("packages/core/src").BaseElementOption<any>, "key" | "children">>>>;
|
|
5
|
+
export declare const render: import("@vue/runtime-core").RootRenderFunction<Element<import("packages/core/src").ElementProperties, import("packages/core/src").BaseElementOption<any>, Partial<Omit<import("packages/core/src").BaseElementOption<any>, "key" | "children">>>>, baseCreateApp: import("@vue/runtime-core").CreateAppFunction<Element<import("packages/core/src").ElementProperties, import("packages/core/src").BaseElementOption<any>, Partial<Omit<import("packages/core/src").BaseElementOption<any>, "key" | "children">>>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type BaseTransitionProps, type PropType } from "@vue/runtime-core";
|
|
2
|
+
export declare const FTransition: import("@vue/runtime-core").DefineComponent<import("@vue/runtime-core").ExtractPropTypes<{
|
|
3
|
+
appear: {
|
|
4
|
+
type: BooleanConstructor;
|
|
5
|
+
default: boolean;
|
|
6
|
+
};
|
|
7
|
+
mode: {
|
|
8
|
+
type: PropType<BaseTransitionProps["mode"]>;
|
|
9
|
+
default: any;
|
|
10
|
+
};
|
|
11
|
+
enterFrom: {
|
|
12
|
+
type: ObjectConstructor;
|
|
13
|
+
default: any;
|
|
14
|
+
};
|
|
15
|
+
enterTo: {
|
|
16
|
+
type: ObjectConstructor;
|
|
17
|
+
default: any;
|
|
18
|
+
};
|
|
19
|
+
leaveTo: {
|
|
20
|
+
type: ObjectConstructor;
|
|
21
|
+
default: any;
|
|
22
|
+
};
|
|
23
|
+
duration: {
|
|
24
|
+
type: NumberConstructor;
|
|
25
|
+
default: number;
|
|
26
|
+
};
|
|
27
|
+
}>, () => import("@vue/runtime-core").VNode<import("@vue/runtime-core").RendererNode, import("@vue/runtime-core").RendererElement, {
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}>, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, ("enter" | "leave" | "beforeEnter" | "afterEnter" | "enterCancelled" | "beforeLeave" | "afterLeave" | "leaveCancelled" | "beforeAppear" | "appear" | "afterAppear" | "appearCancelled")[], "enter" | "leave" | "beforeEnter" | "afterEnter" | "enterCancelled" | "beforeLeave" | "afterLeave" | "leaveCancelled" | "beforeAppear" | "appear" | "afterAppear" | "appearCancelled", import("@vue/runtime-core").PublicProps, Readonly<import("@vue/runtime-core").ExtractPropTypes<{
|
|
30
|
+
appear: {
|
|
31
|
+
type: BooleanConstructor;
|
|
32
|
+
default: boolean;
|
|
33
|
+
};
|
|
34
|
+
mode: {
|
|
35
|
+
type: PropType<BaseTransitionProps["mode"]>;
|
|
36
|
+
default: any;
|
|
37
|
+
};
|
|
38
|
+
enterFrom: {
|
|
39
|
+
type: ObjectConstructor;
|
|
40
|
+
default: any;
|
|
41
|
+
};
|
|
42
|
+
enterTo: {
|
|
43
|
+
type: ObjectConstructor;
|
|
44
|
+
default: any;
|
|
45
|
+
};
|
|
46
|
+
leaveTo: {
|
|
47
|
+
type: ObjectConstructor;
|
|
48
|
+
default: any;
|
|
49
|
+
};
|
|
50
|
+
duration: {
|
|
51
|
+
type: NumberConstructor;
|
|
52
|
+
default: number;
|
|
53
|
+
};
|
|
54
|
+
}>> & Readonly<{
|
|
55
|
+
onEnter?: (...args: any[]) => any;
|
|
56
|
+
onLeave?: (...args: any[]) => any;
|
|
57
|
+
onBeforeEnter?: (...args: any[]) => any;
|
|
58
|
+
onAfterEnter?: (...args: any[]) => any;
|
|
59
|
+
onEnterCancelled?: (...args: any[]) => any;
|
|
60
|
+
onBeforeLeave?: (...args: any[]) => any;
|
|
61
|
+
onAfterLeave?: (...args: any[]) => any;
|
|
62
|
+
onLeaveCancelled?: (...args: any[]) => any;
|
|
63
|
+
onBeforeAppear?: (...args: any[]) => any;
|
|
64
|
+
onAppear?: (...args: any[]) => any;
|
|
65
|
+
onAfterAppear?: (...args: any[]) => any;
|
|
66
|
+
onAppearCancelled?: (...args: any[]) => any;
|
|
67
|
+
}>, {
|
|
68
|
+
duration: number;
|
|
69
|
+
mode: "in-out" | "out-in" | "default";
|
|
70
|
+
appear: boolean;
|
|
71
|
+
enterFrom: Record<string, any>;
|
|
72
|
+
enterTo: Record<string, any>;
|
|
73
|
+
leaveTo: Record<string, any>;
|
|
74
|
+
}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ElementJSON, type ElementTransformClass, type Layer, type PropertyChanges, type Root } from "@fulate/core";
|
|
2
|
+
import { Rectangle, RectangleProperties, type RectangleOption } from "@fulate/ui";
|
|
3
|
+
import { type Component } from "@vue/runtime-core";
|
|
4
|
+
export declare function registerVueComponent(name: string, comp: Component): void;
|
|
5
|
+
export declare function getVueComponent(name: string): Component | undefined;
|
|
6
|
+
/** 将当前 effective Properties 映射为完整 Vue component props。 */
|
|
7
|
+
export type VueComponentPropsResolver<P extends RectangleProperties = RectangleProperties> = (properties: Readonly<P>) => Record<string, unknown>;
|
|
8
|
+
export type VueShapeJSON<O extends RectangleOption<any> = RectangleOption<any>> = ElementJSON<O> & {
|
|
9
|
+
component?: string;
|
|
10
|
+
componentProps?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export declare class VueShape<P extends RectangleProperties = RectangleProperties, O extends RectangleOption<any> = RectangleOption<any>> extends Rectangle<P, O> {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
private comp;
|
|
15
|
+
private compName;
|
|
16
|
+
private componentProps;
|
|
17
|
+
private componentPropsResolver?;
|
|
18
|
+
private componentPropsRef;
|
|
19
|
+
private sizeRef;
|
|
20
|
+
private app;
|
|
21
|
+
constructor(options?: NoInfer<O>, componentProps?: Record<string, unknown>, properties?: P, Transform?: ElementTransformClass);
|
|
22
|
+
/**
|
|
23
|
+
* 绑定一次 Vue component;提供 resolver 时,Properties 成为 component props 唯一真源。
|
|
24
|
+
*/
|
|
25
|
+
bindComponent(comp: Component, resolveComponentProps?: VueComponentPropsResolver<P>): this;
|
|
26
|
+
protected onActivate(root: Root | null, layer: Layer | null): void;
|
|
27
|
+
protected onDeactivate(root: Root | null, layer: Layer | null): void;
|
|
28
|
+
protected onUnmount(): void;
|
|
29
|
+
private mountVue;
|
|
30
|
+
private unmountVue;
|
|
31
|
+
/** 采用完整 component props whole-value;不合并字段,同一引用为 noop。 */
|
|
32
|
+
setComponentProps(componentProps: Record<string, unknown>): this;
|
|
33
|
+
protected onPropertiesChanged(current: P, changes: PropertyChanges): void;
|
|
34
|
+
private syncSize;
|
|
35
|
+
private syncComponentProps;
|
|
36
|
+
/** manual 模式保存 component metadata;mapped 模式只保存 Properties。 */
|
|
37
|
+
toJSON(): VueShapeJSON<O>;
|
|
38
|
+
}
|
|
39
|
+
export declare const vueClipboardPlugin: {
|
|
40
|
+
readonly type: "vue-component";
|
|
41
|
+
readonly deserialize: (data: Record<string, any>) => VueShape<RectangleProperties, RectangleOption<any>>;
|
|
42
|
+
};
|
|
43
|
+
/** 从彼此独立的 Rectangle options 和 Vue component props 创建 VueShape。 */
|
|
44
|
+
export declare function fromVueToFulate(comp: Component, options?: RectangleOption<VueShape>, componentProps?: Record<string, unknown>): VueShape;
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fulate/vue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@fulate/core": "*",
|
|
23
|
+
"@fulate/ui": "*",
|
|
24
|
+
"@vue/runtime-core": "^3.5.0",
|
|
25
|
+
"lodash-es": "^4.17.22"
|
|
26
|
+
}
|
|
27
|
+
}
|