@flowgram-vue/fixed-layout-editor 0.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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { createSelectBoxPlugin } from '@flowgram-vue/select-box-plugin';
7
+ import { FixedLayoutContainerModule } from '@flowgram-vue/fixed-layout-core';
8
+ import { FixedHistoryService, createFixedHistoryPlugin } from '@flowgram-vue/fixed-history-plugin';
9
+ import { createFixedDragPlugin } from '@flowgram-vue/fixed-drag-plugin';
10
+ import {
11
+ PluginsProvider,
12
+ createDefaultPreset,
13
+ createVariablePlugin,
14
+ createPlaygroundPlugin,
15
+ createShortcutsPlugin,
16
+ SelectionService,
17
+ Command,
18
+ Plugin,
19
+ FlowDocument,
20
+ FlowNodeEntity,
21
+ FlowDocumentOptionsDefault,
22
+ FlowDocumentOptions,
23
+ FlowNodesContentLayer,
24
+ FlowNodesTransformLayer,
25
+ FlowScrollBarLayer,
26
+ FlowScrollLimitLayer,
27
+ createPlaygroundVuePreset,
28
+ } from '@flowgram-vue/editor';
29
+
30
+ import { compose } from '../utils/compose';
31
+ import { FlowOperationService } from '../types';
32
+ import { createOperationPlugin } from '../plugins/create-operation-plugin';
33
+ import { fromNodeJSON, toNodeJSON } from './node-serialize';
34
+ import { FixedLayoutPluginContext, FixedLayoutProps } from './fixed-layout-props';
35
+
36
+ export function createFixedLayoutPreset(
37
+ opts: FixedLayoutProps
38
+ ): PluginsProvider<FixedLayoutPluginContext> {
39
+ return (ctx: FixedLayoutPluginContext) => {
40
+ opts = { ...FixedLayoutProps.DEFAULT, ...opts };
41
+ let plugins: Plugin[] = [createOperationPlugin(opts)];
42
+ /**
43
+ * 注册默认的快捷键
44
+ */
45
+ plugins.push(
46
+ createShortcutsPlugin({
47
+ registerShortcuts(registry) {
48
+ const selection = ctx.get<SelectionService>(SelectionService);
49
+ registry.addHandlers({
50
+ commandId: Command.Default.DELETE,
51
+ shortcuts: ['backspace', 'delete'],
52
+ isEnabled: () =>
53
+ selection.selection.length > 0 && !ctx.playground.config.readonlyOrDisabled,
54
+ execute: () => {
55
+ // TODO 这里要判断 CurrentEditor
56
+ const nodes = selection.selection.filter(
57
+ (entity) => entity instanceof FlowNodeEntity
58
+ ) as FlowNodeEntity[];
59
+
60
+ const flowOperationService = ctx.get<FlowOperationService>(FlowOperationService);
61
+ flowOperationService.deleteNodes(nodes);
62
+ selection.selection = selection.selection.filter((s) => !s.disposed);
63
+ },
64
+ });
65
+
66
+ if (opts?.history?.enable) {
67
+ const fixedHistoryService = ctx.get<FixedHistoryService>(FixedHistoryService);
68
+
69
+ if (!opts.history.disableShortcuts) {
70
+ registry.addHandlers({
71
+ commandId: Command.Default.UNDO,
72
+ shortcuts: ['meta z', 'ctrl z'],
73
+ isEnabled: () => true,
74
+ execute: () => {
75
+ fixedHistoryService.undo();
76
+ },
77
+ });
78
+ registry.addHandlers({
79
+ commandId: Command.Default.REDO,
80
+ shortcuts: ['meta shift z', 'ctrl shift z'],
81
+ isEnabled: () => true,
82
+ execute: () => {
83
+ fixedHistoryService.redo();
84
+ },
85
+ });
86
+ }
87
+ }
88
+ },
89
+ }),
90
+ /**
91
+ * 圈选逻辑实现
92
+ */
93
+ createSelectBoxPlugin({
94
+ canSelect: (e) =>
95
+ // 需满足以下条件:
96
+ // - 鼠标左键
97
+ e.button === 0 &&
98
+ !(ctx.get(FlowDocument) as FlowDocument).renderState.config.nodeHoveredId,
99
+ ...(opts.selectBox || {}),
100
+ }),
101
+ /**
102
+ * 固定布局拖拽逻辑实现
103
+ */
104
+ createFixedDragPlugin((opts.dragdrop || {}) as Parameters<typeof createFixedDragPlugin>[0])
105
+ );
106
+ /**
107
+ * 加载默认编辑器配置
108
+ */
109
+ plugins = createDefaultPreset(opts, plugins)(ctx);
110
+ /**
111
+ * 注册 变量系统
112
+ */
113
+ if (opts.variableEngine?.enable) {
114
+ plugins.push(
115
+ createVariablePlugin({
116
+ ...opts.variableEngine,
117
+ layout: 'fixed',
118
+ })
119
+ );
120
+ }
121
+ /**
122
+ * 注册 历史记录
123
+ */
124
+ if (opts.history?.enable) {
125
+ plugins.push(createFixedHistoryPlugin(opts.history as Parameters<typeof createFixedHistoryPlugin>[0]));
126
+ }
127
+ /*
128
+ * 加载固定布局画布模块
129
+ * */
130
+ plugins.push(
131
+ createPlaygroundPlugin<FixedLayoutPluginContext>({
132
+ containerModules: [FixedLayoutContainerModule],
133
+ onBind(bindConfig) {
134
+ if (!bindConfig.isBound(FlowDocumentOptions)) {
135
+ bindConfig.bind(FlowDocumentOptions).toConstantValue({
136
+ ...FlowDocumentOptionsDefault,
137
+ defaultLayout: opts.defaultLayout,
138
+ toNodeJSON: (node) => toNodeJSON(opts, node),
139
+ fromNodeJSON: (node, json, isFirstCreate) =>
140
+ fromNodeJSON(opts, node, json, isFirstCreate),
141
+ allNodesDefaultExpanded: opts.allNodesDefaultExpanded,
142
+ } as FlowDocumentOptions);
143
+ }
144
+ },
145
+ onInit: (ctx) => {
146
+ ctx.playground.registerLayers(
147
+ FlowNodesContentLayer, // 节点内容渲染
148
+ FlowNodesTransformLayer // 节点位置偏移计算
149
+ );
150
+ // 劫持节点线条
151
+ if (opts.formatNodeLines) {
152
+ ctx.document.options.formatNodeLines = compose([
153
+ ctx.document.options.formatNodeLines,
154
+ opts.formatNodeLines,
155
+ ]);
156
+ }
157
+ // 劫持节点 label
158
+ if (opts.formatNodeLabels) {
159
+ ctx.document.options.formatNodeLabels = compose([
160
+ ctx.document.options.formatNodeLabels,
161
+ opts.formatNodeLabels,
162
+ ]);
163
+ }
164
+ if (opts.scroll?.enableScrollLimit) {
165
+ // 控制滚动范围
166
+ ctx.playground.registerLayer(FlowScrollLimitLayer);
167
+ }
168
+ if (!opts.scroll?.disableScrollBar) {
169
+ // 控制条
170
+ ctx.playground.registerLayer(FlowScrollBarLayer);
171
+ }
172
+ if (opts.scroll?.disableScroll) {
173
+ ctx.playground.config.scrollDisable = true;
174
+ }
175
+ if (opts.nodeRegistries) {
176
+ ctx.document.registerFlowNodes(...opts.nodeRegistries);
177
+ }
178
+ },
179
+ })
180
+ );
181
+
182
+ return createPlaygroundVuePreset(opts, plugins)(ctx);
183
+ };
184
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { SelectBoxPluginOptions } from '@flowgram-vue/select-box-plugin';
7
+ import { FixedHistoryPluginOptions, HistoryService } from '@flowgram-vue/fixed-history-plugin';
8
+ import { type FixDragPluginOptions } from '@flowgram-vue/fixed-drag-plugin';
9
+ import {
10
+ ClipboardService,
11
+ EditorPluginContext,
12
+ EditorProps,
13
+ FlowDocument,
14
+ FlowDocumentJSON,
15
+ FlowLayoutDefault,
16
+ SelectionService,
17
+ PluginContext,
18
+ FlowNodeEntity,
19
+ FlowTransitionLine,
20
+ FlowTransitionLabel,
21
+ } from '@flowgram-vue/editor';
22
+
23
+ import { FlowOperationService } from '../types';
24
+
25
+ export const FixedLayoutPluginContext = PluginContext;
26
+
27
+ export interface FixedLayoutPluginTools {
28
+ fitView: (easing?: boolean) => Promise<void>;
29
+ }
30
+ export interface FixedLayoutPluginContext extends EditorPluginContext {
31
+ document: FlowDocument;
32
+ /**
33
+ * 提供对画布节点相关操作方法, 并 支持 redo/undo
34
+ */
35
+ operation: FlowOperationService;
36
+ clipboard: ClipboardService;
37
+ selection: SelectionService;
38
+ history: HistoryService;
39
+ tools: FixedLayoutPluginTools;
40
+ }
41
+
42
+ /**
43
+ * 固定布局配置
44
+ */
45
+ export interface FixedLayoutProps extends EditorProps<FixedLayoutPluginContext, FlowDocumentJSON> {
46
+ /**
47
+ * SelectBox config
48
+ */
49
+ selectBox?: SelectBoxPluginOptions;
50
+ /**
51
+ * Drag/Drop config
52
+ */
53
+ dragdrop?: FixDragPluginOptions<FixedLayoutPluginContext>;
54
+ /**
55
+ * Redo/Undo enable
56
+ */
57
+ history?: FixedHistoryPluginOptions<FixedLayoutPluginContext> & { disableShortcuts?: boolean };
58
+ /**
59
+ * vertical or horizontal layout
60
+ */
61
+ defaultLayout?: FlowLayoutDefault | string; // 默认布局
62
+ /**
63
+ * Customize the node line
64
+ * 自定义节点线条
65
+ */
66
+ formatNodeLines?: (node: FlowNodeEntity, lines: FlowTransitionLine[]) => FlowTransitionLine[];
67
+ /**
68
+ * Custom node label
69
+ * 自定义节点 label
70
+ */
71
+ formatNodeLabels?: (node: FlowNodeEntity, lines: FlowTransitionLabel[]) => FlowTransitionLabel[];
72
+ }
73
+
74
+ export namespace FixedLayoutProps {
75
+ /**
76
+ * 默认配置
77
+ */
78
+ export const DEFAULT: FixedLayoutProps = {
79
+ ...EditorProps.DEFAULT,
80
+ scroll: {
81
+ enableScrollLimit: true,
82
+ },
83
+ } as FixedLayoutProps;
84
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './fixed-layout-props';
7
+ export * from './fixed-layout-preset';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { FlowNodeEntity, FlowNodeJSON, FlowNodeFormData } from '@flowgram-vue/editor';
7
+
8
+ import { FixedLayoutProps } from './fixed-layout-props';
9
+
10
+ export function fromNodeJSON(
11
+ opts: FixedLayoutProps,
12
+ node: FlowNodeEntity,
13
+ json: FlowNodeJSON,
14
+ isFirstCreate: boolean
15
+ ) {
16
+ json = opts.fromNodeJSON ? opts.fromNodeJSON(node, json, isFirstCreate) : json;
17
+ const formData = node.getData(FlowNodeFormData)!;
18
+ // 如果没有使用表单引擎,将 data 数据填入 extInfo
19
+ if (!formData) {
20
+ if (json.data) {
21
+ node.updateExtInfo(json.data, true);
22
+ }
23
+ } else {
24
+ const defaultFormMeta = opts.nodeEngine?.createDefaultFormMeta?.(node);
25
+
26
+ const formMeta = node.getNodeRegistry()?.formMeta || defaultFormMeta;
27
+
28
+ if (formMeta) {
29
+ if (isFirstCreate) {
30
+ formData.createForm(formMeta, json.data);
31
+ } else {
32
+ formData.updateFormValues(json.data);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ export function toNodeJSON(opts: FixedLayoutProps, node: FlowNodeEntity): FlowNodeJSON {
39
+ const nodesMap: Record<string, FlowNodeJSON> = {};
40
+ let startNodeJSON: FlowNodeJSON;
41
+ node.document.traverse((node) => {
42
+ const isSystemNode = node.id.startsWith('$');
43
+ if (isSystemNode) return;
44
+ const formData = node.getData(FlowNodeFormData);
45
+ let formJSON =
46
+ formData && formData.formModel && formData.formModel.initialized
47
+ ? formData.toJSON()
48
+ : undefined;
49
+ let nodeJSON: FlowNodeJSON = {
50
+ id: node.id,
51
+ type: node.flowNodeType,
52
+ data: formData ? formJSON : node.getExtInfo(),
53
+ blocks: [],
54
+ };
55
+ if (opts.toNodeJSON) {
56
+ nodeJSON = opts.toNodeJSON(node, nodeJSON);
57
+ }
58
+ if (!startNodeJSON) startNodeJSON = nodeJSON;
59
+ let { parent } = node;
60
+ if (parent && parent.id.startsWith('$')) {
61
+ parent = parent.originParent;
62
+ }
63
+ const parentJSON = parent ? nodesMap[parent.id] : undefined;
64
+ if (parentJSON) {
65
+ parentJSON.blocks?.push(nodeJSON);
66
+ }
67
+ nodesMap[node.id] = nodeJSON;
68
+ }, node);
69
+
70
+ // @ts-ignore
71
+ return startNodeJSON;
72
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { inject, injectable } from 'inversify';
7
+ import {
8
+ FlowGroupService,
9
+ FlowNodeEntity,
10
+ FlowNodeEntityOrId,
11
+ FlowNodeFormData,
12
+ FlowOperationBaseServiceImpl,
13
+ FormModel,
14
+ FormModelV2,
15
+ isFormModelV2,
16
+ } from '@flowgram-vue/editor';
17
+
18
+ import { FlowOperationService } from '../types';
19
+
20
+ @injectable()
21
+ export class FlowOperationServiceImpl
22
+ extends FlowOperationBaseServiceImpl
23
+ implements FlowOperationService
24
+ {
25
+ @inject(FlowGroupService)
26
+ protected groupService: FlowGroupService;
27
+
28
+ createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {
29
+ return this.groupService.createGroup(nodes);
30
+ }
31
+
32
+ ungroup(groupNode: FlowNodeEntity): void {
33
+ return this.groupService.ungroup(groupNode);
34
+ }
35
+
36
+ setFormValue(nodeOrId: FlowNodeEntityOrId, path: string, value: unknown): void {
37
+ const node = this.toNodeEntity(nodeOrId);
38
+ const formModel = node?.getData(FlowNodeFormData)?.getFormModel<FormModel | FormModelV2>();
39
+
40
+ if (!formModel) {
41
+ return;
42
+ }
43
+
44
+ if (isFormModelV2(formModel)) {
45
+ (formModel as FormModelV2).setValueIn(path, value);
46
+ } else {
47
+ const formItem = (formModel as FormModel).getFormItemByPath(path);
48
+
49
+ if (!formItem) {
50
+ return;
51
+ }
52
+ formItem.value = value;
53
+ }
54
+ }
55
+
56
+ startTransaction(): void {}
57
+
58
+ endTransaction(): void {}
59
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { inject, injectable, postConstruct } from 'inversify';
7
+ import { HistoryService } from '@flowgram-vue/history';
8
+ import { FixedHistoryService } from '@flowgram-vue/fixed-history-plugin';
9
+ import {
10
+ AddBlockConfig,
11
+ AddOrDeleteNodeValue,
12
+ FlowDocument,
13
+ FlowNodeEntity,
14
+ FlowNodeEntityOrId,
15
+ FlowNodeJSON,
16
+ FlowOperation,
17
+ MoveChildNodesOperationValue,
18
+ OnNodeAddEvent,
19
+ OperationType,
20
+ } from '@flowgram-vue/editor';
21
+
22
+ import { FlowOperationService } from '../types';
23
+ import { FlowOperationServiceImpl } from './flow-operation-service';
24
+
25
+ @injectable()
26
+ export class HistoryOperationServiceImpl
27
+ extends FlowOperationServiceImpl
28
+ implements FlowOperationService
29
+ {
30
+ @inject(FixedHistoryService)
31
+ protected fixedHistoryService: FixedHistoryService;
32
+
33
+ @inject(HistoryService)
34
+ protected historyService: HistoryService;
35
+
36
+ @inject(FlowDocument)
37
+ protected document: FlowDocument;
38
+
39
+ @postConstruct()
40
+ protected init() {
41
+ this.toDispose.push(this.onNodeAdd(this.handleNodeAdd.bind(this)));
42
+ }
43
+
44
+ addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity {
45
+ return this.fixedHistoryService.addFromNode(fromNode, nodeJSON);
46
+ }
47
+
48
+ addBlock(
49
+ target: FlowNodeEntityOrId,
50
+ blockJSON: FlowNodeJSON,
51
+ config: AddBlockConfig = {}
52
+ ): FlowNodeEntity {
53
+ const { parent, index } = config;
54
+ return this.fixedHistoryService.addBlock(target, blockJSON, parent, index);
55
+ }
56
+
57
+ deleteNode(nodeOrId: FlowNodeEntityOrId): void {
58
+ const node = this.toNodeEntity(nodeOrId);
59
+ if (!node) {
60
+ return;
61
+ }
62
+ this.fixedHistoryService.deleteNode(node);
63
+ }
64
+
65
+ deleteNodes(nodes: FlowNodeEntityOrId[]): void {
66
+ const nodesEntities = nodes.map((node) =>
67
+ typeof node === 'string' ? this.document.getNode(node) : node
68
+ ) as FlowNodeEntity[];
69
+ return this.fixedHistoryService.deleteNodes(nodesEntities);
70
+ }
71
+
72
+ startTransaction(): void {
73
+ this.historyService.startTransaction();
74
+ }
75
+
76
+ endTransaction(): void {
77
+ this.historyService.endTransaction();
78
+ }
79
+
80
+ apply(operation: FlowOperation) {
81
+ this.historyService.pushOperation(operation);
82
+ }
83
+
84
+ protected doMoveNode(node: FlowNodeEntity, newParent: FlowNodeEntity, index: number) {
85
+ const fromParentId = node.parent?.id;
86
+
87
+ if (!fromParentId) {
88
+ return;
89
+ }
90
+
91
+ const value: MoveChildNodesOperationValue = {
92
+ nodeIds: [this.toId(node)],
93
+ fromParentId: node.parent.id,
94
+ toParentId: this.toId(newParent),
95
+ fromIndex: this.getNodeIndex(node),
96
+ toIndex: index,
97
+ };
98
+
99
+ return this.historyService.pushOperation({
100
+ type: OperationType.moveChildNodes,
101
+ value,
102
+ });
103
+ }
104
+
105
+ protected handleNodeAdd({ data: addNodeData }: OnNodeAddEvent): FlowNodeEntity {
106
+ const { parent, index, hidden, originParent, ...nodeJSON } = addNodeData;
107
+ const value: AddOrDeleteNodeValue = {
108
+ data: nodeJSON,
109
+ parentId: parent?.id,
110
+ index,
111
+ hidden,
112
+ };
113
+
114
+ return this.historyService.pushOperation(
115
+ {
116
+ type: OperationType.addNode,
117
+ value: value,
118
+ uri: this.fixedHistoryService.config.getNodeURI(nodeJSON.id),
119
+ },
120
+ {
121
+ noApply: true,
122
+ }
123
+ );
124
+ }
125
+ }
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import {
7
+ FlowNodeEntity,
8
+ FlowNodeEntityOrId,
9
+ FlowOperationBaseService,
10
+ } from '@flowgram-vue/editor';
11
+
12
+ export interface FlowOperationService extends FlowOperationBaseService {
13
+ /**
14
+ * 创建分组
15
+ * @param nodes 节点列表
16
+ */
17
+ createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined;
18
+ /**
19
+ * 取消分组
20
+ * @param groupNode
21
+ */
22
+ ungroup(groupNode: FlowNodeEntity): void;
23
+ /**
24
+ * 开始事务
25
+ */
26
+ startTransaction(): void;
27
+ /**
28
+ * 结束事务
29
+ */
30
+ endTransaction(): void;
31
+ /**
32
+ * 修改表单数据
33
+ * @param node 节点
34
+ * @param path 属性路径
35
+ * @param value 值
36
+ */
37
+ setFormValue(node: FlowNodeEntityOrId, path: string, value: unknown): void;
38
+ }
39
+
40
+ export const FlowOperationService = Symbol('FlowOperationService');
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { FlowNodeEntity } from '@flowgram-vue/editor';
7
+
8
+ export type ComposeListItem<T> = (node: FlowNodeEntity, data: T[]) => T[];
9
+
10
+ export const compose =
11
+ <T>(fnList: (ComposeListItem<T> | undefined)[]) =>
12
+ (node: FlowNodeEntity, data: T[]): T[] => {
13
+ const list = fnList.filter(Boolean) as ComposeListItem<T>[];
14
+ if (!list.length) {
15
+ return data;
16
+ }
17
+ return list.reduce((acc: T[], fn) => fn(node, acc), data);
18
+ };