@bpmn-nova/studio 0.3.0-preview

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,168 @@
1
+ import {
2
+ NODE_DEFINITIONS,
3
+ getEdge,
4
+ getNode,
5
+ isEmbeddedSubProcess,
6
+ } from '@bpmn-nova/core';
7
+ import { selectedDiagramNodes, selectedGroupableNodes, selectedLayoutNodes } from './selection-layout.js';
8
+
9
+ function insert(actions, action) {
10
+ if (!action?.id) return actions;
11
+ const position = action.position || {};
12
+ if (position.remove) return actions.filter((item) => item.id !== position.remove && item.id !== action.id);
13
+ const next = [...actions];
14
+ let index = next.length;
15
+ const existing = next.findIndex((item) => item.id === action.id);
16
+ if (existing >= 0) { index = existing; next.splice(existing, 1); }
17
+ if (position.replace) {
18
+ const target = next.findIndex((item) => item.id === position.replace);
19
+ if (target >= 0) { index = target; next.splice(target, 1); }
20
+ } else if (position.prepend) index = 0;
21
+ else if (position.before) {
22
+ const target = next.findIndex((item) => item.id === position.before);
23
+ if (target >= 0) index = target;
24
+ } else if (position.after) {
25
+ const target = next.findIndex((item) => item.id === position.after);
26
+ if (target >= 0) index = target + 1;
27
+ } else if (position.append) index = next.length;
28
+ index = Math.max(0, Math.min(index, next.length));
29
+ next.splice(index, 0, action);
30
+ return next;
31
+ }
32
+
33
+ function action(config) {
34
+ return { group: 'primary', disabled: false, danger: false, ...config };
35
+ }
36
+
37
+ const flowKinds = new Set(['event', 'boundary', 'task', 'container', 'gateway']);
38
+
39
+ function canvasActions() {
40
+ return [
41
+ action({ id: 'canvas.fit', label: '最佳视图', iconId: 'ui.fitView', execute: ({ canvas }) => canvas.fitView(72) }),
42
+ action({ id: 'canvas.beautify', label: '美化当前流程', iconId: 'ui.magic', execute: ({ studio }) => studio.commands.beautify() }),
43
+ action({ id: 'canvas.reroute', label: '优化全部连线', iconId: 'ui.rerouteSelection', execute: ({ studio }) => studio.commands.rerouteEdges() }),
44
+ action({ id: 'pointer.select', label: '单选', iconId: 'ui.pointer', group: 'pointer', execute: ({ canvas }) => canvas.setPointerMode('select') }),
45
+ action({ id: 'pointer.marquee', label: '框选', iconId: 'ui.marquee', group: 'pointer', execute: ({ canvas }) => canvas.setPointerMode('marquee') }),
46
+ action({ id: 'pointer.pan', label: '拖动', iconId: 'ui.hand', group: 'pointer', execute: ({ canvas }) => canvas.setPointerMode('pan') }),
47
+ ];
48
+ }
49
+
50
+ function nodeActions(context) {
51
+ const node = getNode(context.studio.model, context.target.id);
52
+ if (!node) return [];
53
+ const definition = NODE_DEFINITIONS[node.type] || NODE_DEFINITIONS.generic;
54
+ const canOutgoing = flowKinds.has(definition.kind) && definition.eventStage !== 'end';
55
+ const actions = [];
56
+ if (canOutgoing) {
57
+ actions.push(
58
+ action({ id: 'node.quickAdd', label: '快捷添加下一节点', iconId: 'ui.add', execute: ({ canvas, target }) => canvas.openQuickAdd(target.id) }),
59
+ action({ id: 'node.connect', label: '连接已有节点', iconId: 'ui.connect', execute: ({ canvas, target }) => canvas.startConnect(target.id) }),
60
+ );
61
+ }
62
+ if (isEmbeddedSubProcess(node)) {
63
+ actions.push(action({ id: 'node.enterScope', label: '进入子流程', iconId: 'ui.enterScope', group: 'navigation', execute: ({ studio, target }) => studio.enterScope(target.id) }));
64
+ }
65
+ if (definition.kind === 'group') {
66
+ actions.push(action({ id: 'node.ungroup', label: '取消分组', iconId: 'ui.layoutSelection', execute: ({ studio, target }) => studio.commands.ungroup(target.id) }));
67
+ return actions;
68
+ }
69
+ actions.push(action({ id: 'selection.delete', label: '删除节点', iconId: 'ui.delete', group: 'danger', danger: true, execute: ({ studio, target }) => studio.commands.remove(target) }));
70
+ return actions;
71
+ }
72
+
73
+ function edgeActions(context) {
74
+ const edge = getEdge(context.studio.model, context.target.id);
75
+ if (!edge) return [];
76
+ return [
77
+ action({ id: 'edge.rename', label: '编辑连线名称', iconId: 'ui.edit', execute: ({ canvas, target }) => canvas.editEdgeName(target.id) }),
78
+ action({ id: 'edge.resetRoute', label: '恢复自动路由', iconId: 'ui.rerouteSelection', disabled: !edge.waypoints?.length, execute: ({ studio, target }) => studio.commands.resetEdgeRoute(target.id) }),
79
+ action({ id: 'edge.locateSource', label: '定位起点', iconId: 'ui.locateSource', group: 'navigation', execute: ({ studio }) => studio.select({ kind: 'node', id: edge.source }) }),
80
+ action({ id: 'edge.locateTarget', label: '定位终点', iconId: 'ui.locateTarget', group: 'navigation', execute: ({ studio }) => studio.select({ kind: 'node', id: edge.target }) }),
81
+ action({ id: 'selection.delete', label: '删除连线', iconId: 'ui.delete', group: 'danger', danger: true, execute: ({ studio, target }) => studio.commands.remove(target) }),
82
+ ];
83
+ }
84
+
85
+ const alignments = [
86
+ ['left', '左对齐', 'ui.alignLeft'],
87
+ ['centerX', '水平居中', 'ui.alignCenterHorizontal'],
88
+ ['right', '右对齐', 'ui.alignRight'],
89
+ ['top', '顶部对齐', 'ui.alignTop'],
90
+ ['centerY', '垂直居中', 'ui.alignCenterVertical'],
91
+ ['bottom', '底部对齐', 'ui.alignBottom'],
92
+ ];
93
+
94
+ function multiActions(context) {
95
+ const nodes = selectedLayoutNodes(context.studio.model, context.selection, context.studio.activeScopeId);
96
+ const diagramNodes = selectedDiagramNodes(context.studio.model, context.selection, context.studio.activeScopeId);
97
+ const groupableNodes = selectedGroupableNodes(context.studio.model, context.selection, context.studio.activeScopeId);
98
+ const actions = alignments.map(([alignment, label, iconId]) => action({
99
+ id: `selection.align.${alignment}`,
100
+ label,
101
+ iconId,
102
+ group: 'align',
103
+ disabled: nodes.length < 2,
104
+ execute: ({ studio }) => studio.arrangeSelection({ type: 'align', alignment }),
105
+ }));
106
+ actions.push(
107
+ action({ id: 'selection.group', label: '创建分组', iconId: 'ui.layoutSelection', group: 'layout', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length, execute: ({ studio }) => studio.commands.groupSelection() }),
108
+ action({ id: 'selection.distribute.horizontal', label: '水平等距', iconId: 'ui.distributeHorizontal', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'horizontal' }) }),
109
+ action({ id: 'selection.distribute.vertical', label: '垂直等距', iconId: 'ui.distributeVertical', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'vertical' }) }),
110
+ action({ id: 'selection.layout', label: '美化选区', iconId: 'ui.layoutSelection', group: 'layout', disabled: nodes.length < 2, execute: ({ studio }) => studio.arrangeSelection({ type: 'layout' }) }),
111
+ action({ id: 'selection.reroute', label: '优化关联连线', iconId: 'ui.rerouteSelection', group: 'layout', disabled: nodes.length < 1, execute: ({ studio }) => studio.arrangeSelection({ type: 'reroute' }) }),
112
+ action({ id: 'selection.fit', label: '适应选区', iconId: 'ui.fitSelection', group: 'layout', disabled: nodes.length < 1, execute: ({ canvas }) => canvas.fitSelection() }),
113
+ action({ id: 'selection.delete', label: '删除选区', iconId: 'ui.delete', group: 'danger', danger: true, execute: ({ studio, selection }) => studio.commands.remove(selection) }),
114
+ );
115
+ return actions;
116
+ }
117
+
118
+ export const defaultContextMenuProvider = {
119
+ id: 'bpmn-default-context-menu',
120
+ priority: 100,
121
+ getActions(context) {
122
+ if (context.target?.kind === 'canvas') return canvasActions(context);
123
+ if (context.target?.kind === 'node') return nodeActions(context);
124
+ if (context.target?.kind === 'edge') return edgeActions(context);
125
+ if (context.target?.kind === 'multi') return multiActions(context);
126
+ return [];
127
+ },
128
+ };
129
+
130
+ export class ContextMenuRegistry {
131
+ constructor() { this._providers = []; }
132
+
133
+ registerProvider(priority = 500, provider) {
134
+ if (typeof priority === 'object') { provider = priority; priority = provider.priority ?? 500; }
135
+ if (!provider?.id) throw new Error('Context menu provider requires an id.');
136
+ const record = { priority, provider, order: this._providers.length };
137
+ this._providers.push(record);
138
+ this._providers.sort((a, b) => b.priority - a.priority || a.order - b.order);
139
+ return () => { this._providers = this._providers.filter((item) => item !== record); };
140
+ }
141
+
142
+ resolve(context = {}) {
143
+ let actions = [];
144
+ for (const { provider } of [...this._providers].reverse()) {
145
+ const output = typeof provider.getActions === 'function' ? provider.getActions(context) : provider.actions;
146
+ for (const rawAction of output || []) {
147
+ if (!rawAction) continue;
148
+ if (typeof rawAction.visible === 'function' && !rawAction.visible(context)) continue;
149
+ const resolved = {
150
+ ...rawAction,
151
+ disabled: typeof rawAction.disabled === 'function' ? Boolean(rawAction.disabled(context)) : Boolean(rawAction.disabled),
152
+ danger: Boolean(rawAction.danger),
153
+ };
154
+ actions = insert(actions, resolved);
155
+ }
156
+ }
157
+ return actions;
158
+ }
159
+ }
160
+
161
+ export function createContextMenuRegistry() { return new ContextMenuRegistry(); }
162
+
163
+ export function createDefaultContextMenuRegistry({ providers = [] } = {}) {
164
+ const registry = createContextMenuRegistry();
165
+ registry.registerProvider(defaultContextMenuProvider.priority, defaultContextMenuProvider);
166
+ for (const provider of providers) registry.registerProvider(provider.priority ?? 500, provider);
167
+ return registry;
168
+ }