@bpmn-nova/studio 0.3.0-preview → 0.3.2-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.
- package/README.md +245 -20
- package/dist/canvas.js +7 -4
- package/dist/context-menu.js +2 -2
- package/dist/controller.js +84 -6
- package/dist/index.d.ts +138 -38
- package/dist/index.js +12 -11
- package/dist/interactions.js +1 -1
- package/dist/modules/bpmn-model/index.d.ts +11 -0
- package/dist/modules/bpmn-model/index.js +703 -0
- package/dist/modules/core/containment.js +590 -0
- package/dist/modules/core/gateway.js +72 -0
- package/dist/modules/core/history.js +32 -0
- package/dist/modules/core/index.d.ts +268 -0
- package/dist/modules/core/index.js +7 -0
- package/dist/modules/core/layout.js +644 -0
- package/dist/modules/core/model.js +287 -0
- package/dist/modules/core/runtime-transition-route.js +360 -0
- package/dist/modules/core/scope.js +99 -0
- package/dist/modules/designer/index.d.ts +79 -0
- package/dist/modules/designer/index.js +607 -0
- package/dist/modules/engine-activiti/index.d.ts +19 -0
- package/dist/modules/engine-activiti/index.js +160 -0
- package/dist/modules/engine-flowable/index.d.ts +19 -0
- package/dist/modules/engine-flowable/index.js +160 -0
- package/dist/modules/export-svg/index.d.ts +112 -0
- package/dist/modules/export-svg/index.js +2 -0
- package/dist/modules/export-svg/preview.js +327 -0
- package/dist/modules/export-svg/render.js +718 -0
- package/dist/modules/icons/index.d.ts +24 -0
- package/dist/modules/icons/index.js +264 -0
- package/dist/modules/palette/index.d.ts +74 -0
- package/dist/modules/palette/index.js +99 -0
- package/dist/modules/palette/panel.js +99 -0
- package/dist/modules/properties/index.d.ts +20 -0
- package/dist/modules/properties/index.js +19 -0
- package/dist/modules/properties-activiti/index.d.ts +3 -0
- package/dist/modules/properties-activiti/index.js +97 -0
- package/dist/modules/properties-bpmn/index.d.ts +3 -0
- package/dist/modules/properties-bpmn/index.js +518 -0
- package/dist/modules/properties-core/index.d.ts +124 -0
- package/dist/modules/properties-core/index.js +312 -0
- package/dist/modules/properties-flowable/index.d.ts +3 -0
- package/dist/modules/properties-flowable/index.js +114 -0
- package/dist/modules/properties-renderer/index.d.ts +25 -0
- package/dist/modules/properties-renderer/index.js +491 -0
- package/dist/modules/renderer-svg/index.d.ts +118 -0
- package/dist/modules/renderer-svg/index.js +1460 -0
- package/dist/modules/runtime/index.d.ts +169 -0
- package/dist/modules/runtime/index.js +535 -0
- package/dist/modules/theme/index.d.ts +95 -0
- package/dist/modules/theme/index.js +368 -0
- package/dist/modules/viewer/index.d.ts +265 -0
- package/dist/modules/viewer/index.js +1011 -0
- package/dist/modules/viewer/runtime-content.js +123 -0
- package/dist/modules/viewer/runtime-details-motion.js +228 -0
- package/dist/modules/viewer/runtime-trace.js +574 -0
- package/dist/modules/viewer/timeline.js +276 -0
- package/dist/selection-layout.js +1 -1
- package/dist/shell.js +210 -26
- package/dist/styles.css +116 -7
- package/llms-full.txt +3082 -0
- package/llms.txt +225 -0
- package/package.json +39 -16
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { NODE_DEFINITIONS } from '../core/index.js';
|
|
2
|
+
import { normalizeOptions } from '../properties-core/index.js';
|
|
3
|
+
import { createDefaultIconRegistry, hydrateIcons, iconIdForNode } from '../icons/index.js';
|
|
4
|
+
import { ThemeController } from '../theme/index.js';
|
|
5
|
+
|
|
6
|
+
const escapeHtml = (value = '') => String(value)
|
|
7
|
+
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
|
8
|
+
.replaceAll('"', '"').replaceAll("'", ''');
|
|
9
|
+
|
|
10
|
+
function valueText(value) {
|
|
11
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
12
|
+
if (value === true) return 'true';
|
|
13
|
+
if (value === false) return 'false';
|
|
14
|
+
return value ?? '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function optionHtml(options, value) {
|
|
18
|
+
return normalizeOptions(options).map((option) => `<option value="${escapeHtml(option.value)}" ${String(option.value) === String(value ?? '') ? 'selected' : ''}>${escapeHtml(option.label)}</option>`).join('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let selectSeed = 0;
|
|
22
|
+
function customSelectHtml(options, value, nativeAttrs, { label = '选择', compact = false, disabled = false } = {}) {
|
|
23
|
+
const normalized = normalizeOptions(options);
|
|
24
|
+
const selectedValue = String(value ?? '');
|
|
25
|
+
const selected = normalized.find((option) => String(option.value) === selectedValue);
|
|
26
|
+
const selectedLabel = selected?.label ?? normalized[0]?.label ?? '请选择';
|
|
27
|
+
const listboxId = `nova-property-select-${++selectSeed}`;
|
|
28
|
+
const optionList = normalized.map((option, index) => {
|
|
29
|
+
const isSelected = String(option.value) === selectedValue;
|
|
30
|
+
return `<button type="button" class="property-select-option ${isSelected ? 'is-selected' : ''}" role="option" aria-selected="${isSelected ? 'true' : 'false'}" tabindex="-1" data-property-select-option data-option-index="${index}" data-value="${escapeHtml(option.value)}"><span>${escapeHtml(option.label)}</span><span class="nova-icon nova-icon-sm property-select-check" data-icon="ui.check"></span></button>`;
|
|
31
|
+
}).join('');
|
|
32
|
+
return `<div class="property-select ${compact ? 'property-select-compact' : ''}" data-property-select>
|
|
33
|
+
<button type="button" class="property-select-trigger" data-property-select-trigger aria-label="${escapeHtml(`${label}:${selectedLabel}`)}" aria-haspopup="listbox" aria-expanded="false" aria-controls="${listboxId}" ${disabled ? 'disabled' : ''}><span class="property-select-value">${escapeHtml(selectedLabel)}</span><span class="nova-icon nova-icon-sm property-select-chevron" data-icon="ui.chevron"></span></button>
|
|
34
|
+
<div class="property-select-popover" id="${listboxId}" role="listbox" aria-label="${escapeHtml(label)}" hidden>${optionList || '<div class="property-select-empty">暂无选项</div>'}</div>
|
|
35
|
+
<select class="property-select-native" data-property-select-native tabindex="-1" aria-hidden="true" hidden ${nativeAttrs}>${optionHtml(normalized, value)}</select>
|
|
36
|
+
</div>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveMaybe(value, context) {
|
|
40
|
+
return typeof value === 'function' ? value(context) : value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fieldShell(entry, control, error = null) {
|
|
44
|
+
const required = entry.required ? '<span class="property-required">*</span>' : '';
|
|
45
|
+
const note = entry.note ? `<div class="property-note">${escapeHtml(entry.note)}</div>` : '';
|
|
46
|
+
const validation = error ? `<div class="property-error">${escapeHtml(error)}</div>` : '';
|
|
47
|
+
const label = entry.label ? `<label class="property-label">${escapeHtml(entry.label)}${required}</label>` : '';
|
|
48
|
+
const variant = entry.variant ? ` property-variant-${escapeHtml(entry.variant)}` : '';
|
|
49
|
+
return `<div class="property-field property-type-${escapeHtml(entry.type || 'text')}${variant}" data-entry-id="${escapeHtml(entry.id)}">${label}${control}${note}${validation}</div>`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderBasicEntry(entry, value, context) {
|
|
53
|
+
const attrs = `data-property-input="${escapeHtml(entry.id)}" ${entry.readonly ? 'disabled' : ''}`;
|
|
54
|
+
const placeholder = entry.placeholder ? `placeholder="${escapeHtml(entry.placeholder)}"` : '';
|
|
55
|
+
if (entry.type === 'readonly') return fieldShell(entry, `<div class="property-readonly">${escapeHtml(resolveMaybe(entry.getValue ? value : (entry.value ?? value), context) || '—')}</div>`);
|
|
56
|
+
if (entry.type === 'textarea' || entry.type === 'code' || entry.type === 'expression') {
|
|
57
|
+
const cls = entry.type === 'code' ? ' property-code' : entry.type === 'expression' ? ' property-expression' : '';
|
|
58
|
+
return fieldShell(entry, `<textarea class="property-control${cls}" rows="${entry.rows || (entry.type === 'code' ? 8 : 4)}" ${attrs} ${placeholder}>${escapeHtml(valueText(value))}</textarea>`);
|
|
59
|
+
}
|
|
60
|
+
if (entry.type === 'select') {
|
|
61
|
+
const options = resolveMaybe(entry.options, context) || [];
|
|
62
|
+
return fieldShell(entry, customSelectHtml(options, value, attrs, { label: entry.label, disabled: entry.readonly }));
|
|
63
|
+
}
|
|
64
|
+
if (entry.type === 'switch') {
|
|
65
|
+
return fieldShell(entry, `<button type="button" class="property-switch ${value ? 'is-on' : ''}" data-property-switch="${escapeHtml(entry.id)}" aria-pressed="${value ? 'true' : 'false'}"><span></span><strong>${value ? '开启' : '关闭'}</strong></button>`);
|
|
66
|
+
}
|
|
67
|
+
if (entry.type === 'range') {
|
|
68
|
+
return fieldShell(entry, `<div class="property-range"><input type="range" min="${entry.min ?? 0}" max="${entry.max ?? 100}" step="${entry.step ?? 1}" value="${Number(value ?? entry.min ?? 0)}" ${attrs}/><span data-range-value="${escapeHtml(entry.id)}">${escapeHtml(valueText(value))}${escapeHtml(entry.unit || '')}</span></div>`);
|
|
69
|
+
}
|
|
70
|
+
if (entry.type === 'action') {
|
|
71
|
+
const actionIcon = entry.iconId ? `<span class="nova-icon nova-icon-sm" data-icon="${escapeHtml(entry.iconId)}"></span>` : '';
|
|
72
|
+
return fieldShell(entry, `<button type="button" class="property-action ${entry.variant === 'danger' ? 'is-danger' : ''}" data-property-action="${escapeHtml(entry.id)}">${actionIcon}<span>${escapeHtml(entry.actionLabel || entry.label || '执行')}</span></button>`);
|
|
73
|
+
}
|
|
74
|
+
if (entry.type === 'tags') {
|
|
75
|
+
return fieldShell(entry, `<input class="property-control" type="text" value="${escapeHtml(valueText(value))}" ${attrs} ${placeholder}/><div class="property-inline-hint">使用逗号分隔多个值;也可填写表达式。</div>`);
|
|
76
|
+
}
|
|
77
|
+
if (entry.type === 'number') {
|
|
78
|
+
return fieldShell(entry, `<input class="property-control" type="number" value="${escapeHtml(valueText(value))}" min="${entry.min ?? ''}" max="${entry.max ?? ''}" step="${entry.step ?? 1}" ${attrs} ${placeholder}/>`);
|
|
79
|
+
}
|
|
80
|
+
return fieldShell(entry, `<input class="property-control" type="text" value="${escapeHtml(valueText(value))}" ${attrs} ${placeholder}/>`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderSmartSelect(entry, value, context, panelId) {
|
|
84
|
+
const options = smartOptions(entry, context);
|
|
85
|
+
const listId = `${panelId}-${entry.id}-list`;
|
|
86
|
+
const optionList = Array.isArray(options) ? options : [];
|
|
87
|
+
const datalist = `<datalist id="${escapeHtml(listId)}">${normalizeOptions(optionList).map((option) => `<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>`).join('')}</datalist>`;
|
|
88
|
+
return fieldShell(entry, `<div class="property-smart-select"><input class="property-control" list="${escapeHtml(listId)}" type="text" value="${escapeHtml(valueText(value))}" data-property-input="${escapeHtml(entry.id)}" placeholder="${escapeHtml(entry.placeholder || '选择或输入值')}"/><span class="nova-icon nova-icon-sm property-smart-icon" data-icon="ui.chevron"></span></div>${datalist}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function smartOptions(entry, context) {
|
|
92
|
+
if (entry.type === 'resource-select') return resolveMaybe(entry.options, context) || [];
|
|
93
|
+
if (entry.type === 'node-select') {
|
|
94
|
+
return context.model.nodes.filter((node) => !entry.nodeFilter || entry.nodeFilter(node, context)).map((node) => ({ value: node.id, label: `${node.name} · ${node.id}` }));
|
|
95
|
+
}
|
|
96
|
+
const provider = entry.dataProvider ? context.data(entry.dataProvider) : null;
|
|
97
|
+
if (!provider) return [];
|
|
98
|
+
if (entry.type === 'user-select') {
|
|
99
|
+
const method = provider.searchUsers || provider.listUsers || provider.list;
|
|
100
|
+
return typeof method === 'function' ? method.call(provider, '') : [];
|
|
101
|
+
}
|
|
102
|
+
if (entry.type === 'form-select') {
|
|
103
|
+
const method = provider.listForms || provider.list;
|
|
104
|
+
return typeof method === 'function' ? method.call(provider) : [];
|
|
105
|
+
}
|
|
106
|
+
if (entry.type === 'process-select') {
|
|
107
|
+
const method = provider.listProcesses || provider.list;
|
|
108
|
+
return typeof method === 'function' ? method.call(provider) : [];
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderNodeList(entry, value, context) {
|
|
114
|
+
const rows = (value || []).map((id) => context.model.nodes.find((node) => node.id === id)).filter(Boolean);
|
|
115
|
+
const content = rows.length ? rows.map((node) => {
|
|
116
|
+
const definition = NODE_DEFINITIONS[node.type] || NODE_DEFINITIONS.generic;
|
|
117
|
+
const label = node.name || definition?.label || node.id;
|
|
118
|
+
const typeLabel = definition?.label || node.type || '流程节点';
|
|
119
|
+
return `<button type="button" class="property-node-ref" data-node-ref="${escapeHtml(node.id)}" aria-label="${escapeHtml(`定位到节点:${label}`)}">
|
|
120
|
+
<span class="property-node-ref-icon"><span class="nova-icon nova-icon-sm" data-icon="${escapeHtml(iconIdForNode(node.type, definition))}"></span></span>
|
|
121
|
+
<span class="property-node-ref-copy"><strong title="${escapeHtml(label)}">${escapeHtml(label)}</strong><small title="${escapeHtml(`${typeLabel} · ${node.id}`)}">${escapeHtml(typeLabel)} · ${escapeHtml(node.id)}</small></span>
|
|
122
|
+
<span class="property-node-ref-locate nova-icon nova-icon-xs" data-icon="ui.locate"></span>
|
|
123
|
+
</button>`;
|
|
124
|
+
}).join('') : '<div class="property-empty-row">暂无节点引用</div>';
|
|
125
|
+
return fieldShell(entry, `<div class="property-node-list">${content}</div>`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderFlowList(entry, value, context) {
|
|
129
|
+
const rows = Array.isArray(value) ? value : [];
|
|
130
|
+
const content = rows.length ? rows.map((edge, index) => {
|
|
131
|
+
const relation = entry.relation === 'target' ? 'target' : 'source';
|
|
132
|
+
const related = context.model.nodes.find((node) => node.id === edge[relation]);
|
|
133
|
+
return `<button type="button" class="property-flow-ref" data-flow-ref="${escapeHtml(edge.id)}">
|
|
134
|
+
<span class="property-flow-index">${index + 1}</span>
|
|
135
|
+
<span class="property-flow-copy"><strong>${escapeHtml(related?.name || edge[relation] || '未连接')}</strong><small>${escapeHtml(edge.name || edge.id)}</small></span>
|
|
136
|
+
<span class="nova-icon nova-icon-xs" data-icon="ui.locate"></span>
|
|
137
|
+
</button>`;
|
|
138
|
+
}).join('') : `<div class="property-empty-row">${escapeHtml(entry.emptyText || '暂无线路')}</div>`;
|
|
139
|
+
return fieldShell(entry, `<div class="property-flow-list">${content}</div>`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderTable(entry, value) {
|
|
143
|
+
const rows = Array.isArray(value) ? value : [];
|
|
144
|
+
const columns = entry.columns || [];
|
|
145
|
+
const head = columns.map((column) => `<span>${escapeHtml(column.label)}</span>`).join('') + '<span></span>';
|
|
146
|
+
const rowHtml = rows.map((row, rowIndex) => {
|
|
147
|
+
const cells = columns.map((column) => {
|
|
148
|
+
const cellValue = row?.[column.key] ?? '';
|
|
149
|
+
if (column.type === 'select') return customSelectHtml(column.options || [], cellValue, `data-table-input data-table-entry="${escapeHtml(entry.id)}" data-row="${rowIndex}" data-key="${escapeHtml(column.key)}"`, { label: column.label, compact: true });
|
|
150
|
+
return `<input class="property-table-input" type="text" value="${escapeHtml(cellValue)}" data-table-entry="${escapeHtml(entry.id)}" data-row="${rowIndex}" data-key="${escapeHtml(column.key)}" placeholder="${escapeHtml(column.placeholder || '')}"/>`;
|
|
151
|
+
}).join('');
|
|
152
|
+
return `<div class="property-table-row" style="--property-columns:${Math.max(1, columns.length)}">${cells}<button type="button" class="property-row-remove" data-table-remove="${escapeHtml(entry.id)}" data-row="${rowIndex}" title="删除" aria-label="删除"><span class="nova-icon nova-icon-sm" data-icon="ui.delete"></span></button></div>`;
|
|
153
|
+
}).join('');
|
|
154
|
+
return fieldShell(entry, `<div class="property-table"><div class="property-table-head" style="--property-columns:${Math.max(1, columns.length)}">${head}</div>${rowHtml || '<div class="property-empty-row">暂无配置</div>'}<button type="button" class="property-row-add" data-table-add="${escapeHtml(entry.id)}"><span class="nova-icon nova-icon-sm" data-icon="ui.add"></span><span>添加${escapeHtml(entry.rowLabel || '一项')}</span></button></div>`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderBranchList(entry, value, context) {
|
|
158
|
+
const rows = Array.isArray(value) ? value : [];
|
|
159
|
+
const fields = new Set(entry.fields || ['name', 'condition', 'default']);
|
|
160
|
+
const html = rows.length ? rows.map((row, index) => {
|
|
161
|
+
const target = context.model.nodes.find((node) => node.id === row.target);
|
|
162
|
+
return `<div class="property-branch-card ${row.isDefault ? 'is-default' : ''}" data-branch-index="${index}">
|
|
163
|
+
<div class="property-branch-head">
|
|
164
|
+
<div class="property-branch-identity"><span>${index + 1}</span><div><strong>分支 ${index + 1}</strong><small>Sequence Flow</small></div></div>
|
|
165
|
+
<button type="button" class="property-branch-locate" data-branch-select="${index}"><span>定位</span><span class="nova-icon nova-icon-xs" data-icon="ui.locate"></span></button>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="property-branch-condition ${fields.size === 1 ? 'is-single-field' : ''}">
|
|
168
|
+
${fields.has('name') ? `<label><span>出口名称</span><input class="property-control" type="text" value="${escapeHtml(row.name || '')}" data-branch-field="name" data-row="${index}" placeholder="例如:采购审批"/></label>` : ''}
|
|
169
|
+
${fields.has('condition') ? `<label><span>条件表达式</span><textarea class="property-control property-expression" rows="2" data-branch-field="condition" data-row="${index}" placeholder="${escapeHtml('${condition}')}">${escapeHtml(row.condition || '')}</textarea></label>` : ''}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="property-branch-foot ${fields.has('default') ? '' : 'is-target-only'}">
|
|
172
|
+
${fields.has('default') ? `<label class="property-branch-default ${row.isDefault ? 'is-active' : ''}"><input type="radio" name="default-branch-${escapeHtml(context.element.id)}" data-branch-default="${index}" ${row.isDefault ? 'checked' : ''}/><i></i><span>默认线路</span></label>` : ''}
|
|
173
|
+
<div class="property-branch-target"><small>流向</small><strong>${escapeHtml(target?.name || row.target || '未连接')}</strong><span>→</span></div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>`;
|
|
176
|
+
}).join('') : '<div class="property-empty-row">当前网关没有出口分支</div>';
|
|
177
|
+
return fieldShell(entry, `<div class="property-branch-list">${html}</div>`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let panelSeed = 0;
|
|
181
|
+
export class PropertiesPanel {
|
|
182
|
+
constructor({ container, registry, studio = registry?.studio, designer = null, canvas = null, iconRegistry = createDefaultIconRegistry(), emptyText = '请选择元素查看属性', themeController = null, theme = null, onThemeChange = null } = {}) {
|
|
183
|
+
if (!container) throw new Error('PropertiesPanel requires a container.');
|
|
184
|
+
if (!registry) throw new Error('PropertiesPanel requires a PropertiesRegistry.');
|
|
185
|
+
this.container = container;
|
|
186
|
+
this.registry = registry;
|
|
187
|
+
this.studio = studio || designer;
|
|
188
|
+
this.designer = this.studio;
|
|
189
|
+
this.canvas = canvas;
|
|
190
|
+
this.iconRegistry = iconRegistry;
|
|
191
|
+
this.registry.setStudio(this.studio);
|
|
192
|
+
this.emptyText = emptyText;
|
|
193
|
+
this.collapseState = new Map();
|
|
194
|
+
this.id = `nova-properties-${++panelSeed}`;
|
|
195
|
+
this.container.classList.add('nova-properties');
|
|
196
|
+
this.themeController = themeController || new ThemeController({ root: container, theme, onChange: onThemeChange });
|
|
197
|
+
this._ownsThemeController = !themeController;
|
|
198
|
+
this._customCleanups = [];
|
|
199
|
+
this._offStudio = this.studio?.subscribe?.((event) => {
|
|
200
|
+
if (['selectionChanged', 'modelChanged', 'propertiesProfileChanged'].includes(event.type)) this.render();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
destroy() {
|
|
205
|
+
this._customCleanups.forEach((cleanup) => cleanup?.());
|
|
206
|
+
this._customCleanups = [];
|
|
207
|
+
this._offStudio?.();
|
|
208
|
+
this.container.innerHTML = '';
|
|
209
|
+
this.container.classList.remove('nova-properties');
|
|
210
|
+
if (this._ownsThemeController) this.themeController.destroy();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setTheme(theme) { return this.themeController.setTheme(theme); }
|
|
214
|
+
setThemeMode(mode) { return this.themeController.setMode(mode); }
|
|
215
|
+
getThemeState() { return this.themeController.getState(); }
|
|
216
|
+
|
|
217
|
+
render(selection, element) {
|
|
218
|
+
this._customCleanups.forEach((cleanup) => cleanup?.());
|
|
219
|
+
this._customCleanups = [];
|
|
220
|
+
const { groups, context } = this.registry.getGroups(selection, element);
|
|
221
|
+
const header = this._header(context);
|
|
222
|
+
const groupHtml = groups.map((group) => this._group(group, context)).join('');
|
|
223
|
+
const emptyContent = context.kind === 'multi' ? this._multiSummary(context) : `<div class="properties-empty">${escapeHtml(this.emptyText)}</div>`;
|
|
224
|
+
this.container.innerHTML = `${header}<div class="properties-body">${groupHtml || emptyContent}</div>`;
|
|
225
|
+
hydrateIcons(this.container, this.iconRegistry);
|
|
226
|
+
this._bind(groups, context);
|
|
227
|
+
this._renderCustomEntries(groups, context);
|
|
228
|
+
return { groups, context };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_multiSummary(context) {
|
|
232
|
+
const labels = { task: '任务', event: '事件', gateway: '网关', container: '子流程', boundary: '边界事件' };
|
|
233
|
+
const counts = new Map();
|
|
234
|
+
for (const element of context.element || []) {
|
|
235
|
+
const definition = element?.type ? NODE_DEFINITIONS[element.type] : null;
|
|
236
|
+
if (!definition) continue;
|
|
237
|
+
const label = labels[definition.kind] || '其他';
|
|
238
|
+
counts.set(label, (counts.get(label) || 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
const chips = [...counts].map(([label, count]) => `<span>${escapeHtml(label)} <strong>${count}</strong></span>`).join('');
|
|
241
|
+
return `<section class="properties-multi-summary">
|
|
242
|
+
<div class="properties-multi-icon"><span class="nova-icon" data-icon="ui.layoutSelection"></span></div>
|
|
243
|
+
<strong>批量布局选区</strong>
|
|
244
|
+
<p>在画布上使用悬浮工具栏进行对齐、分布和局部美化。</p>
|
|
245
|
+
<div class="properties-multi-counts">${chips}</div>
|
|
246
|
+
${this.canvas ? '<button type="button" data-fit-selection><span class="nova-icon nova-icon-sm" data-icon="ui.fitSelection"></span><span>适应选区</span></button>' : ''}
|
|
247
|
+
</section>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_header(context) {
|
|
251
|
+
const profile = context.profile || 'business';
|
|
252
|
+
const profileSwitch = `<div class="properties-profile-switch" aria-label="属性视图"><button type="button" data-properties-profile="business" class="${profile === 'business' ? 'is-active' : ''}">业务</button><button type="button" data-properties-profile="developer" class="${profile === 'developer' ? 'is-active' : ''}">开发者</button></div>`;
|
|
253
|
+
const renderHeader = (title, className = '') => `<div class="properties-head ${className}"><div class="properties-title-row"><div class="properties-title">${escapeHtml(title)}</div>${profileSwitch}</div></div>`;
|
|
254
|
+
if (context.kind === 'none') return renderHeader('未选择元素');
|
|
255
|
+
if (context.kind === 'multi') return renderHeader(`已选择 ${context.element.length} 个元素`);
|
|
256
|
+
if (context.kind === 'process') return renderHeader('流程与画布', 'properties-head-process');
|
|
257
|
+
if (context.kind === 'edge') {
|
|
258
|
+
return renderHeader(context.element.name || (context.element.type === 'association' ? '关联' : '流程分支'));
|
|
259
|
+
}
|
|
260
|
+
return renderHeader(context.element.name || context.definition?.label || '未命名元素');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_group(group, context) {
|
|
264
|
+
const key = `${context.kind}:${context.element?.id || context.model.id}:${group.id}`;
|
|
265
|
+
const collapsed = this.collapseState.has(key) ? this.collapseState.get(key) : !!group.collapsed;
|
|
266
|
+
const count = this.registry.configuredCount(group, context);
|
|
267
|
+
const description = group.description ? `<div class="properties-group-description">${escapeHtml(group.description)}</div>` : '';
|
|
268
|
+
const entries = group.entries.map((entry) => this._entry(entry, context)).join('');
|
|
269
|
+
return `<section class="properties-group ${collapsed ? 'is-collapsed' : ''}" data-group-id="${escapeHtml(group.id)}" data-group-key="${escapeHtml(key)}">
|
|
270
|
+
<button type="button" class="properties-group-toggle" data-group-toggle="${escapeHtml(group.id)}" aria-expanded="${collapsed ? 'false' : 'true'}"><span class="nova-icon nova-icon-sm properties-chevron" data-icon="ui.chevron"></span><strong>${escapeHtml(group.label)}</strong>${count ? `<span class="properties-count">${count}</span>` : ''}</button>
|
|
271
|
+
<div class="properties-group-content">${description}${entries}</div>
|
|
272
|
+
</section>`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_entry(entry, context) {
|
|
276
|
+
const value = this.registry.getValue(entry, context);
|
|
277
|
+
const error = this.registry.validateEntry(entry, context);
|
|
278
|
+
const custom = this.registry.components.get(entry.component || entry.type);
|
|
279
|
+
if (custom) return fieldShell(entry, `<div class="property-custom-host" data-custom-entry="${escapeHtml(entry.id)}"></div>`, error);
|
|
280
|
+
if (entry.type === 'table') return renderTable(entry, value);
|
|
281
|
+
if (entry.type === 'branchList') return renderBranchList(entry, value, context);
|
|
282
|
+
if (entry.type === 'flow-list') return renderFlowList(entry, value, context);
|
|
283
|
+
if (entry.type === 'node-list') return renderNodeList(entry, value, context);
|
|
284
|
+
if (['user-select', 'form-select', 'process-select', 'resource-select', 'node-select'].includes(entry.type)) return renderSmartSelect(entry, value, context, this.id);
|
|
285
|
+
const basic = renderBasicEntry(entry, value, context);
|
|
286
|
+
return error ? basic.replace(/<\/div>$/, `<div class="property-error">${escapeHtml(error)}</div></div>`) : basic;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_bind(groups, context) {
|
|
290
|
+
const entryMap = new Map(groups.flatMap((group) => group.entries.map((entry) => [entry.id, entry])));
|
|
291
|
+
this.container.querySelectorAll('[data-properties-profile]').forEach((button) => button.addEventListener('click', () => this.studio?.setPropertiesProfile(button.dataset.propertiesProfile)));
|
|
292
|
+
this.container.querySelector('[data-fit-selection]')?.addEventListener('click', () => this.canvas?.fitSelection());
|
|
293
|
+
this.container.querySelectorAll('[data-group-toggle]').forEach((button) => button.addEventListener('click', () => {
|
|
294
|
+
const section = button.closest('.properties-group');
|
|
295
|
+
section.classList.toggle('is-collapsed');
|
|
296
|
+
const collapsed = section.classList.contains('is-collapsed');
|
|
297
|
+
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
298
|
+
this.collapseState.set(section.dataset.groupKey, collapsed);
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
this.container.querySelectorAll('[data-property-input]').forEach((input) => {
|
|
302
|
+
const entry = entryMap.get(input.dataset.propertyInput);
|
|
303
|
+
const commit = () => {
|
|
304
|
+
let value = input.value;
|
|
305
|
+
if (entry.type === 'number' || entry.type === 'range') value = input.value === '' ? '' : Number(input.value);
|
|
306
|
+
this.registry.setValue(entry, value, context);
|
|
307
|
+
};
|
|
308
|
+
input.addEventListener('change', commit);
|
|
309
|
+
if (input.type === 'range') input.addEventListener('input', () => {
|
|
310
|
+
const label = this.container.querySelector(`[data-range-value="${CSS.escape(entry.id)}"]`);
|
|
311
|
+
if (label) label.textContent = `${input.value}${entry.unit || ''}`;
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this.container.querySelectorAll('[data-property-switch]').forEach((button) => button.addEventListener('click', () => {
|
|
316
|
+
const entry = entryMap.get(button.dataset.propertySwitch);
|
|
317
|
+
const value = !button.classList.contains('is-on');
|
|
318
|
+
this.registry.setValue(entry, value, context);
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
this.container.querySelectorAll('[data-property-action]').forEach((button) => button.addEventListener('click', () => {
|
|
322
|
+
const entry = entryMap.get(button.dataset.propertyAction);
|
|
323
|
+
entry?.action?.(context);
|
|
324
|
+
}));
|
|
325
|
+
|
|
326
|
+
this.container.querySelectorAll('[data-table-input]').forEach((input) => input.addEventListener('change', () => {
|
|
327
|
+
const entry = entryMap.get(input.dataset.tableEntry);
|
|
328
|
+
const rows = JSON.parse(JSON.stringify(this.registry.getValue(entry, context) || []));
|
|
329
|
+
const row = rows[Number(input.dataset.row)] || {};
|
|
330
|
+
row[input.dataset.key] = input.value;
|
|
331
|
+
rows[Number(input.dataset.row)] = row;
|
|
332
|
+
this.registry.setValue(entry, rows, context);
|
|
333
|
+
}));
|
|
334
|
+
this.container.querySelectorAll('[data-table-add]').forEach((button) => button.addEventListener('click', () => {
|
|
335
|
+
const entry = entryMap.get(button.dataset.tableAdd);
|
|
336
|
+
const rows = JSON.parse(JSON.stringify(this.registry.getValue(entry, context) || []));
|
|
337
|
+
rows.push(typeof entry.defaultRow === 'function' ? entry.defaultRow(context) : { ...(entry.defaultRow || {}) });
|
|
338
|
+
this.registry.setValue(entry, rows, context);
|
|
339
|
+
}));
|
|
340
|
+
this.container.querySelectorAll('[data-table-remove]').forEach((button) => button.addEventListener('click', () => {
|
|
341
|
+
const entry = entryMap.get(button.dataset.tableRemove);
|
|
342
|
+
const rows = JSON.parse(JSON.stringify(this.registry.getValue(entry, context) || []));
|
|
343
|
+
rows.splice(Number(button.dataset.row), 1);
|
|
344
|
+
this.registry.setValue(entry, rows, context);
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
this._bindCustomSelects();
|
|
348
|
+
|
|
349
|
+
const branchEntry = groups.flatMap((group) => group.entries).find((entry) => entry.type === 'branchList');
|
|
350
|
+
if (branchEntry) {
|
|
351
|
+
const commitBranches = () => {
|
|
352
|
+
const rows = JSON.parse(JSON.stringify(this.registry.getValue(branchEntry, context) || []));
|
|
353
|
+
this.container.querySelectorAll('[data-branch-field]').forEach((input) => { rows[Number(input.dataset.row)][input.dataset.branchField] = input.value; });
|
|
354
|
+
if (!branchEntry.fields || branchEntry.fields.includes('default')) {
|
|
355
|
+
const checked = this.container.querySelector('[data-branch-default]:checked');
|
|
356
|
+
rows.forEach((row, index) => { row.isDefault = !!checked && Number(checked.dataset.branchDefault) === index; });
|
|
357
|
+
}
|
|
358
|
+
this.registry.setValue(branchEntry, rows, context);
|
|
359
|
+
};
|
|
360
|
+
this.container.querySelectorAll('[data-branch-field]').forEach((input) => input.addEventListener('change', commitBranches));
|
|
361
|
+
this.container.querySelectorAll('[data-branch-default]').forEach((input) => input.addEventListener('change', commitBranches));
|
|
362
|
+
this.container.querySelectorAll('[data-branch-select]').forEach((button) => button.addEventListener('click', () => {
|
|
363
|
+
const rows = this.registry.getValue(branchEntry, context) || [];
|
|
364
|
+
branchEntry.selectRow?.(context, rows[Number(button.dataset.branchSelect)]);
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.container.querySelectorAll('[data-node-ref]').forEach((button) => button.addEventListener('click', () => context.studio.select({ kind: 'node', id: button.dataset.nodeRef })));
|
|
369
|
+
this.container.querySelectorAll('[data-flow-ref]').forEach((button) => button.addEventListener('click', () => context.studio.select({ kind: 'edge', id: button.dataset.flowRef })));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_bindCustomSelects() {
|
|
373
|
+
const ownerDocument = this.container.ownerDocument;
|
|
374
|
+
const wrappers = [...this.container.querySelectorAll('[data-property-select]')];
|
|
375
|
+
if (!wrappers.length) return;
|
|
376
|
+
|
|
377
|
+
const optionNodes = (wrapper) => [...wrapper.querySelectorAll('[data-property-select-option]')];
|
|
378
|
+
const close = (wrapper, restoreFocus = false) => {
|
|
379
|
+
const trigger = wrapper.querySelector('[data-property-select-trigger]');
|
|
380
|
+
const popover = wrapper.querySelector('.property-select-popover');
|
|
381
|
+
wrapper.classList.remove('is-open', 'is-above');
|
|
382
|
+
trigger?.setAttribute('aria-expanded', 'false');
|
|
383
|
+
if (popover) popover.hidden = true;
|
|
384
|
+
optionNodes(wrapper).forEach((option) => option.classList.remove('is-active'));
|
|
385
|
+
if (restoreFocus) trigger?.focus({ preventScroll: true });
|
|
386
|
+
};
|
|
387
|
+
const closeOthers = (current) => wrappers.forEach((wrapper) => {
|
|
388
|
+
if (wrapper !== current) close(wrapper);
|
|
389
|
+
});
|
|
390
|
+
const activate = (wrapper, index, focus = true) => {
|
|
391
|
+
const options = optionNodes(wrapper);
|
|
392
|
+
if (!options.length) return;
|
|
393
|
+
const nextIndex = Math.max(0, Math.min(index, options.length - 1));
|
|
394
|
+
options.forEach((option, optionIndex) => option.classList.toggle('is-active', optionIndex === nextIndex));
|
|
395
|
+
if (focus) options[nextIndex].focus({ preventScroll: true });
|
|
396
|
+
options[nextIndex].scrollIntoView({ block: 'nearest' });
|
|
397
|
+
};
|
|
398
|
+
const open = (wrapper, direction = 1) => {
|
|
399
|
+
const trigger = wrapper.querySelector('[data-property-select-trigger]');
|
|
400
|
+
if (trigger?.disabled) return;
|
|
401
|
+
closeOthers(wrapper);
|
|
402
|
+
const popover = wrapper.querySelector('.property-select-popover');
|
|
403
|
+
wrapper.classList.add('is-open');
|
|
404
|
+
trigger?.setAttribute('aria-expanded', 'true');
|
|
405
|
+
if (popover) popover.hidden = false;
|
|
406
|
+
const rect = popover?.getBoundingClientRect();
|
|
407
|
+
if (rect && rect.bottom > (ownerDocument.defaultView?.innerHeight ?? Infinity) - 8 && wrapper.getBoundingClientRect().top > rect.height + 8) wrapper.classList.add('is-above');
|
|
408
|
+
const options = optionNodes(wrapper);
|
|
409
|
+
const selectedIndex = options.findIndex((option) => option.classList.contains('is-selected'));
|
|
410
|
+
const startIndex = selectedIndex < 0 ? 0 : Math.max(0, Math.min(selectedIndex + (direction < 0 ? -1 : 0), options.length - 1));
|
|
411
|
+
activate(wrapper, startIndex);
|
|
412
|
+
};
|
|
413
|
+
const choose = (wrapper, option) => {
|
|
414
|
+
const nativeSelect = wrapper.querySelector('[data-property-select-native]');
|
|
415
|
+
if (!nativeSelect || !option) return;
|
|
416
|
+
nativeSelect.value = option.dataset.value;
|
|
417
|
+
nativeSelect.dispatchEvent(new ownerDocument.defaultView.Event('change', { bubbles: true }));
|
|
418
|
+
};
|
|
419
|
+
const move = (wrapper, delta) => {
|
|
420
|
+
const options = optionNodes(wrapper);
|
|
421
|
+
const current = options.findIndex((option) => option.classList.contains('is-active'));
|
|
422
|
+
activate(wrapper, current < 0 ? 0 : (current + delta + options.length) % options.length);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
wrappers.forEach((wrapper) => {
|
|
426
|
+
const trigger = wrapper.querySelector('[data-property-select-trigger]');
|
|
427
|
+
const options = optionNodes(wrapper);
|
|
428
|
+
trigger?.addEventListener('click', () => wrapper.classList.contains('is-open') ? close(wrapper) : open(wrapper));
|
|
429
|
+
trigger?.addEventListener('keydown', (event) => {
|
|
430
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter', ' '].includes(event.key)) return;
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
if (!wrapper.classList.contains('is-open')) open(wrapper, event.key === 'ArrowUp' ? -1 : 1);
|
|
433
|
+
else if (event.key === 'ArrowDown') move(wrapper, 1);
|
|
434
|
+
else if (event.key === 'ArrowUp') move(wrapper, -1);
|
|
435
|
+
else if (event.key === 'Home') activate(wrapper, 0);
|
|
436
|
+
else if (event.key === 'End') activate(wrapper, options.length - 1);
|
|
437
|
+
});
|
|
438
|
+
options.forEach((option) => {
|
|
439
|
+
option.addEventListener('pointermove', () => activate(wrapper, Number(option.dataset.optionIndex), false));
|
|
440
|
+
option.addEventListener('click', () => choose(wrapper, option));
|
|
441
|
+
option.addEventListener('keydown', (event) => {
|
|
442
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
443
|
+
event.preventDefault();
|
|
444
|
+
move(wrapper, event.key === 'ArrowDown' ? 1 : -1);
|
|
445
|
+
} else if (event.key === 'Home' || event.key === 'End') {
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
activate(wrapper, event.key === 'Home' ? 0 : options.length - 1);
|
|
448
|
+
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
choose(wrapper, option);
|
|
451
|
+
} else if (event.key === 'Escape') {
|
|
452
|
+
event.preventDefault();
|
|
453
|
+
close(wrapper, true);
|
|
454
|
+
} else if (event.key === 'Tab') close(wrapper);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const onPointerDown = (event) => wrappers.forEach((wrapper) => {
|
|
460
|
+
if (!wrapper.contains(event.target)) close(wrapper);
|
|
461
|
+
});
|
|
462
|
+
const onKeyDown = (event) => {
|
|
463
|
+
if (event.key !== 'Escape') return;
|
|
464
|
+
const openWrapper = wrappers.find((wrapper) => wrapper.classList.contains('is-open'));
|
|
465
|
+
if (openWrapper) close(openWrapper, true);
|
|
466
|
+
};
|
|
467
|
+
ownerDocument.addEventListener('pointerdown', onPointerDown);
|
|
468
|
+
ownerDocument.addEventListener('keydown', onKeyDown);
|
|
469
|
+
this._customCleanups.push(() => {
|
|
470
|
+
ownerDocument.removeEventListener('pointerdown', onPointerDown);
|
|
471
|
+
ownerDocument.removeEventListener('keydown', onKeyDown);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_renderCustomEntries(groups, context) {
|
|
476
|
+
for (const entry of groups.flatMap((group) => group.entries)) {
|
|
477
|
+
const renderer = this.registry.components.get(entry.component || entry.type);
|
|
478
|
+
if (!renderer) continue;
|
|
479
|
+
const host = this.container.querySelector(`[data-custom-entry="${CSS.escape(entry.id)}"]`);
|
|
480
|
+
if (!host) continue;
|
|
481
|
+
const cleanup = renderer({
|
|
482
|
+
container: host,
|
|
483
|
+
entry,
|
|
484
|
+
context,
|
|
485
|
+
value: this.registry.getValue(entry, context),
|
|
486
|
+
commit: (value) => this.registry.setValue(entry, value, context),
|
|
487
|
+
});
|
|
488
|
+
if (typeof cleanup === 'function') this._customCleanups.push(cleanup);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { AlignmentGuide, BpmnEdge, BpmnNode, ElementSelection, Point, ProcessModel, SelectionRect } from '../core/index.js'
|
|
2
|
+
import type { IconRegistry, NodeVisualResolution } from '../icons/index.js'
|
|
3
|
+
import type { NodeRuntimePresentation, ProcessInstanceSnapshot, RuntimePresentation } from '../runtime/index.js'
|
|
4
|
+
import type { NovaThemeInput, NovaThemeMode, NovaThemeState, RuntimeAppearanceResolver, ThemeController } from '../theme/index.js'
|
|
5
|
+
import type { SvgExportArtifact, SvgExportOptions, SvgExportPreviewController, SvgNodeRenderer } from '../export-svg/index.js'
|
|
6
|
+
|
|
7
|
+
export interface SceneBounds {
|
|
8
|
+
left: number
|
|
9
|
+
top: number
|
|
10
|
+
right: number
|
|
11
|
+
bottom: number
|
|
12
|
+
width: number
|
|
13
|
+
height: number
|
|
14
|
+
}
|
|
15
|
+
export type ViewportBounds = Pick<SceneBounds, 'left' | 'top' | 'right' | 'bottom'>
|
|
16
|
+
export function computeSceneBounds(model: ProcessModel, viewportBounds?: ViewportBounds | null, margin?: number): SceneBounds
|
|
17
|
+
export interface EdgeLabelMetrics {
|
|
18
|
+
width: number
|
|
19
|
+
height: number
|
|
20
|
+
paddingX: number
|
|
21
|
+
paddingY: number
|
|
22
|
+
maxWidth: number
|
|
23
|
+
lineHeight: number
|
|
24
|
+
radius: number
|
|
25
|
+
lines: string[]
|
|
26
|
+
}
|
|
27
|
+
export interface EdgeLabelBounds extends SelectionRect {
|
|
28
|
+
left: number
|
|
29
|
+
top: number
|
|
30
|
+
right: number
|
|
31
|
+
bottom: number
|
|
32
|
+
}
|
|
33
|
+
export interface EdgeLabelPlacement {
|
|
34
|
+
x: number
|
|
35
|
+
y: number
|
|
36
|
+
bounds: EdgeLabelBounds
|
|
37
|
+
strategy: 'inline' | 'offset' | 'fallback'
|
|
38
|
+
segmentIndex: number
|
|
39
|
+
}
|
|
40
|
+
export function edgeLabelMetrics(value: string, options?: Partial<Pick<EdgeLabelMetrics, 'paddingX' | 'paddingY' | 'maxWidth' | 'lineHeight' | 'radius'>>): EdgeLabelMetrics
|
|
41
|
+
export function edgeLabelPlacement(points: Point[], metrics: EdgeLabelMetrics, options?: {
|
|
42
|
+
startClearance?: number
|
|
43
|
+
endClearance?: number
|
|
44
|
+
innerClearance?: number
|
|
45
|
+
obstaclePadding?: number
|
|
46
|
+
labelGap?: number
|
|
47
|
+
offsetStep?: number
|
|
48
|
+
maxOffset?: number
|
|
49
|
+
obstacles?: Array<SelectionRect | ViewportBounds>
|
|
50
|
+
}): EdgeLabelPlacement
|
|
51
|
+
|
|
52
|
+
export interface DiagramNodeRendererContext {
|
|
53
|
+
container: HTMLElement
|
|
54
|
+
node: BpmnNode
|
|
55
|
+
definition: Record<string, unknown>
|
|
56
|
+
runtimeState: NodeRuntimePresentation
|
|
57
|
+
runtimePresentation: NodeRuntimePresentation
|
|
58
|
+
visual: NodeVisualResolution
|
|
59
|
+
semanticIconId: string | null
|
|
60
|
+
mode: 'design' | 'viewer' | 'instance'
|
|
61
|
+
model: ProcessModel
|
|
62
|
+
themeState: NovaThemeState
|
|
63
|
+
runtimeAppearance: RuntimeAppearanceResolver | null
|
|
64
|
+
select(): void
|
|
65
|
+
openRuntimeDetails(anchor?: Element): void
|
|
66
|
+
renderSemanticIcon(target?: Element, options?: { className?: string; title?: string }): SVGSVGElement | null
|
|
67
|
+
}
|
|
68
|
+
export type DiagramNodeRenderer = (context: DiagramNodeRendererContext) => void | (() => void)
|
|
69
|
+
export interface DiagramRendererOptions {
|
|
70
|
+
model?: ProcessModel
|
|
71
|
+
mode?: 'design' | 'viewer' | 'instance'
|
|
72
|
+
runtime?: ProcessInstanceSnapshot | null
|
|
73
|
+
iconRegistry?: IconRegistry
|
|
74
|
+
nodeRenderers?: Record<string, DiagramNodeRenderer>
|
|
75
|
+
nodeRenderer?: DiagramNodeRenderer
|
|
76
|
+
runtimePresenter?: (context: { model: ProcessModel; runtime: ProcessInstanceSnapshot | null; appearance: RuntimeAppearanceResolver | null }) => RuntimePresentation
|
|
77
|
+
runtimeAppearance?: RuntimeAppearanceResolver | null
|
|
78
|
+
svgExport?: SvgExportOptions & { label?: string; nodeRenderers?: Record<string, SvgNodeRenderer> }
|
|
79
|
+
themeController?: ThemeController
|
|
80
|
+
theme?: NovaThemeInput
|
|
81
|
+
onThemeChange?: (state: NovaThemeState) => void
|
|
82
|
+
onNodeClick?: (node: BpmnNode, event: Event) => void
|
|
83
|
+
onNodePointerDown?: (node: BpmnNode, event: PointerEvent) => void
|
|
84
|
+
onEdgeClick?: (edge: BpmnEdge, event: Event) => void
|
|
85
|
+
onCanvasClick?: (event: Event) => void
|
|
86
|
+
onViewportChange?: (state: { zoom: number; pan: Point }) => void
|
|
87
|
+
canCreateNodeType?: (nodeType: string) => boolean
|
|
88
|
+
[key: string]: unknown
|
|
89
|
+
}
|
|
90
|
+
export class DiagramRenderer {
|
|
91
|
+
constructor(container: HTMLElement, options?: DiagramRendererOptions)
|
|
92
|
+
model: ProcessModel
|
|
93
|
+
screenToWorld(clientX: number, clientY: number): Point
|
|
94
|
+
openQuickMenu(nodeId: string): void
|
|
95
|
+
editEdgeName(edgeId: string): void
|
|
96
|
+
closeTransientOverlays(): void
|
|
97
|
+
setAlignmentGuides(guides?: AlignmentGuide[]): void
|
|
98
|
+
setModel(model: ProcessModel): void
|
|
99
|
+
setMode(mode: 'design' | 'viewer' | 'instance'): void
|
|
100
|
+
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
101
|
+
setSelection(selection: ElementSelection | null): void
|
|
102
|
+
setConnectingSource(nodeId: string | null): void
|
|
103
|
+
setSpacePressed(active: boolean): void
|
|
104
|
+
setInteractionMode(mode: string): void
|
|
105
|
+
getViewportState(): { zoom: number; pan: Point }
|
|
106
|
+
setViewportState(state: { zoom?: number; pan?: Point }): void
|
|
107
|
+
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
108
|
+
setThemeMode(mode: NovaThemeMode): NovaThemeState
|
|
109
|
+
getThemeState(): NovaThemeState
|
|
110
|
+
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact>
|
|
111
|
+
openSvgExportPreview(options?: SvgExportOptions & { previewTitle?: string; onDownload?: (artifact: SvgExportArtifact) => void }): SvgExportPreviewController
|
|
112
|
+
zoomBy(delta: number): void
|
|
113
|
+
fitView(padding?: number, options?: Record<string, unknown>): void
|
|
114
|
+
fitReadable(padding?: number): void
|
|
115
|
+
actualSize(padding?: number): void
|
|
116
|
+
render(): void
|
|
117
|
+
destroy(): void
|
|
118
|
+
}
|