@bpmn-nova/studio 0.3.2-preview → 0.3.4-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 +64 -1
- package/dist/canvas.js +3 -1
- package/dist/index.d.ts +39 -3
- package/dist/modules/export-svg/index.d.ts +2 -0
- package/dist/modules/export-svg/render.js +32 -8
- package/dist/modules/node-presentation/index.d.ts +25 -0
- package/dist/modules/node-presentation/index.js +51 -0
- package/dist/modules/renderer-svg/index.d.ts +3 -0
- package/dist/modules/renderer-svg/index.js +79 -18
- package/dist/modules/viewer/index.d.ts +3 -0
- package/dist/modules/viewer/index.js +21 -4
- package/dist/shell.js +378 -166
- package/dist/styles.css +4 -3
- package/llms-full.txt +367 -59
- package/llms.txt +33 -8
- package/package.json +1 -1
package/dist/shell.js
CHANGED
|
@@ -29,6 +29,11 @@ function mountSlot(slot, container, context) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const STUDIO_MODES = ['design', 'viewer', 'instance'];
|
|
32
|
+
const STUDIO_MODE_OPTIONS = Object.freeze([
|
|
33
|
+
Object.freeze({ value: 'design', label: '流程设计', iconId: 'ui.design' }),
|
|
34
|
+
Object.freeze({ value: 'viewer', label: '流程展示', iconId: 'ui.preview' }),
|
|
35
|
+
Object.freeze({ value: 'instance', label: '审批轨迹', iconId: 'ui.trace' }),
|
|
36
|
+
]);
|
|
32
37
|
const STUDIO_SHELL_REGIONS = ['header', 'left', 'right', 'footer'];
|
|
33
38
|
const DEFAULT_STUDIO_SHELL_REGIONS = Object.freeze({
|
|
34
39
|
header: 'default',
|
|
@@ -37,14 +42,13 @@ const DEFAULT_STUDIO_SHELL_REGIONS = Object.freeze({
|
|
|
37
42
|
footer: 'default',
|
|
38
43
|
});
|
|
39
44
|
|
|
40
|
-
function normalizeAllowedModes(allowedModes) {
|
|
41
|
-
if (allowedModes === undefined || allowedModes === null) return [...STUDIO_MODES];
|
|
45
|
+
function normalizeAllowedModes(allowedModes, { allowDefault = true } = {}) {
|
|
46
|
+
if (allowDefault && (allowedModes === undefined || allowedModes === null)) return [...STUDIO_MODES];
|
|
42
47
|
if (!Array.isArray(allowedModes)) throw new Error('allowedModes must be an array of Studio modes.');
|
|
43
|
-
|
|
44
|
-
if (!normalized.length || normalized.length !== allowedModes.length) {
|
|
48
|
+
if (!allowedModes.length || allowedModes.some((mode) => !STUDIO_MODES.includes(mode)) || new Set(allowedModes).size !== allowedModes.length) {
|
|
45
49
|
throw new Error('allowedModes must contain one or more unique design, viewer, or instance modes.');
|
|
46
50
|
}
|
|
47
|
-
return
|
|
51
|
+
return [...allowedModes];
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function normalizeRegions(regions, current = DEFAULT_STUDIO_SHELL_REGIONS) {
|
|
@@ -71,6 +75,7 @@ export class BpmnStudioShell {
|
|
|
71
75
|
templateRegistry = createTemplateRegistry(),
|
|
72
76
|
contextMenuRegistry = createDefaultContextMenuRegistry(),
|
|
73
77
|
rendererOptions = {},
|
|
78
|
+
nodeSubtitleResolver = rendererOptions.nodeSubtitleResolver,
|
|
74
79
|
slots = {},
|
|
75
80
|
layout = null,
|
|
76
81
|
regions = null,
|
|
@@ -102,7 +107,7 @@ export class BpmnStudioShell {
|
|
|
102
107
|
paletteRegistry,
|
|
103
108
|
templateRegistry,
|
|
104
109
|
contextMenuRegistry,
|
|
105
|
-
rendererOptions,
|
|
110
|
+
rendererOptions: { ...rendererOptions, nodeSubtitleResolver },
|
|
106
111
|
slots,
|
|
107
112
|
runtime,
|
|
108
113
|
mode: resolvedMode,
|
|
@@ -120,6 +125,10 @@ export class BpmnStudioShell {
|
|
|
120
125
|
this.interactions = createInteractionController({ studio, templates: templateRegistry });
|
|
121
126
|
this._cleanups = [];
|
|
122
127
|
this._instances = [];
|
|
128
|
+
this._modeListeners = new Set();
|
|
129
|
+
this._validationListeners = new Set();
|
|
130
|
+
this._modeViewports = new Map();
|
|
131
|
+
this._destroyed = false;
|
|
123
132
|
this._svgExportPreview = null;
|
|
124
133
|
this.actions = Object.freeze({
|
|
125
134
|
undo: () => this.studio.undo(),
|
|
@@ -133,7 +142,7 @@ export class BpmnStudioShell {
|
|
|
133
142
|
requestAnimationFrame(() => this._fitActive());
|
|
134
143
|
},
|
|
135
144
|
fitView: () => this.fitView(),
|
|
136
|
-
validate: () => this.
|
|
145
|
+
validate: () => this._runValidation('toolbar'),
|
|
137
146
|
importXml: (xml, engine) => this.studio.importXml(xml, engine),
|
|
138
147
|
exportXml: (engine) => this.studio.exportXml(engine),
|
|
139
148
|
exportSvg: (options) => this.exportSvg(options),
|
|
@@ -149,6 +158,10 @@ export class BpmnStudioShell {
|
|
|
149
158
|
actions: this.actions,
|
|
150
159
|
getState: () => this.studio.getState(),
|
|
151
160
|
subscribe: (listener) => this.studio.subscribe(listener),
|
|
161
|
+
getMode: () => this.getMode(),
|
|
162
|
+
getAllowedModes: () => this.getAllowedModes(),
|
|
163
|
+
subscribeMode: (listener) => this.subscribeMode(listener),
|
|
164
|
+
subscribeValidation: (listener) => this.subscribeValidation(listener),
|
|
152
165
|
iconRegistry,
|
|
153
166
|
paletteRegistry,
|
|
154
167
|
propertiesRegistry: this.propertiesRegistry,
|
|
@@ -176,20 +189,8 @@ export class BpmnStudioShell {
|
|
|
176
189
|
brandCopy.append(node('strong', '', 'BPMN Nova'), node('span', '', this.studio.model.name));
|
|
177
190
|
brand.append(brandMark, brandCopy);
|
|
178
191
|
const modeSwitch = node('nav', 'nova-studio-mode-switch');
|
|
192
|
+
modeSwitch.setAttribute('aria-label', 'Studio 模式');
|
|
179
193
|
const modeButtons = new Map();
|
|
180
|
-
for (const item of [
|
|
181
|
-
{ value: 'design', label: '流程设计', iconId: 'ui.design' },
|
|
182
|
-
{ value: 'viewer', label: '流程展示', iconId: 'ui.preview' },
|
|
183
|
-
{ value: 'instance', label: '审批轨迹', iconId: 'ui.trace' },
|
|
184
|
-
].filter((item) => this.allowedModes.includes(item.value))) {
|
|
185
|
-
const button = node('button');
|
|
186
|
-
button.type = 'button';
|
|
187
|
-
button.setAttribute('data-mode', item.value);
|
|
188
|
-
button.append(iconNode(item.iconId, 'nova-icon nova-icon-xs'), node('span', '', item.label));
|
|
189
|
-
button.addEventListener('click', () => this._setMode(item.value));
|
|
190
|
-
modeButtons.set(item.value, button);
|
|
191
|
-
modeSwitch.appendChild(button);
|
|
192
|
-
}
|
|
193
194
|
const projectionSwitch = node('nav', 'nova-studio-projection-switch is-hidden');
|
|
194
195
|
projectionSwitch.setAttribute('aria-label', '审批轨迹视图');
|
|
195
196
|
const projectionButtons = new Map();
|
|
@@ -208,11 +209,11 @@ export class BpmnStudioShell {
|
|
|
208
209
|
const centerControls = node('div', 'nova-studio-center-controls');
|
|
209
210
|
centerControls.append(modeSwitch, projectionSwitch);
|
|
210
211
|
const tools = node('div', 'nova-studio-tools');
|
|
211
|
-
const tool = (label, title, action, className = '', iconId = null) => {
|
|
212
|
+
const tool = (label, title, action, className = '', iconId = null, target = tools) => {
|
|
212
213
|
const button = node('button', `nova-studio-tool ${className}`.trim()); button.type = 'button'; button.title = title;
|
|
213
214
|
if (iconId) button.appendChild(iconNode(iconId));
|
|
214
215
|
if (label) button.appendChild(node('span', '', label));
|
|
215
|
-
button.addEventListener('click', action);
|
|
216
|
+
button.addEventListener('click', action); target.appendChild(button); return button;
|
|
216
217
|
};
|
|
217
218
|
|
|
218
219
|
const divider = () => tools.appendChild(node('span', 'nova-studio-tool-divider'));
|
|
@@ -272,56 +273,62 @@ export class BpmnStudioShell {
|
|
|
272
273
|
});
|
|
273
274
|
beautifySplit.append(beautifyButton, beautifyCaret, beautifyMenu);
|
|
274
275
|
tools.appendChild(beautifySplit);
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
exportPopover
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
276
|
+
const headerActions = node('div', 'nova-studio-header-actions');
|
|
277
|
+
let validateButton = null;
|
|
278
|
+
let importButton = null;
|
|
279
|
+
let exportMenu = null;
|
|
280
|
+
let exportPopover = null;
|
|
281
|
+
let exportBpmnButton = null;
|
|
282
|
+
if (!this.slots.headerActions) {
|
|
283
|
+
const importInput = node('input', 'nova-studio-import-input');
|
|
284
|
+
importInput.type = 'file';
|
|
285
|
+
importInput.accept = '.bpmn,.xml,.bpmn20.xml,text/xml,application/xml';
|
|
286
|
+
importInput.hidden = true;
|
|
287
|
+
validateButton = tool('校验', '校验流程结构', () => this.actions.validate(), 'nova-studio-validate', null, headerActions);
|
|
288
|
+
importButton = tool('导入', '导入 BPMN XML', () => importInput.click(), 'nova-studio-import', null, headerActions);
|
|
289
|
+
exportMenu = node('div', 'nova-studio-export-menu');
|
|
290
|
+
const exportButton = node('button', 'nova-studio-tool nova-studio-export is-primary');
|
|
291
|
+
exportButton.type = 'button';
|
|
292
|
+
exportButton.title = '导出当前内容';
|
|
293
|
+
exportButton.append(node('span', '', '导出'), iconNode('ui.chevron', 'nova-icon nova-icon-xs'));
|
|
294
|
+
exportPopover = node('div', 'nova-studio-export-popover is-hidden');
|
|
295
|
+
const exportSvgButton = node('button', 'nova-studio-export-option');
|
|
296
|
+
exportSvgButton.type = 'button';
|
|
297
|
+
exportSvgButton.append(iconNode('ui.preview'), node('strong', '', '导出 SVG'), node('small', '', '预览确认后下载'));
|
|
298
|
+
exportSvgButton.addEventListener('click', () => {
|
|
299
|
+
exportPopover.classList.add('is-hidden');
|
|
300
|
+
exportButton.focus({ preventScroll: true });
|
|
301
|
+
this.actions.openSvgExportPreview();
|
|
302
|
+
});
|
|
303
|
+
exportBpmnButton = node('button', 'nova-studio-export-option');
|
|
304
|
+
exportBpmnButton.type = 'button';
|
|
305
|
+
exportBpmnButton.append(iconNode('ui.design'), node('strong', '', '导出 BPMN'), node('small', '', '下载流程 XML'));
|
|
306
|
+
exportBpmnButton.addEventListener('click', () => {
|
|
307
|
+
exportPopover.classList.add('is-hidden');
|
|
308
|
+
this._exportBpmn();
|
|
309
|
+
});
|
|
310
|
+
exportPopover.append(exportSvgButton, exportBpmnButton);
|
|
311
|
+
exportButton.addEventListener('click', (event) => {
|
|
312
|
+
event.stopPropagation();
|
|
313
|
+
exportPopover.classList.toggle('is-hidden');
|
|
314
|
+
});
|
|
315
|
+
exportMenu.append(exportButton, exportPopover);
|
|
316
|
+
headerActions.append(exportMenu, importInput);
|
|
317
|
+
importInput.addEventListener('change', async () => {
|
|
318
|
+
const file = importInput.files?.[0];
|
|
319
|
+
if (!file) return;
|
|
320
|
+
try {
|
|
321
|
+
this.actions.importXml(await file.text(), this.studio.model.engine);
|
|
322
|
+
this._setStatus('已导入 BPMN 文件', 'ok');
|
|
323
|
+
requestAnimationFrame(() => this._fitActive());
|
|
324
|
+
} catch (error) {
|
|
325
|
+
this._setStatus(`导入失败:${error.message}`, 'error');
|
|
326
|
+
} finally {
|
|
327
|
+
importInput.value = '';
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
tools.appendChild(headerActions);
|
|
325
332
|
header.append(brand, centerControls, tools);
|
|
326
333
|
|
|
327
334
|
const body = node('div', 'nova-studio-body');
|
|
@@ -369,6 +376,7 @@ export class BpmnStudioShell {
|
|
|
369
376
|
Object.assign(this, {
|
|
370
377
|
_header: header,
|
|
371
378
|
_headerStart: brand,
|
|
379
|
+
_headerActions: headerActions,
|
|
372
380
|
_body: body,
|
|
373
381
|
_left: left,
|
|
374
382
|
_right: right,
|
|
@@ -379,10 +387,11 @@ export class BpmnStudioShell {
|
|
|
379
387
|
_scopeBack: scopeBack,
|
|
380
388
|
_scopeNav: scopeNav,
|
|
381
389
|
_modeButtons: modeButtons,
|
|
390
|
+
_modeSwitch: modeSwitch,
|
|
382
391
|
_projectionSwitch: projectionSwitch,
|
|
383
392
|
_projectionButtons: projectionButtons,
|
|
384
393
|
_viewportTools: viewportTools,
|
|
385
|
-
_designControls: [undo, redo, beautifySplit, validateButton, importButton],
|
|
394
|
+
_designControls: [undo, redo, beautifySplit, validateButton, importButton].filter(Boolean),
|
|
386
395
|
_exportMenu: exportMenu,
|
|
387
396
|
_exportPopover: exportPopover,
|
|
388
397
|
_exportBpmnButton: exportBpmnButton,
|
|
@@ -390,6 +399,7 @@ export class BpmnStudioShell {
|
|
|
390
399
|
_defaultMount: mount,
|
|
391
400
|
_usesDefaultProperties: !this.slots.right,
|
|
392
401
|
});
|
|
402
|
+
this._rebuildModeButtons();
|
|
393
403
|
this.canvas = mount.canvas(canvasHost, {
|
|
394
404
|
rendererOptions: {
|
|
395
405
|
...this.rendererOptions,
|
|
@@ -404,13 +414,19 @@ export class BpmnStudioShell {
|
|
|
404
414
|
else this.palette = mount.palette(left, { canvas: this.canvas });
|
|
405
415
|
if (this.slots.right) this._recordCleanup(mountSlot(this.slots.right, right, { ...context, canvas: this.canvas }));
|
|
406
416
|
else this.properties = mount.properties(right, { canvas: this.canvas });
|
|
417
|
+
const mountHeaderActions = () => {
|
|
418
|
+
if (this.slots.headerActions) {
|
|
419
|
+
this._recordCleanup(mountSlot(this.slots.headerActions, headerActions, { ...context, canvas: this.canvas }));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
407
422
|
if (this.slots.header) {
|
|
408
423
|
header.innerHTML = '';
|
|
409
424
|
this._recordCleanup(mountSlot(this.slots.header, header, { ...context, canvas: this.canvas }));
|
|
410
425
|
} else if (this.slots.headerStart) {
|
|
411
426
|
brand.innerHTML = '';
|
|
412
427
|
this._recordCleanup(mountSlot(this.slots.headerStart, brand, { ...context, canvas: this.canvas }));
|
|
413
|
-
|
|
428
|
+
mountHeaderActions();
|
|
429
|
+
} else mountHeaderActions();
|
|
414
430
|
if (this.slots.footer) { statusbar.innerHTML = ''; this._recordCleanup(mountSlot(this.slots.footer, statusbar, { ...context, canvas: this.canvas })); }
|
|
415
431
|
hydrateIcons(this.container, this.iconRegistry);
|
|
416
432
|
this._offState = this.studio.subscribe((event) => {
|
|
@@ -436,13 +452,12 @@ export class BpmnStudioShell {
|
|
|
436
452
|
this._setStatus(`${initialGraph.nodes.length} 节点 · ${initialGraph.edges.length} 连线`, 'ok');
|
|
437
453
|
const closeBeautify = (event) => {
|
|
438
454
|
if (!beautifySplit.contains(event.target)) beautifyMenu.classList.add('is-hidden');
|
|
439
|
-
if (!exportMenu.contains(event.target)) exportPopover.classList.add('is-hidden');
|
|
455
|
+
if (exportMenu && !exportMenu.contains(event.target)) exportPopover.classList.add('is-hidden');
|
|
440
456
|
};
|
|
441
457
|
document.addEventListener('click', closeBeautify);
|
|
442
458
|
this._cleanups.push(() => document.removeEventListener('click', closeBeautify));
|
|
443
459
|
this._syncDensityButtons();
|
|
444
|
-
this._setMode(this.mode, { force: true });
|
|
445
|
-
requestAnimationFrame(() => fit.click());
|
|
460
|
+
this._setMode(this.mode, { force: true, emit: false });
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
_renderScopePath() {
|
|
@@ -497,94 +512,225 @@ export class BpmnStudioShell {
|
|
|
497
512
|
this.container.querySelectorAll('[data-density]').forEach((button) => button.classList.toggle('is-active', button.dataset.density === value));
|
|
498
513
|
}
|
|
499
514
|
|
|
500
|
-
|
|
501
|
-
if (!this.
|
|
502
|
-
|
|
503
|
-
this.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
515
|
+
_rebuildModeButtons() {
|
|
516
|
+
if (!this._modeSwitch) return;
|
|
517
|
+
this._modeSwitch.replaceChildren();
|
|
518
|
+
this._modeButtons = new Map();
|
|
519
|
+
for (const item of STUDIO_MODE_OPTIONS.filter(({ value }) => this.allowedModes.includes(value))) {
|
|
520
|
+
const button = node('button');
|
|
521
|
+
const active = item.value === this.mode;
|
|
522
|
+
button.type = 'button';
|
|
523
|
+
button.setAttribute('data-mode', item.value);
|
|
524
|
+
button.setAttribute('aria-label', `切换到${item.label}`);
|
|
525
|
+
button.setAttribute('aria-pressed', String(active));
|
|
526
|
+
button.classList.toggle('is-active', active);
|
|
527
|
+
button.append(iconNode(item.iconId, 'nova-icon nova-icon-xs'), node('span', '', item.label));
|
|
528
|
+
button.addEventListener('click', () => this.setMode(item.value, 'toolbar'));
|
|
529
|
+
this._modeButtons.set(item.value, button);
|
|
530
|
+
this._modeSwitch.appendChild(button);
|
|
531
|
+
}
|
|
532
|
+
hydrateIcons(this._modeSwitch, this.iconRegistry);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
_syncModeChrome() {
|
|
536
|
+
this._modeButtons?.forEach((button, value) => {
|
|
537
|
+
const active = value === this.mode;
|
|
538
|
+
button.classList.toggle('is-active', active);
|
|
539
|
+
button.setAttribute('aria-pressed', String(active));
|
|
540
|
+
});
|
|
541
|
+
const readonly = this.mode !== 'design';
|
|
507
542
|
this._body?.classList.toggle('is-readonly', readonly);
|
|
508
543
|
this._applyRegions({ fit: false });
|
|
509
544
|
this._floatingHead?.classList.toggle('is-hidden', readonly);
|
|
510
545
|
this._designControls?.forEach((control) => control.classList.toggle('is-hidden-by-mode', readonly));
|
|
511
|
-
this._projectionSwitch?.classList.toggle('is-hidden', mode !== 'instance');
|
|
512
|
-
this._exportBpmnButton?.classList.toggle('is-hidden', mode !== 'design');
|
|
546
|
+
this._projectionSwitch?.classList.toggle('is-hidden', this.mode !== 'instance');
|
|
547
|
+
this._exportBpmnButton?.classList.toggle('is-hidden', this.mode !== 'design');
|
|
513
548
|
this._syncProjectionButtons();
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
nodeRenderers: this.rendererOptions.nodeRenderers,
|
|
530
|
-
nodeRenderer: this.rendererOptions.nodeRenderer,
|
|
531
|
-
runtime: mode === 'instance' ? this.runtime : null,
|
|
532
|
-
projection: this.projection,
|
|
533
|
-
responsive: this.responsive,
|
|
534
|
-
runtimeTraceOptions: this.rendererOptions.runtimeTraceOptions,
|
|
535
|
-
timeline: this.rendererOptions.timeline,
|
|
536
|
-
runtimeDetails: this.rendererOptions.runtimeDetails,
|
|
537
|
-
runtimePresenter: this.rendererOptions.runtimePresenter,
|
|
538
|
-
runtimeTraceProjector: this.rendererOptions.runtimeTraceProjector,
|
|
539
|
-
runtimeAssetResolver: this.rendererOptions.runtimeAssetResolver,
|
|
540
|
-
svgExport: this.svgExport,
|
|
541
|
-
runtimeTimelineRenderer: this.slots.runtimeTimeline
|
|
542
|
-
? (timeline) => mountSlot(this.slots.runtimeTimeline, timeline.container, { ...timeline, studio: this.studio, shell: this })
|
|
543
|
-
: this.rendererOptions.runtimeTimelineRenderer,
|
|
544
|
-
onRuntimeTraceItemClick: this.rendererOptions.onRuntimeTraceItemClick,
|
|
545
|
-
runtimeDetailsRenderer: this.slots.runtimeDetails
|
|
546
|
-
? (details) => mountSlot(this.slots.runtimeDetails, details.container, { ...details, studio: this.studio, shell: this })
|
|
547
|
-
: this.rendererOptions.runtimeDetailsRenderer,
|
|
548
|
-
onRuntimeDetailsOpen: this.rendererOptions.onRuntimeDetailsOpen,
|
|
549
|
-
runtimeTransitionDetailsRenderer: this.slots.runtimeTransitionDetails
|
|
550
|
-
? (details) => mountSlot(this.slots.runtimeTransitionDetails, details.container, { ...details, studio: this.studio, shell: this })
|
|
551
|
-
: this.rendererOptions.runtimeTransitionDetailsRenderer,
|
|
552
|
-
onRuntimeTransitionDetailsOpen: this.rendererOptions.onRuntimeTransitionDetailsOpen,
|
|
553
|
-
onTraceClick: (payload) => {
|
|
554
|
-
const kind = payload.targetType === 'visit' ? 'node' : payload.targetType === 'transition' ? 'node' : payload.targetType;
|
|
555
|
-
this._renderReadonlyDetails({ kind, element: payload.element, presentation: payload.presentation, transition: payload.transition });
|
|
556
|
-
this.rendererOptions.onTraceClick?.(payload);
|
|
557
|
-
},
|
|
558
|
-
onProjectionChange: ({ active }) => {
|
|
559
|
-
const timeline = active === 'compact';
|
|
560
|
-
this._body?.classList.toggle('is-timeline', timeline);
|
|
561
|
-
this._viewportTools?.classList.toggle('is-hidden', timeline);
|
|
562
|
-
const status = timeline ? '审批轨迹 · 移动时间线' : active === 'standard' ? '审批轨迹 · 完整 BPMN' : '审批轨迹 · 实际路径';
|
|
563
|
-
this._setStatus(status, 'ok');
|
|
564
|
-
},
|
|
565
|
-
onViewportChange: (viewport) => this._handleViewport(viewport),
|
|
566
|
-
onElementClick: (selection) => {
|
|
567
|
-
if (!selection) this._renderReadonlyDetails(null);
|
|
568
|
-
this.rendererOptions.onElementClick?.(selection);
|
|
569
|
-
},
|
|
570
|
-
});
|
|
571
|
-
this._renderReadonlyDetails(null);
|
|
572
|
-
this._setStatus(mode === 'instance' ? '审批轨迹 · 运行实例' : '流程展示 · 只读预览', 'ok');
|
|
573
|
-
} else if (!this.canvas) {
|
|
574
|
-
this.viewer?.destroy?.();
|
|
575
|
-
this.viewer = null;
|
|
576
|
-
this._canvasHost.innerHTML = '';
|
|
577
|
-
this.canvas = this._defaultMount.canvas(this._canvasHost, {
|
|
578
|
-
rendererOptions: {
|
|
579
|
-
...this.rendererOptions,
|
|
580
|
-
onViewportChange: (viewport) => this._handleViewport(viewport),
|
|
581
|
-
},
|
|
582
|
-
});
|
|
583
|
-
if (this.palette) this.palette.canvas = this.canvas;
|
|
584
|
-
if (this._usesDefaultProperties) this.properties = this._defaultMount.properties(this._right, { canvas: this.canvas });
|
|
585
|
-
this._setStatus(`${this.studio.model.nodes.length} 节点 · ${this.studio.model.edges.length} 连线`, 'ok');
|
|
549
|
+
this._syncProjectionChrome();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
_syncProjectionChrome() {
|
|
553
|
+
const timeline = this.mode === 'instance' && this.viewer?.activeProjection === 'compact';
|
|
554
|
+
this._body?.classList.toggle('is-timeline', timeline);
|
|
555
|
+
this._viewportTools?.classList.toggle('is-hidden', timeline);
|
|
556
|
+
if (this.mode === 'design') {
|
|
557
|
+
const graph = this.studio.getActiveGraph();
|
|
558
|
+
this._setStatus(`${graph.nodes.length} 节点 · ${graph.edges.length} 连线`, 'ok');
|
|
559
|
+
} else if (this.mode === 'viewer') this._setStatus('流程展示 · 只读预览', 'ok');
|
|
560
|
+
else {
|
|
561
|
+
const active = this.viewer?.activeProjection || this.projection;
|
|
562
|
+
const status = active === 'compact' ? '审批轨迹 · 移动时间线' : active === 'standard' ? '审批轨迹 · 完整 BPMN' : '审批轨迹 · 实际路径';
|
|
563
|
+
this._setStatus(status, 'ok');
|
|
586
564
|
}
|
|
587
|
-
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
_captureModeViewport(mode = this.mode) {
|
|
568
|
+
if (this.viewer?.activeProjection === 'compact') return;
|
|
569
|
+
const renderer = this.canvas?.renderer || this.viewer?.renderer;
|
|
570
|
+
const viewport = renderer?.getViewportState?.();
|
|
571
|
+
if (viewport) this._modeViewports.set(mode, viewport);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_restoreModeViewport(mode) {
|
|
575
|
+
const viewport = this._modeViewports.get(mode);
|
|
576
|
+
const renderer = this.canvas?.renderer || this.viewer?.renderer;
|
|
577
|
+
if (viewport && renderer?.setViewportState) {
|
|
578
|
+
renderer.setViewportState(viewport);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
requestAnimationFrame(() => {
|
|
582
|
+
if (this._destroyed || this.mode !== mode) return;
|
|
583
|
+
this._fitActive();
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_createViewer(container, mode) {
|
|
588
|
+
let viewer = null;
|
|
589
|
+
viewer = new BpmnViewer({
|
|
590
|
+
container,
|
|
591
|
+
themeController: this.themeController,
|
|
592
|
+
runtimeAppearance: this.runtimeAppearance,
|
|
593
|
+
model: this.studio.model,
|
|
594
|
+
mode,
|
|
595
|
+
iconRegistry: this.iconRegistry,
|
|
596
|
+
nodeRenderers: this.rendererOptions.nodeRenderers,
|
|
597
|
+
nodeRenderer: this.rendererOptions.nodeRenderer,
|
|
598
|
+
nodeSubtitleResolver: this.rendererOptions.nodeSubtitleResolver,
|
|
599
|
+
runtime: mode === 'instance' ? this.runtime : null,
|
|
600
|
+
projection: mode === 'instance' ? this._instanceProjection : 'standard',
|
|
601
|
+
responsive: this.responsive,
|
|
602
|
+
runtimeTraceOptions: this.rendererOptions.runtimeTraceOptions,
|
|
603
|
+
timeline: this.rendererOptions.timeline,
|
|
604
|
+
runtimeDetails: this.rendererOptions.runtimeDetails,
|
|
605
|
+
runtimePresenter: this.rendererOptions.runtimePresenter,
|
|
606
|
+
runtimeTraceProjector: this.rendererOptions.runtimeTraceProjector,
|
|
607
|
+
runtimeAssetResolver: this.rendererOptions.runtimeAssetResolver,
|
|
608
|
+
svgExport: this.svgExport,
|
|
609
|
+
runtimeTimelineRenderer: this.slots.runtimeTimeline
|
|
610
|
+
? (timeline) => mountSlot(this.slots.runtimeTimeline, timeline.container, { ...timeline, studio: this.studio, shell: this })
|
|
611
|
+
: this.rendererOptions.runtimeTimelineRenderer,
|
|
612
|
+
onRuntimeTraceItemClick: this.rendererOptions.onRuntimeTraceItemClick,
|
|
613
|
+
runtimeDetailsRenderer: this.slots.runtimeDetails
|
|
614
|
+
? (details) => mountSlot(this.slots.runtimeDetails, details.container, { ...details, studio: this.studio, shell: this })
|
|
615
|
+
: this.rendererOptions.runtimeDetailsRenderer,
|
|
616
|
+
onRuntimeDetailsOpen: this.rendererOptions.onRuntimeDetailsOpen,
|
|
617
|
+
runtimeTransitionDetailsRenderer: this.slots.runtimeTransitionDetails
|
|
618
|
+
? (details) => mountSlot(this.slots.runtimeTransitionDetails, details.container, { ...details, studio: this.studio, shell: this })
|
|
619
|
+
: this.rendererOptions.runtimeTransitionDetailsRenderer,
|
|
620
|
+
onRuntimeTransitionDetailsOpen: this.rendererOptions.onRuntimeTransitionDetailsOpen,
|
|
621
|
+
onTraceClick: (payload) => {
|
|
622
|
+
const kind = payload.targetType === 'visit' ? 'node' : payload.targetType === 'transition' ? 'node' : payload.targetType;
|
|
623
|
+
this._renderReadonlyDetails({ kind, element: payload.element, presentation: payload.presentation, transition: payload.transition });
|
|
624
|
+
this.rendererOptions.onTraceClick?.(payload);
|
|
625
|
+
},
|
|
626
|
+
onProjectionChange: () => {
|
|
627
|
+
if (this.viewer === viewer) this._syncProjectionChrome();
|
|
628
|
+
},
|
|
629
|
+
onViewportChange: (viewport) => {
|
|
630
|
+
if (this.viewer === viewer) this._handleViewport(viewport);
|
|
631
|
+
},
|
|
632
|
+
onElementClick: (selection) => {
|
|
633
|
+
if (!selection) this._renderReadonlyDetails(null);
|
|
634
|
+
this.rendererOptions.onElementClick?.(selection);
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
return viewer;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_emitModeChange(previousMode, source) {
|
|
641
|
+
const event = Object.freeze({
|
|
642
|
+
mode: this.mode,
|
|
643
|
+
previousMode,
|
|
644
|
+
source,
|
|
645
|
+
allowedModes: this.getAllowedModes(),
|
|
646
|
+
});
|
|
647
|
+
for (const listener of [...this._modeListeners]) {
|
|
648
|
+
try {
|
|
649
|
+
listener(event);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error('BpmnStudioShell mode listener failed.', error);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
_setMode(mode, { force = false, emit = true, source = 'api', allowedModes = this.allowedModes } = {}) {
|
|
657
|
+
if (this._destroyed || !allowedModes.includes(mode)) return false;
|
|
658
|
+
const previousMode = this.mode;
|
|
659
|
+
if (!force && previousMode === mode) return false;
|
|
660
|
+
|
|
661
|
+
const reusable = (mode === 'design' && this.canvas) || (mode !== 'design' && this.viewer);
|
|
662
|
+
if (force && previousMode === mode && reusable) {
|
|
663
|
+
this.allowedModes = allowedModes;
|
|
664
|
+
this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
|
|
665
|
+
this._rebuildModeButtons();
|
|
666
|
+
this._syncModeChrome();
|
|
667
|
+
this._restoreModeViewport(mode);
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!this._canvasHost) {
|
|
672
|
+
this.allowedModes = allowedModes;
|
|
673
|
+
this.mode = mode;
|
|
674
|
+
this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
|
|
675
|
+
this._rebuildModeButtons();
|
|
676
|
+
this._syncModeChrome();
|
|
677
|
+
if (emit && previousMode !== mode) this._emitModeChange(previousMode, source);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!force || previousMode !== mode) this._captureModeViewport(previousMode);
|
|
682
|
+
const previousHost = this._canvasHost;
|
|
683
|
+
const nextHost = node('main', 'nova-studio-canvas');
|
|
684
|
+
nextHost.hidden = true;
|
|
685
|
+
previousHost.parentNode?.insertBefore(nextHost, previousHost.nextSibling);
|
|
686
|
+
let nextCanvas = null;
|
|
687
|
+
let nextViewer = null;
|
|
688
|
+
try {
|
|
689
|
+
if (mode === 'design') {
|
|
690
|
+
nextCanvas = this._createCanvas(nextHost, {
|
|
691
|
+
rendererOptions: {
|
|
692
|
+
...this.rendererOptions,
|
|
693
|
+
onViewportChange: (viewport) => {
|
|
694
|
+
if (this.canvas === nextCanvas) this._handleViewport(viewport);
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
} else nextViewer = this._createViewer(nextHost, mode);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
nextCanvas?.destroy?.();
|
|
701
|
+
nextViewer?.destroy?.();
|
|
702
|
+
nextHost.remove();
|
|
703
|
+
this._syncModeChrome();
|
|
704
|
+
console.error(`BpmnStudioShell failed to mount mode "${mode}".`, error);
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const previousCanvas = this.canvas;
|
|
709
|
+
const previousViewer = this.viewer;
|
|
710
|
+
nextHost.hidden = false;
|
|
711
|
+
previousHost.replaceWith(nextHost);
|
|
712
|
+
this._canvasHost = nextHost;
|
|
713
|
+
this.canvas = nextCanvas;
|
|
714
|
+
this.viewer = nextViewer;
|
|
715
|
+
this.allowedModes = allowedModes;
|
|
716
|
+
this.mode = mode;
|
|
717
|
+
this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
|
|
718
|
+
if (nextCanvas) this._instances.push(nextCanvas);
|
|
719
|
+
this._releaseInstance(previousCanvas);
|
|
720
|
+
previousViewer?.destroy?.();
|
|
721
|
+
|
|
722
|
+
if (this.palette) this.palette.canvas = this.canvas;
|
|
723
|
+
if (this._usesDefaultProperties) {
|
|
724
|
+
this._releaseInstance(this.properties);
|
|
725
|
+
this.properties = null;
|
|
726
|
+
if (this.canvas) this.properties = this._mountProperties(this._right, { canvas: this.canvas });
|
|
727
|
+
else this._renderReadonlyDetails(null);
|
|
728
|
+
}
|
|
729
|
+
this._rebuildModeButtons();
|
|
730
|
+
this._syncModeChrome();
|
|
731
|
+
this._restoreModeViewport(mode);
|
|
732
|
+
if (emit && previousMode !== mode) this._emitModeChange(previousMode, source);
|
|
733
|
+
return true;
|
|
588
734
|
}
|
|
589
735
|
|
|
590
736
|
_handleViewport(viewport) {
|
|
@@ -635,7 +781,34 @@ export class BpmnStudioShell {
|
|
|
635
781
|
if (this.canvas) this.canvas.fitView(72);
|
|
636
782
|
else this.viewer?.fitView?.();
|
|
637
783
|
}
|
|
638
|
-
|
|
784
|
+
getMode() { return this.mode; }
|
|
785
|
+
getAllowedModes() { return Object.freeze([...this.allowedModes]); }
|
|
786
|
+
subscribeMode(listener) {
|
|
787
|
+
if (typeof listener !== 'function') throw new TypeError('subscribeMode() requires a listener function.');
|
|
788
|
+
if (this._destroyed) return () => {};
|
|
789
|
+
this._modeListeners.add(listener);
|
|
790
|
+
return () => this._modeListeners.delete(listener);
|
|
791
|
+
}
|
|
792
|
+
subscribeValidation(listener) {
|
|
793
|
+
if (typeof listener !== 'function') throw new TypeError('subscribeValidation() requires a listener function.');
|
|
794
|
+
if (this._destroyed) return () => {};
|
|
795
|
+
this._validationListeners.add(listener);
|
|
796
|
+
return () => this._validationListeners.delete(listener);
|
|
797
|
+
}
|
|
798
|
+
setMode(mode, source = 'api') { return this._setMode(mode, { source }); }
|
|
799
|
+
setAllowedModes(modes) {
|
|
800
|
+
const next = normalizeAllowedModes(modes, { allowDefault: false });
|
|
801
|
+
if (this._destroyed) return this.getAllowedModes();
|
|
802
|
+
if (next.length === this.allowedModes.length && next.every((mode, index) => mode === this.allowedModes[index])) {
|
|
803
|
+
return this.getAllowedModes();
|
|
804
|
+
}
|
|
805
|
+
if (next.includes(this.mode)) {
|
|
806
|
+
this.allowedModes = next;
|
|
807
|
+
this._rebuildModeButtons();
|
|
808
|
+
this._syncModeChrome();
|
|
809
|
+
} else this._setMode(next[0], { source: 'allowed-modes', allowedModes: next });
|
|
810
|
+
return this.getAllowedModes();
|
|
811
|
+
}
|
|
639
812
|
setRuntime(runtime) {
|
|
640
813
|
this.runtime = runtime;
|
|
641
814
|
if (this.mode === 'instance') this.viewer?.setRuntime(runtime);
|
|
@@ -682,6 +855,8 @@ export class BpmnStudioShell {
|
|
|
682
855
|
this.runtimeAppearance = runtimeAppearance || null;
|
|
683
856
|
this.viewer?.setRuntimeAppearance?.(this.runtimeAppearance);
|
|
684
857
|
}
|
|
858
|
+
refreshPresentation() { (this.canvas || this.viewer)?.refreshPresentation?.(); }
|
|
859
|
+
validate() { return this._runValidation('api'); }
|
|
685
860
|
|
|
686
861
|
_syncProjectionButtons() {
|
|
687
862
|
this._projectionButtons?.forEach((button, value) => {
|
|
@@ -728,12 +903,32 @@ export class BpmnStudioShell {
|
|
|
728
903
|
this._statusDot.className = `is-${tone}`;
|
|
729
904
|
}
|
|
730
905
|
|
|
731
|
-
|
|
732
|
-
const issues = this.
|
|
906
|
+
_runValidation(source) {
|
|
907
|
+
const issues = this.studio.validate();
|
|
908
|
+
const errorCount = issues.filter((issue) => issue.level === 'error').length;
|
|
909
|
+
const warningCount = issues.filter((issue) => issue.level === 'warning').length;
|
|
733
910
|
if (!issues.length) this._setStatus('校验通过:流程结构正常', 'ok');
|
|
734
911
|
else {
|
|
735
|
-
const
|
|
736
|
-
|
|
912
|
+
const summary = [
|
|
913
|
+
errorCount ? `${errorCount} 个错误` : null,
|
|
914
|
+
warningCount ? `${warningCount} 个警告` : null,
|
|
915
|
+
].filter(Boolean).join(' · ');
|
|
916
|
+
this._setStatus(`校验完成:${summary}`, errorCount ? 'error' : 'warning');
|
|
917
|
+
}
|
|
918
|
+
const eventIssues = Object.freeze(issues.map((issue) => Object.freeze({ ...issue })));
|
|
919
|
+
const event = Object.freeze({
|
|
920
|
+
source,
|
|
921
|
+
valid: errorCount === 0,
|
|
922
|
+
errorCount,
|
|
923
|
+
warningCount,
|
|
924
|
+
issues: eventIssues,
|
|
925
|
+
});
|
|
926
|
+
for (const listener of [...this._validationListeners]) {
|
|
927
|
+
try {
|
|
928
|
+
listener(event);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.error('BpmnStudioShell validation listener failed.', error);
|
|
931
|
+
}
|
|
737
932
|
}
|
|
738
933
|
return issues;
|
|
739
934
|
}
|
|
@@ -752,8 +947,8 @@ export class BpmnStudioShell {
|
|
|
752
947
|
}
|
|
753
948
|
|
|
754
949
|
_recordCleanup(cleanup) { if (typeof cleanup === 'function') this._cleanups.push(cleanup); }
|
|
755
|
-
|
|
756
|
-
|
|
950
|
+
_createCanvas(container, options = {}) {
|
|
951
|
+
return new BpmnCanvas({
|
|
757
952
|
container,
|
|
758
953
|
studio: this.studio,
|
|
759
954
|
interactions: this.interactions,
|
|
@@ -768,6 +963,9 @@ export class BpmnStudioShell {
|
|
|
768
963
|
},
|
|
769
964
|
themeController: this.themeController,
|
|
770
965
|
});
|
|
966
|
+
}
|
|
967
|
+
_mountCanvas(container, options = {}) {
|
|
968
|
+
const instance = this._createCanvas(container, options);
|
|
771
969
|
this._instances.push(instance); return instance;
|
|
772
970
|
}
|
|
773
971
|
_mountPalette(container, options = {}) {
|
|
@@ -778,11 +976,25 @@ export class BpmnStudioShell {
|
|
|
778
976
|
const instance = new PropertiesPanel({ container, studio: this.studio, registry: this.propertiesRegistry, iconRegistry: this.iconRegistry, themeController: this.themeController, ...options });
|
|
779
977
|
instance.render(); this._instances.push(instance); return instance;
|
|
780
978
|
}
|
|
979
|
+
_releaseInstance(instance) {
|
|
980
|
+
if (!instance) return;
|
|
981
|
+
const index = this._instances.indexOf(instance);
|
|
982
|
+
if (index >= 0) this._instances.splice(index, 1);
|
|
983
|
+
instance.destroy?.();
|
|
984
|
+
}
|
|
781
985
|
destroy() {
|
|
986
|
+
if (this._destroyed) return;
|
|
987
|
+
this._destroyed = true;
|
|
988
|
+
this._modeListeners.clear();
|
|
989
|
+
this._validationListeners.clear();
|
|
782
990
|
this._svgExportPreview?.close?.({ immediate: true });
|
|
783
991
|
this._svgExportPreview = null;
|
|
784
992
|
this._cleanups.splice(0).reverse().forEach((cleanup) => cleanup?.());
|
|
993
|
+
this.viewer?.destroy?.();
|
|
994
|
+
this.viewer = null;
|
|
785
995
|
this._instances.splice(0).reverse().forEach((instance) => instance.destroy?.());
|
|
996
|
+
this.canvas = null;
|
|
997
|
+
this.properties = null;
|
|
786
998
|
this.container.innerHTML = '';
|
|
787
999
|
this.container.classList.remove('nova-studio-shell');
|
|
788
1000
|
this.themeController.destroy();
|