@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.
Files changed (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
@@ -0,0 +1,1011 @@
1
+ import { cloneModel, createEdge, getScopeGraph, NODE_DEFINITIONS } from '../core/index.js';
2
+ import { createDefaultIconRegistry, createIconElement } from '../icons/index.js';
3
+ import { DiagramRenderer } from '../renderer-svg/index.js';
4
+ import { createRuntimePresentation, normalizeRuntime } from '../runtime/index.js';
5
+ import { createRuntimeTraceProjection } from './runtime-trace.js';
6
+ import { renderDefaultRuntimeTimeline, RuntimeTimelineHost } from './timeline.js';
7
+ import { renderRuntimeApprovalContent } from './runtime-content.js';
8
+ import { createRuntimeDetailsMotionController } from './runtime-details-motion.js';
9
+ import { ThemeController, applyRuntimeTone, createRuntimeAppearance } from '../theme/index.js';
10
+ import { exportRuntimeTimelineSvg, openSvgExportPreview } from '../export-svg/index.js';
11
+ export { createRuntimeTraceProjection } from './runtime-trace.js';
12
+ export { renderDefaultRuntimeTimeline } from './timeline.js';
13
+ export { formatRuntimeAssetSize, renderRuntimeApprovalContent } from './runtime-content.js';
14
+
15
+ function element(tag, className, text) {
16
+ const node = document.createElement(tag);
17
+ if (className) node.className = className;
18
+ if (text !== undefined) node.textContent = text;
19
+ return node;
20
+ }
21
+
22
+ function outcomeLabel(outcome) {
23
+ return { approved: '通过', rejected: '驳回', returned: '退回', submitted: '提交', pending: '待处理' }[outcome] || outcome || '';
24
+ }
25
+
26
+ const defaultDetailsIconRegistry = createDefaultIconRegistry();
27
+
28
+ function runtimeDetailsCloseButton({ viewer, label, close }) {
29
+ const button = element('button', 'mb-runtime-details-close');
30
+ button.type = 'button';
31
+ button.title = label;
32
+ button.setAttribute('aria-label', label);
33
+ const iconHost = element('span', 'nova-icon nova-icon-md');
34
+ const iconRegistry = viewer?.renderer?.iconRegistry || defaultDetailsIconRegistry;
35
+ const icon = createIconElement(iconRegistry.resolve('ui.close', null), { className: 'nova-icon-svg' });
36
+ if (icon) iconHost.appendChild(icon);
37
+ button.appendChild(iconHost);
38
+ button.addEventListener('click', close);
39
+ return button;
40
+ }
41
+
42
+ function renderRuntimeAction({ container, viewer, action, resolveAsset, compact = false }) {
43
+ const appearance = viewer?.runtimeAppearance?.resolveAction(action) || { label: action.label, tone: 'neutral' };
44
+ const article = element('article', `mb-runtime-action type-${action.type}${compact ? ' is-compact' : ''}`);
45
+ article.dataset.runtimeActionId = action.id;
46
+ applyRuntimeTone(article, appearance.tone);
47
+ const header = element('header', 'mb-runtime-action-head');
48
+ const heading = element('div', 'mb-runtime-action-heading');
49
+ heading.append(
50
+ element('strong', '', action.actor?.name || '系统'),
51
+ element('em', '', appearance.label),
52
+ );
53
+ header.appendChild(heading);
54
+ if (action.occurredAt) header.appendChild(element('time', '', action.occurredAt));
55
+ article.appendChild(header);
56
+ if (action.targets?.length) article.appendChild(element('div', 'mb-runtime-action-targets', `目标:${action.targets.map((target) => target.name).join('、')}`));
57
+ if (action.targetElementId && !action.targets?.length) article.appendChild(element('div', 'mb-runtime-action-targets', `目标节点:${action.targetElementId}`));
58
+ renderRuntimeApprovalContent({
59
+ container: article,
60
+ action,
61
+ resolveAsset,
62
+ compact,
63
+ onPreview: (asset, sourceAction) => viewer?._openRuntimeAssetPreview(asset, sourceAction),
64
+ });
65
+ container.appendChild(article);
66
+ return article;
67
+ }
68
+
69
+ export function renderDefaultRuntimeDetails({ container, viewer, node, presentation, close, resolveAsset }) {
70
+ const popover = element('section', 'mb-runtime-details-popover');
71
+ popover.setAttribute('role', 'dialog');
72
+ popover.setAttribute('aria-label', `${node.name || '节点'}审批详情`);
73
+ popover.tabIndex = -1;
74
+ const header = element('header', 'mb-runtime-details-head');
75
+ const heading = element('div', 'mb-runtime-details-heading');
76
+ heading.append(element('strong', '', node.name || '节点详情'), element('span', '', presentation.round > 1 ? `第 ${presentation.round} 次处理` : '审批轨迹'));
77
+ const status = element('em', `status-${presentation.status}`, presentation.statusLabel);
78
+ applyRuntimeTone(status, viewer?.runtimeAppearance?.resolveStatus(presentation.status));
79
+ const closeButton = runtimeDetailsCloseButton({ viewer, label: '关闭审批详情', close });
80
+ header.append(heading, status, closeButton);
81
+ popover.appendChild(header);
82
+
83
+ if (presentation.approvalMode || presentation.total > 1) {
84
+ const mode = presentation.approvalMode === 'all' ? (presentation.multiInstanceMode === 'sequential' ? '顺序会签' : '会签') : presentation.approvalMode === 'any' ? '或签' : '多人审批';
85
+ const summary = element('div', 'mb-runtime-details-progress');
86
+ summary.append(element('span', '', mode), element('strong', '', `${presentation.completed}/${presentation.total || presentation.required}`));
87
+ popover.appendChild(summary);
88
+ }
89
+
90
+ const body = element('div', 'mb-runtime-details-body');
91
+ const appendRecord = (record) => {
92
+ const row = element('article', 'mb-runtime-details-person');
93
+ const avatar = element('span', 'mb-runtime-details-avatar', (record.participant?.name || record.assignee || '?').slice(0, 1));
94
+ if (record.participant?.avatarUrl) {
95
+ avatar.textContent = '';
96
+ const image = document.createElement('img');
97
+ image.src = record.participant.avatarUrl;
98
+ image.alt = '';
99
+ avatar.appendChild(image);
100
+ }
101
+ const copy = element('div', 'mb-runtime-details-person-copy');
102
+ const title = element('div', 'mb-runtime-details-person-title');
103
+ const recordStatus = element('em', `status-${record.status}`, outcomeLabel(record.outcome) || presentation.statusLabel);
104
+ applyRuntimeTone(recordStatus, viewer?.runtimeAppearance?.resolveStatus(record.status));
105
+ title.append(element('strong', '', record.participant?.name || record.assignee || '待分配审批人'), recordStatus);
106
+ copy.appendChild(title);
107
+ const time = [record.startTime, record.endTime].filter(Boolean).join(' — ');
108
+ if (time) copy.appendChild(element('time', '', time));
109
+ row.append(avatar, copy);
110
+ body.appendChild(row);
111
+ };
112
+ const visits = [...presentation.visits].reverse();
113
+ const renderedActionIds = new Set();
114
+ if (!visits.length) body.appendChild(element('div', 'mb-runtime-details-empty', presentation.summary || presentation.statusLabel));
115
+ visits.forEach((visit) => {
116
+ if (presentation.visits.length > 1) body.appendChild(element('div', 'mb-runtime-details-round', `第 ${visit.round} 次处理`));
117
+ [...visit.records].reverse().forEach(appendRecord);
118
+ [...visit.actions].reverse().forEach((action) => {
119
+ renderRuntimeAction({ container: body, viewer, action, resolveAsset });
120
+ renderedActionIds.add(action.id);
121
+ });
122
+ });
123
+ [...presentation.actions].reverse().filter((action) => !renderedActionIds.has(action.id)).forEach((action) => {
124
+ renderRuntimeAction({ container: body, viewer, action, resolveAsset });
125
+ renderedActionIds.add(action.id);
126
+ });
127
+
128
+ const abnormal = [...presentation.transitions].filter((item) => ['reject', 'return'].includes(item.type)).reverse();
129
+ abnormal.forEach((transition) => {
130
+ const block = element('article', `mb-runtime-details-transition type-${transition.type}`);
131
+ applyRuntimeTone(block, viewer?.runtimeAppearance?.resolveTransition(transition).tone);
132
+ block.appendChild(element('strong', '', transition.type === 'reject' ? `驳回至 ${transition.targetName || transition.targetElementId}` : `退回至 ${transition.targetName || transition.targetElementId}`));
133
+ const meta = [transition.operator, transition.occurredAt || transition.time].filter(Boolean).join(' · ');
134
+ if (meta) block.appendChild(element('span', '', meta));
135
+ if (transition.action && !renderedActionIds.has(transition.action.id)) {
136
+ renderRuntimeApprovalContent({
137
+ container: block,
138
+ action: transition.action,
139
+ resolveAsset,
140
+ onPreview: (asset, action) => viewer?._openRuntimeAssetPreview(asset, action),
141
+ });
142
+ } else if (!transition.action && transition.comment) block.appendChild(element('p', '', transition.comment));
143
+ body.appendChild(block);
144
+ });
145
+ popover.appendChild(body);
146
+ container.appendChild(popover);
147
+ requestAnimationFrame(() => popover.focus({ preventScroll: true }));
148
+ return () => popover.remove();
149
+ }
150
+
151
+ export function renderDefaultRuntimeTransitionDetails({
152
+ container,
153
+ viewer,
154
+ transition,
155
+ sourceNode,
156
+ targetNode,
157
+ close,
158
+ locateSource,
159
+ locateTarget,
160
+ resolveAsset,
161
+ }) {
162
+ const actionLabel = transition.type === 'return' ? '退回' : '驳回';
163
+ const popover = element('section', 'mb-runtime-details-popover mb-runtime-transition-details');
164
+ popover.setAttribute('role', 'dialog');
165
+ popover.setAttribute('aria-label', `${sourceNode?.name || transition.sourceName || '节点'}${actionLabel}记录`);
166
+ popover.tabIndex = -1;
167
+
168
+ const header = element('header', 'mb-runtime-details-head');
169
+ const heading = element('div', 'mb-runtime-details-heading');
170
+ heading.append(
171
+ element('strong', '', sourceNode?.name || transition.sourceName || '驳回记录'),
172
+ element('span', '', `${actionLabel}记录`),
173
+ );
174
+ const status = element('em', 'status-rejected', `已${actionLabel}`);
175
+ applyRuntimeTone(status, viewer?.runtimeAppearance?.resolveTransition(transition).tone);
176
+ const closeButton = runtimeDetailsCloseButton({ viewer, label: `关闭${actionLabel}详情`, close });
177
+ header.append(heading, status, closeButton);
178
+
179
+ const body = element('div', 'mb-runtime-details-body');
180
+ const actor = element('article', 'mb-runtime-transition-actor');
181
+ const actorName = transition.action?.actor?.name || transition.operator || '未知处理人';
182
+ const avatar = element('span', 'mb-runtime-details-avatar', actorName.slice(0, 1));
183
+ const actorCopy = element('div', 'mb-runtime-details-person-copy');
184
+ const actorTitle = element('div', 'mb-runtime-details-person-title');
185
+ const actorStatus = element('em', 'status-rejected', actionLabel);
186
+ applyRuntimeTone(actorStatus, viewer?.runtimeAppearance?.resolveTransition(transition).tone);
187
+ actorTitle.append(
188
+ element('strong', '', actorName),
189
+ actorStatus,
190
+ );
191
+ actorCopy.appendChild(actorTitle);
192
+ const time = transition.action?.occurredAt || transition.occurredAt || transition.time;
193
+ if (time) actorCopy.appendChild(element('time', '', time));
194
+ actor.append(avatar, actorCopy);
195
+ body.appendChild(actor);
196
+
197
+ const target = element('article', `mb-runtime-details-transition type-${transition.type}`);
198
+ applyRuntimeTone(target, viewer?.runtimeAppearance?.resolveTransition(transition).tone);
199
+ target.appendChild(element('strong', '', `${actionLabel}至 ${targetNode?.name || transition.targetName || transition.targetElementId}`));
200
+ if (transition.action) {
201
+ renderRuntimeApprovalContent({
202
+ container: target,
203
+ action: transition.action,
204
+ resolveAsset,
205
+ onPreview: (asset, action) => viewer?._openRuntimeAssetPreview(asset, action),
206
+ });
207
+ } else if (transition.comment) target.appendChild(element('p', '', transition.comment));
208
+ body.appendChild(target);
209
+
210
+ if (transition.issues?.length) {
211
+ const warning = element('div', 'mb-runtime-transition-warning');
212
+ transition.issues.forEach((issue) => warning.appendChild(element('p', '', issue)));
213
+ body.appendChild(warning);
214
+ }
215
+
216
+ const footer = element('footer', 'mb-runtime-transition-actions');
217
+ const sourceButton = element('button', '', '定位来源');
218
+ sourceButton.type = 'button';
219
+ sourceButton.disabled = !sourceNode;
220
+ sourceButton.addEventListener('click', locateSource);
221
+ const targetButton = element('button', '', '定位目标');
222
+ targetButton.type = 'button';
223
+ targetButton.disabled = !targetNode;
224
+ targetButton.addEventListener('click', locateTarget);
225
+ footer.append(sourceButton, targetButton);
226
+
227
+ popover.append(header, body, footer);
228
+ container.appendChild(popover);
229
+ requestAnimationFrame(() => popover.focus({ preventScroll: true }));
230
+ return () => popover.remove();
231
+ }
232
+
233
+ function buildVisibleGraph(model, predicate, orientation = 'horizontal') {
234
+ const visible = new Set(model.nodes.filter(predicate).map((n) => n.id));
235
+ const outgoing = new Map(model.nodes.map((n) => [n.id, []]));
236
+ for (const edge of model.edges) {
237
+ if (outgoing.has(edge.source)) outgoing.get(edge.source).push(edge);
238
+ }
239
+
240
+ const copy = cloneModel(model);
241
+ copy.nodes = model.nodes.filter((n) => visible.has(n.id)).map((n) => cloneModel(n));
242
+ copy.edges = [];
243
+
244
+ for (const source of copy.nodes) {
245
+ const queue = (outgoing.get(source.id) || []).map((edge) => ({ nodeId: edge.target, edges: [edge.id], trail: [source.id] }));
246
+ const seen = new Set();
247
+ while (queue.length) {
248
+ const item = queue.shift();
249
+ if (item.trail.includes(item.nodeId)) continue;
250
+ const key = `${item.nodeId}|${item.trail.join('>')}`;
251
+ if (seen.has(key)) continue;
252
+ seen.add(key);
253
+ if (visible.has(item.nodeId)) {
254
+ if (item.nodeId !== source.id && !copy.edges.some((e) => e.source === source.id && e.target === item.nodeId)) {
255
+ const first = model.edges.find((e) => e.id === item.edges[0]);
256
+ copy.edges.push(createEdge(source.id, item.nodeId, {
257
+ id: `Projected_${source.id}_${item.nodeId}`,
258
+ name: first?.name || '',
259
+ condition: first?.condition || '',
260
+ sourceEdgeIds: item.edges,
261
+ }));
262
+ }
263
+ continue;
264
+ }
265
+ for (const edge of outgoing.get(item.nodeId) || []) queue.push({ nodeId: edge.target, edges: [...item.edges, edge.id], trail: [...item.trail, item.nodeId] });
266
+ }
267
+ }
268
+
269
+ const sorted = [...copy.nodes].sort((a, b) => (a.x - b.x) || (a.y - b.y));
270
+ const startX = 110;
271
+ const startY = 150;
272
+ const gap = orientation === 'vertical' ? 104 : 260;
273
+ sorted.forEach((node, index) => {
274
+ const def = NODE_DEFINITIONS[node.type] || NODE_DEFINITIONS.generic;
275
+ if (orientation === 'vertical') {
276
+ node.x = 180;
277
+ node.y = startY + index * gap;
278
+ if (def.kind === 'task') { node.width = 260; node.height = 74; }
279
+ } else {
280
+ node.x = startX + index * gap;
281
+ node.y = 260;
282
+ if (def.kind === 'task') { node.width = 210; node.height = 78; }
283
+ }
284
+ });
285
+ copy.edges.forEach((e) => { e.waypoints = null; });
286
+ return copy;
287
+ }
288
+
289
+ export function projectModel(model, projection = 'standard') {
290
+ const rootGraph = getScopeGraph(model, model.id);
291
+ if (projection === 'approval') {
292
+ return buildVisibleGraph(rootGraph, (n) => { const def = NODE_DEFINITIONS[n.type] || {}; return ['start', 'end'].includes(def.eventStage) || ['userTask', 'callActivity'].includes(n.type); }, 'horizontal');
293
+ }
294
+ if (projection === 'compact') {
295
+ return buildVisibleGraph(rootGraph, (n) => { const def = NODE_DEFINITIONS[n.type] || {}; return ['start', 'end'].includes(def.eventStage) || ['userTask', 'callActivity'].includes(n.type); }, 'vertical');
296
+ }
297
+ return cloneModel(rootGraph);
298
+ }
299
+
300
+ function projectRuntime(runtime, projectedModel) {
301
+ if (!runtime) return null;
302
+ const normalized = normalizeRuntime(runtime);
303
+ const visited = new Set(normalized.visitedEdges);
304
+ const projectedVisited = [...normalized.visitedEdges];
305
+ for (const edge of projectedModel.edges) {
306
+ if (edge.sourceEdgeIds?.length && edge.sourceEdgeIds.every((id) => visited.has(id))) projectedVisited.push(edge.id);
307
+ }
308
+ const projectedEdgeVisits = [...(normalized.edgeVisits || [])];
309
+ for (const edge of projectedModel.edges) {
310
+ if (!edge.sourceEdgeIds?.length) continue;
311
+ const visits = edge.sourceEdgeIds
312
+ .map((id) => normalized.edgeVisits?.filter((visit) => visit.edgeId === id).at(-1))
313
+ .filter(Boolean);
314
+ if (visits.length === edge.sourceEdgeIds.length) {
315
+ const latest = visits.sort((a, b) => String(a.occurredAt || a.time || '').localeCompare(String(b.occurredAt || b.time || ''))).at(-1);
316
+ projectedEdgeVisits.push({ ...latest, id: `ProjectedVisit_${edge.id}`, edgeId: edge.id });
317
+ }
318
+ }
319
+ const transitions = normalized.transitions.map((transition) => ({
320
+ ...transition,
321
+ invalidatedEdgeIds: transition.invalidatedEdgeIds
322
+ ? [
323
+ ...transition.invalidatedEdgeIds,
324
+ ...projectedModel.edges
325
+ .filter((edge) => edge.sourceEdgeIds?.some((id) => transition.invalidatedEdgeIds.includes(id)))
326
+ .map((edge) => edge.id),
327
+ ]
328
+ : undefined,
329
+ }));
330
+ return { ...normalized, transitions, edgeVisits: projectedEdgeVisits, visitedEdges: projectedVisited };
331
+ }
332
+
333
+ const DEFAULT_TIMELINE_DESCRIPTION = '按实际处理顺序展示';
334
+ const DEFAULT_RUNTIME_DETAILS_LAYOUT = Object.freeze({
335
+ desktop: Object.freeze({ placement: 'popover' }),
336
+ mobile: Object.freeze({ placement: 'bottom' }),
337
+ });
338
+
339
+ function mergeTimelineOptions(current = {}, patch = {}) {
340
+ return { ...current, ...patch };
341
+ }
342
+
343
+ function mergeRuntimeDetailsOptions(current = {}, patch = {}) {
344
+ return {
345
+ autoOpen: patch.autoOpen ?? current.autoOpen ?? true,
346
+ desktop: { ...(current.desktop || DEFAULT_RUNTIME_DETAILS_LAYOUT.desktop), ...(patch.desktop || {}) },
347
+ mobile: { ...(current.mobile || DEFAULT_RUNTIME_DETAILS_LAYOUT.mobile), ...(patch.mobile || {}) },
348
+ };
349
+ }
350
+
351
+ function cssLength(value, fallback) {
352
+ const resolved = value ?? fallback;
353
+ return typeof resolved === 'number' ? `${resolved}px` : String(resolved);
354
+ }
355
+
356
+ export class BpmnViewer {
357
+ constructor(options = {}) {
358
+ if (!options.container) throw new Error('BpmnViewer requires container.');
359
+ this.options = options;
360
+ this.container = options.container;
361
+ this.themeController = options.themeController || new ThemeController({ root: this.container, theme: options.theme, onChange: options.onThemeChange });
362
+ this._ownsThemeController = !options.themeController;
363
+ this.runtimeAppearanceOptions = options.runtimeAppearance || {};
364
+ this.runtimeAppearance = createRuntimeAppearance(this.runtimeAppearanceOptions);
365
+ this.model = options.model;
366
+ this.runtime = options.runtime ? normalizeRuntime(options.runtime) : null;
367
+ this.responsive = options.responsive === true;
368
+ this.projection = options.projection || (this.runtime && this.responsive ? 'auto' : this.runtime ? 'approval' : 'standard');
369
+ this.activeProjection = 'standard';
370
+ this.mode = this.runtime ? 'instance' : 'viewer';
371
+ this.onElementClick = options.onElementClick || (() => {});
372
+ this.onTraceClick = options.onTraceClick || (() => {});
373
+ this.runtimePresenter = options.runtimePresenter || ((context) => createRuntimePresentation(context));
374
+ this.runtimeTraceProjector = options.runtimeTraceProjector || ((context) => createRuntimeTraceProjection(context));
375
+ this.runtimeTimelineRenderer = options.runtimeTimelineRenderer || renderDefaultRuntimeTimeline;
376
+ this.runtimeAssetResolver = options.runtimeAssetResolver || null;
377
+ this.svgExportOptions = options.svgExport || {};
378
+ this._svgExportPreview = null;
379
+ this._svgExportAssetCache = new Map();
380
+ this.onRuntimeTraceItemClick = options.onRuntimeTraceItemClick || (() => {});
381
+ this.onProjectionChange = options.onProjectionChange || (() => {});
382
+ this.runtimeDetailsRenderer = options.runtimeDetailsRenderer === null ? null : (options.runtimeDetailsRenderer || renderDefaultRuntimeDetails);
383
+ this.onRuntimeDetailsOpen = options.onRuntimeDetailsOpen || (() => {});
384
+ this.runtimeTransitionDetailsRenderer = options.runtimeTransitionDetailsRenderer === null
385
+ ? null
386
+ : (options.runtimeTransitionDetailsRenderer || renderDefaultRuntimeTransitionDetails);
387
+ this.onRuntimeTransitionDetailsOpen = options.onRuntimeTransitionDetailsOpen || (() => {});
388
+ this.onViewportChange = options.onViewportChange || (() => {});
389
+ this.runtimeTraceOptions = { ...(options.runtimeTraceOptions || {}) };
390
+ this.timelineOptions = mergeTimelineOptions({}, options.timeline || {});
391
+ this.runtimeDetailsOptions = mergeRuntimeDetailsOptions({}, options.runtimeDetails || {});
392
+ this.traceProjection = null;
393
+ this.projectedModel = null;
394
+ this.projectedRuntime = null;
395
+ this.runtimePresentation = null;
396
+ this.selection = null;
397
+ this._detailsCleanup = null;
398
+ this._detailsMotion = null;
399
+ this._detailsRoot = null;
400
+ this._detailsAnchor = null;
401
+ this._detailsLayout = null;
402
+ this._detailsTarget = null;
403
+ this._detailsAnchorFocusVisible = false;
404
+ this._assetControllers = new Set();
405
+ this._assetPreviewRoot = null;
406
+ this._outsideHandler = (event) => {
407
+ if (!this._detailsRoot || this._detailsRoot.contains(event.target) || this._detailsAnchor?.contains?.(event.target) || this._assetPreviewRoot?.contains(event.target)) return;
408
+ this.closeRuntimeDetails();
409
+ };
410
+ this._keyHandler = (event) => {
411
+ if (event.key !== 'Escape') return;
412
+ if (this._assetPreviewRoot) this.closeRuntimeAssetPreview();
413
+ else this.closeRuntimeDetails();
414
+ };
415
+ document.addEventListener('pointerdown', this._outsideHandler, true);
416
+ document.addEventListener('keydown', this._keyHandler);
417
+ this._rebuildProjection();
418
+ this._mountRenderer();
419
+ this._resizeObserver = typeof ResizeObserver === 'function'
420
+ ? new ResizeObserver(() => {
421
+ if (this.projection !== 'auto') return;
422
+ const next = this._resolveProjection();
423
+ if (next !== this.activeProjection) this.refresh();
424
+ })
425
+ : null;
426
+ this._resizeObserver?.observe(options.container);
427
+ }
428
+
429
+ _resolveProjection() {
430
+ if (this.projection !== 'auto') return this.projection;
431
+ if (!this.runtime) return 'standard';
432
+ const width = this.container.clientWidth || this.container.getBoundingClientRect?.().width || 0;
433
+ return width > 0 && width < 720 ? 'compact' : 'approval';
434
+ }
435
+
436
+ _rebuildProjection() {
437
+ const previousProjection = this.activeProjection;
438
+ this.activeProjection = this._resolveProjection();
439
+ if (this.runtime && ['approval', 'compact'].includes(this.activeProjection)) {
440
+ const sourcePresentation = this.runtimePresenter({ model: this.model, runtime: this.runtime, appearance: this.runtimeAppearance });
441
+ this.traceProjection = this.runtimeTraceProjector({ model: this.model, runtime: this.runtime, presentation: sourcePresentation, options: this.runtimeTraceOptions });
442
+ this.projectedModel = this.traceProjection.graphModel;
443
+ this.projectedRuntime = this.traceProjection.graphRuntime;
444
+ } else {
445
+ this.traceProjection = null;
446
+ this.projectedModel = projectModel(this.model, this.activeProjection);
447
+ this.projectedRuntime = projectRuntime(this.runtime, this.projectedModel);
448
+ }
449
+ this.runtimePresentation = this.runtimePresenter({ model: this.projectedModel, runtime: this.projectedRuntime, appearance: this.runtimeAppearance });
450
+ if (previousProjection !== this.activeProjection) this.onProjectionChange({ requested: this.projection, active: this.activeProjection });
451
+ }
452
+
453
+ _resolveTimelineOptions() {
454
+ const context = {
455
+ model: this.model,
456
+ runtime: this.runtime,
457
+ projection: this.activeProjection,
458
+ traceProjection: this.traceProjection,
459
+ viewer: this,
460
+ };
461
+ const resolve = (value, fallback) => {
462
+ const candidate = value === undefined ? fallback : value;
463
+ const result = typeof candidate === 'function' ? candidate(context) : candidate;
464
+ return result == null ? null : String(result);
465
+ };
466
+ return {
467
+ title: resolve(this.timelineOptions.title, this.model.name || '审批轨迹'),
468
+ description: resolve(this.timelineOptions.description, DEFAULT_TIMELINE_DESCRIPTION),
469
+ };
470
+ }
471
+
472
+ _currentRuntimeDetailsLayout() {
473
+ const configured = this.activeProjection === 'compact' ? this.runtimeDetailsOptions.mobile : this.runtimeDetailsOptions.desktop;
474
+ const placement = configured.placement || (this.activeProjection === 'compact' ? 'bottom' : 'popover');
475
+ return {
476
+ placement,
477
+ width: configured.width ?? (placement === 'bottom' ? '100%' : 360),
478
+ maxHeight: configured.maxHeight ?? (placement === 'bottom' ? '75%' : 360),
479
+ backdrop: configured.backdrop ?? placement !== 'popover',
480
+ dragToDismiss: configured.dragToDismiss ?? placement === 'bottom',
481
+ };
482
+ }
483
+
484
+ _activityInstancesFor({ traceItem = null, presentation = null } = {}) {
485
+ if (traceItem?.records) return [...traceItem.records];
486
+ if (presentation?.records) return [...presentation.records];
487
+ return [];
488
+ }
489
+
490
+ _emitTraceClick(payload) {
491
+ let element = payload.element || null;
492
+ if (element && payload.targetType === 'edge') {
493
+ const sourceIds = element.sourceEdgeIds?.length ? element.sourceEdgeIds : [element.id];
494
+ const sourceEdges = sourceIds.map((id) => this.model.edges.find((edge) => edge.id === id)).filter(Boolean);
495
+ element = sourceEdges.find((edge) => element.name && edge.name === element.name) || sourceEdges[0] || element;
496
+ } else if (element?.id && payload.targetType !== 'edge') {
497
+ element = this.model.nodes.find((node) => node.id === element.id) || element;
498
+ }
499
+ const presentation = payload.presentation || (element?.id && payload.targetType !== 'edge'
500
+ ? this.runtimePresentation.getNode(element.id)
501
+ : null);
502
+ this.onTraceClick({
503
+ targetType: payload.targetType,
504
+ elementId: payload.elementId || element?.id,
505
+ element,
506
+ visitId: payload.visitId ?? payload.traceItem?.visitId ?? null,
507
+ traceItem: payload.traceItem || null,
508
+ transition: payload.transition || null,
509
+ presentation,
510
+ activityInstances: this._activityInstancesFor({ traceItem: payload.traceItem, presentation }),
511
+ actions: payload.traceItem?.actions || (payload.transition?.action ? [payload.transition.action] : presentation?.actions) || [],
512
+ runtime: this.runtime,
513
+ projection: this.activeProjection,
514
+ originalEvent: payload.originalEvent,
515
+ viewer: this,
516
+ });
517
+ }
518
+
519
+ _mountRenderer() {
520
+ this.renderer?.destroy?.();
521
+ if (this.runtime && this.activeProjection === 'compact' && this.traceProjection) {
522
+ const timeline = this._resolveTimelineOptions();
523
+ this.renderer = new RuntimeTimelineHost(this.container, {
524
+ model: this.projectedModel,
525
+ runtime: this.projectedRuntime,
526
+ presentation: this.runtimePresentation,
527
+ projection: this.traceProjection,
528
+ ...timeline,
529
+ iconRegistry: this.options.iconRegistry,
530
+ appearance: this.runtimeAppearance,
531
+ themeState: this.themeController.getState(),
532
+ resolveAsset: (asset, purpose, action) => this._resolveRuntimeAsset(asset, purpose, action),
533
+ onAssetPreview: (asset, action) => this._openRuntimeAssetPreview(asset, action),
534
+ renderer: this.runtimeTimelineRenderer,
535
+ onItemClick: (item, event) => {
536
+ this.onRuntimeTraceItemClick({ item, event, projection: this.traceProjection, viewer: this });
537
+ if (item.kind === 'transition') {
538
+ const transition = this.runtimePresentation.getTransition(item.transitionId);
539
+ const element = this.model.nodes.find((candidate) => candidate.id === transition?.sourceElementId) || null;
540
+ this._emitTraceClick({ targetType: 'transition', element, transition, traceItem: item, originalEvent: event });
541
+ return;
542
+ }
543
+ const element = this.model.nodes.find((candidate) => candidate.id === item.elementId) || null;
544
+ const presentation = element ? this.runtimePresentation.getNode(element.id) : null;
545
+ this._emitTraceClick({ targetType: 'visit', element, traceItem: item, presentation, originalEvent: event });
546
+ },
547
+ onDetailsRequest: (item, anchor, event) => {
548
+ const node = this.model.nodes.find((candidate) => candidate.id === item.elementId);
549
+ if (!node) return;
550
+ this._select('node', node, event, { emitTrace: false });
551
+ const presentation = this.runtimePresentation.getNode(node.id);
552
+ if (presentation.hasDetails && this.runtimeDetailsOptions.autoOpen) {
553
+ this._openRuntimeDetails(node, presentation, anchor, {
554
+ restoreFocusVisible: event ? event.detail === 0 : Boolean(anchor?.matches?.(':focus-visible')),
555
+ });
556
+ }
557
+ },
558
+ onTransitionRequest: (item, anchor, event) => {
559
+ const transition = this.runtimePresentation.getTransition(item.transitionId);
560
+ if (this.runtimeDetailsOptions.autoOpen) {
561
+ this._openRuntimeTransitionDetails(transition, anchor, {
562
+ restoreFocusVisible: event ? event.detail === 0 : Boolean(anchor?.matches?.(':focus-visible')),
563
+ });
564
+ }
565
+ },
566
+ });
567
+ return;
568
+ }
569
+ this.renderer = new DiagramRenderer(this.container, {
570
+ themeController: this.themeController,
571
+ model: this.projectedModel,
572
+ visualModel: this.model,
573
+ runtime: this.projectedRuntime,
574
+ runtimePresenter: () => this.runtimePresentation,
575
+ mode: this.mode,
576
+ iconRegistry: this.options.iconRegistry,
577
+ nodeRenderers: this.options.nodeRenderers,
578
+ nodeRenderer: this.options.nodeRenderer,
579
+ svgExport: this.svgExportOptions,
580
+ runtimeAppearance: this.runtimeAppearance,
581
+ onNodeClick: (node, event) => this._select('node', node, event),
582
+ onEdgeClick: (edge, event) => this._select('edge', edge, event),
583
+ onCanvasClick: () => this.clearSelection(),
584
+ onRuntimeDetailsRequest: ({ node, presentation, anchor, event }) => {
585
+ this._emitTraceClick({ targetType: 'node', element: node, presentation, originalEvent: event });
586
+ if (this.runtimeDetailsOptions.autoOpen) this.openRuntimeDetails(node, presentation, anchor);
587
+ },
588
+ onRuntimeTransitionClick: (transition, event) => {
589
+ const element = this.model.nodes.find((node) => node.id === transition.sourceElementId) || null;
590
+ this._emitTraceClick({ targetType: 'transition', element, transition, originalEvent: event });
591
+ if (this.runtimeDetailsOptions.autoOpen) this.openRuntimeTransitionDetails(transition, event.currentTarget);
592
+ },
593
+ onViewportChange: (viewport) => { this._disposeRuntimeDetails(); this.onViewportChange(viewport); },
594
+ });
595
+ }
596
+
597
+ _select(kind, element, originalEvent = undefined, { emitTrace = true } = {}) {
598
+ this.selection = { kind, id: element.id };
599
+ this.renderer.setSelection(this.selection);
600
+ const presentation = kind === 'node' ? this.runtimePresentation.getNode(element.id) : null;
601
+ this.onElementClick({ kind, element, runtime: this.runtime, presentation });
602
+ if (emitTrace) this._emitTraceClick({ targetType: kind, element, presentation, originalEvent });
603
+ }
604
+
605
+ clearSelection() {
606
+ this.selection = null;
607
+ this.renderer.setSelection(null);
608
+ this.onElementClick(null);
609
+ }
610
+
611
+ _createRuntimeDetailsLayer(layout, anchor, restoreFocusVisible = Boolean(anchor?.matches?.(':focus-visible'))) {
612
+ const root = element('div', [
613
+ 'mb-runtime-details-layer',
614
+ 'is-entering',
615
+ this.activeProjection === 'compact' ? 'is-timeline' : '',
616
+ `placement-${layout.placement}`,
617
+ layout.backdrop ? 'has-backdrop' : '',
618
+ ].filter(Boolean).join(' '));
619
+ root.addEventListener('pointerdown', (event) => {
620
+ if (event.target === root && layout.backdrop) this.closeRuntimeDetails();
621
+ });
622
+ this.renderer.container.appendChild(root);
623
+ this._detailsRoot = root;
624
+ this._detailsAnchor = anchor;
625
+ this._detailsLayout = layout;
626
+ this._detailsAnchorFocusVisible = restoreFocusVisible;
627
+ return root;
628
+ }
629
+
630
+ _isRuntimeDetailsTargetOpen(kind, id) {
631
+ if (id == null || !this._detailsRoot?.parentNode || this._detailsRoot.classList.contains('is-closing')) return false;
632
+ return this._detailsTarget?.kind === kind && this._detailsTarget.id === id;
633
+ }
634
+
635
+ _createRuntimeDetailsDragHandle(panel, layout) {
636
+ if (layout.placement !== 'bottom' || !layout.dragToDismiss) return null;
637
+ panel.classList.add('is-drag-dismissible');
638
+ const handle = element('div', 'mb-runtime-details-drag-handle');
639
+ handle.dataset.runtimeDetailsDragHandle = '';
640
+ handle.setAttribute('aria-hidden', 'true');
641
+ panel.prepend(handle);
642
+ return handle;
643
+ }
644
+
645
+ _suppressTimelinePointerFocus(anchor) {
646
+ if (!anchor?.matches?.('.mb-runtime-timeline-card-main, .mb-runtime-timeline-transition-main')) return;
647
+ const className = 'is-pointer-focus-restored';
648
+ const clear = () => {
649
+ anchor.classList.remove(className);
650
+ anchor.removeEventListener('blur', clear);
651
+ anchor.removeEventListener('keydown', clear);
652
+ anchor.removeEventListener('pointerdown', clear);
653
+ };
654
+ anchor.classList.add(className);
655
+ anchor.addEventListener('blur', clear, { once: true });
656
+ anchor.addEventListener('keydown', clear, { once: true });
657
+ anchor.addEventListener('pointerdown', clear, { once: true });
658
+ }
659
+
660
+ _finalizeRuntimeDetails({ root, cleanup, motion, anchor, restoreFocus, restoreFocusVisible = false }) {
661
+ motion?.destroy();
662
+ cleanup?.();
663
+ root?.remove();
664
+ if (this._detailsRoot !== root) return;
665
+ this._detailsCleanup = null;
666
+ this._detailsMotion = null;
667
+ this._detailsRoot = null;
668
+ this._detailsAnchor = null;
669
+ this._detailsLayout = null;
670
+ this._detailsTarget = null;
671
+ this._detailsAnchorFocusVisible = false;
672
+ if (restoreFocus && anchor?.isConnected && typeof anchor.focus === 'function') {
673
+ requestAnimationFrame(() => {
674
+ if (this._detailsRoot || !anchor.isConnected) return;
675
+ if (!restoreFocusVisible) this._suppressTimelinePointerFocus(anchor);
676
+ anchor.focus({ preventScroll: true });
677
+ });
678
+ }
679
+ }
680
+
681
+ _prepareRuntimeDetailsMotion(layout) {
682
+ const root = this._detailsRoot;
683
+ const panel = root?.firstElementChild;
684
+ if (!root || !panel) return;
685
+ panel.classList.add('mb-runtime-details-motion-panel');
686
+ const handle = this._createRuntimeDetailsDragHandle(panel, layout);
687
+ const cleanup = this._detailsCleanup;
688
+ const anchor = this._detailsAnchor;
689
+ const restoreFocusVisible = this._detailsAnchorFocusVisible;
690
+ let motion = null;
691
+ motion = createRuntimeDetailsMotionController({
692
+ layer: root,
693
+ panel,
694
+ handle,
695
+ placement: layout.placement,
696
+ dragToDismiss: layout.dragToDismiss,
697
+ onDismissed: ({ restoreFocus }) => this._finalizeRuntimeDetails({
698
+ root,
699
+ cleanup,
700
+ motion,
701
+ anchor,
702
+ restoreFocus,
703
+ restoreFocusVisible,
704
+ }),
705
+ });
706
+ this._detailsMotion = motion;
707
+ }
708
+
709
+ async _resolveRuntimeAsset(asset, purpose, action, externalSignal = null) {
710
+ if (!this.runtimeAssetResolver) return null;
711
+ const controller = new AbortController();
712
+ const abort = () => controller.abort(externalSignal?.reason);
713
+ if (externalSignal?.aborted) abort();
714
+ else externalSignal?.addEventListener?.('abort', abort, { once: true });
715
+ this._assetControllers.add(controller);
716
+ try {
717
+ const resolved = await this.runtimeAssetResolver(asset, { purpose, action, signal: controller.signal });
718
+ if (!resolved || controller.signal.aborted) return null;
719
+ const url = new URL(String(resolved), this.container.ownerDocument.baseURI);
720
+ return ['http:', 'https:', 'blob:'].includes(url.protocol) || (purpose === 'export' && url.protocol === 'data:') ? url.href : null;
721
+ } catch {
722
+ return null;
723
+ } finally {
724
+ externalSignal?.removeEventListener?.('abort', abort);
725
+ this._assetControllers.delete(controller);
726
+ }
727
+ }
728
+
729
+ _openRuntimeAssetPreview(asset, action) {
730
+ this.closeRuntimeAssetPreview();
731
+ if (!asset?.mediaType?.startsWith('image/')) return false;
732
+ const root = element('div', 'mb-runtime-asset-preview');
733
+ root.setAttribute('role', 'dialog');
734
+ root.setAttribute('aria-modal', 'true');
735
+ root.setAttribute('aria-label', `图片预览:${asset.name}`);
736
+ root.addEventListener('pointerdown', (event) => { if (event.target === root) this.closeRuntimeAssetPreview(); });
737
+ const dialog = element('section', 'mb-runtime-asset-preview-dialog');
738
+ const header = element('header', 'mb-runtime-asset-preview-head');
739
+ header.appendChild(element('strong', '', asset.name));
740
+ const close = runtimeDetailsCloseButton({ viewer: this, label: '关闭图片预览', close: () => this.closeRuntimeAssetPreview() });
741
+ header.appendChild(close);
742
+ const body = element('div', 'mb-runtime-asset-preview-body');
743
+ const state = element('span', 'mb-runtime-asset-preview-state', '图片加载中');
744
+ body.appendChild(state);
745
+ dialog.append(header, body);
746
+ root.appendChild(dialog);
747
+ this.renderer.container.appendChild(root);
748
+ this._assetPreviewRoot = root;
749
+ Promise.resolve(this._resolveRuntimeAsset(asset, 'preview', action)).then((url) => {
750
+ if (!root.isConnected) return;
751
+ if (!url) { state.textContent = '资源不可用'; return; }
752
+ const image = document.createElement('img');
753
+ image.alt = asset.name;
754
+ image.addEventListener('load', () => {
755
+ if (root.isConnected) body.replaceChildren(image);
756
+ }, { once: true });
757
+ image.addEventListener('error', () => {
758
+ if (root.isConnected) state.textContent = '资源不可用';
759
+ }, { once: true });
760
+ image.src = url;
761
+ });
762
+ requestAnimationFrame(() => close.focus({ preventScroll: true }));
763
+ return true;
764
+ }
765
+
766
+ closeRuntimeAssetPreview() {
767
+ this._assetPreviewRoot?.remove();
768
+ this._assetPreviewRoot = null;
769
+ }
770
+
771
+ openRuntimeDetails(node, presentation = this.runtimePresentation.getNode(node.id), anchor = null) {
772
+ return this._openRuntimeDetails(node, presentation, anchor);
773
+ }
774
+
775
+ _openRuntimeDetails(node, presentation, anchor, { restoreFocusVisible = Boolean(anchor?.matches?.(':focus-visible')) } = {}) {
776
+ if (!this.runtimeDetailsRenderer || !node || !presentation) return false;
777
+ if (this._isRuntimeDetailsTargetOpen('node', node.id)) return true;
778
+ this._disposeRuntimeDetails();
779
+ const layout = this._currentRuntimeDetailsLayout();
780
+ const root = this._createRuntimeDetailsLayer(layout, anchor, restoreFocusVisible);
781
+ this._detailsTarget = { kind: 'node', id: node.id };
782
+ const close = () => this.closeRuntimeDetails();
783
+ this._detailsCleanup = this.runtimeDetailsRenderer({
784
+ container: root,
785
+ viewer: this,
786
+ node,
787
+ presentation,
788
+ anchor,
789
+ layout,
790
+ close,
791
+ resolveAsset: (asset, purpose, action) => this._resolveRuntimeAsset(asset, purpose, action),
792
+ themeState: this.themeController.getState(),
793
+ runtimeAppearance: this.runtimeAppearance,
794
+ }) || null;
795
+ this._prepareRuntimeDetailsMotion(layout);
796
+ this._positionRuntimeDetails(anchor, layout);
797
+ this._detailsMotion?.enter();
798
+ this.onRuntimeDetailsOpen({ node, presentation, layout, close });
799
+ return true;
800
+ }
801
+
802
+ openRuntimeTransitionDetails(transitionOrId, anchor = null) {
803
+ const transition = typeof transitionOrId === 'string'
804
+ ? this.runtimePresentation.getTransition(transitionOrId)
805
+ : this.runtimePresentation.getTransition(transitionOrId?.id) || transitionOrId;
806
+ return this._openRuntimeTransitionDetails(transition, anchor);
807
+ }
808
+
809
+ _openRuntimeTransitionDetails(transition, anchor, { restoreFocusVisible = Boolean(anchor?.matches?.(':focus-visible')) } = {}) {
810
+ if (!this.runtimeTransitionDetailsRenderer || !transition) return false;
811
+ if (this._isRuntimeDetailsTargetOpen('transition', transition.id)) return true;
812
+ this._disposeRuntimeDetails();
813
+ const sourceNode = this.projectedModel.nodes.find((node) => node.id === transition.sourceElementId) || null;
814
+ const targetNode = this.projectedModel.nodes.find((node) => node.id === transition.targetElementId) || null;
815
+ const layout = this._currentRuntimeDetailsLayout();
816
+ const root = this._createRuntimeDetailsLayer(layout, anchor, restoreFocusVisible);
817
+ this._detailsTarget = { kind: 'transition', id: transition.id };
818
+ const close = () => this.closeRuntimeDetails();
819
+ const locate = (node) => {
820
+ if (!node) return;
821
+ this._select('node', node);
822
+ this.closeRuntimeDetails();
823
+ };
824
+ this._detailsCleanup = this.runtimeTransitionDetailsRenderer({
825
+ container: root,
826
+ viewer: this,
827
+ transition,
828
+ sourceNode,
829
+ targetNode,
830
+ anchor,
831
+ layout,
832
+ close,
833
+ resolveAsset: (asset, purpose, action) => this._resolveRuntimeAsset(asset, purpose, action),
834
+ themeState: this.themeController.getState(),
835
+ runtimeAppearance: this.runtimeAppearance,
836
+ locateSource: () => locate(sourceNode),
837
+ locateTarget: () => locate(targetNode),
838
+ }) || null;
839
+ this._prepareRuntimeDetailsMotion(layout);
840
+ this._positionRuntimeDetails(anchor, layout);
841
+ this._detailsMotion?.enter();
842
+ this.onRuntimeTransitionDetailsOpen({ transition, sourceNode, targetNode, layout, close });
843
+ return true;
844
+ }
845
+
846
+ _positionRuntimeDetails(anchor, layout = this._detailsLayout || this._currentRuntimeDetailsLayout()) {
847
+ if (!this._detailsRoot) return;
848
+ const host = this.renderer.container.getBoundingClientRect();
849
+ const target = anchor?.getBoundingClientRect?.() || host;
850
+ const popover = this._detailsRoot.firstElementChild;
851
+ if (!popover) return;
852
+ popover.style.width = cssLength(layout.width, layout.placement === 'bottom' ? '100%' : 360);
853
+ popover.style.maxHeight = cssLength(layout.maxHeight, layout.placement === 'bottom' ? '75%' : 360);
854
+ popover.style.setProperty('--mb-runtime-details-max-height', cssLength(layout.maxHeight, layout.placement === 'bottom' ? '75%' : 360));
855
+ if (layout.placement !== 'bottom') popover.style.maxWidth = 'calc(100% - 24px)';
856
+ if (layout.placement === 'bottom') {
857
+ popover.style.left = '0';
858
+ popover.style.right = '0';
859
+ popover.style.top = 'auto';
860
+ popover.style.bottom = '0';
861
+ return;
862
+ }
863
+ const measured = popover.getBoundingClientRect();
864
+ const width = popover.offsetWidth || measured.width || Math.min(360, Math.max(0, host.width - 24));
865
+ const height = popover.offsetHeight || measured.height;
866
+ if (layout.placement === 'center') {
867
+ popover.style.left = `${Math.max(12, (host.width - width) / 2)}px`;
868
+ popover.style.top = `${Math.max(12, (host.height - height) / 2)}px`;
869
+ return;
870
+ }
871
+ let left = target.left - host.left + target.width / 2 - width / 2;
872
+ left = Math.max(12, Math.min(left, host.width - width - 12));
873
+ let top = target.top - host.top - height - 10;
874
+ if (top < 12) top = Math.min(host.height - height - 12, target.bottom - host.top + 10);
875
+ popover.style.left = `${Math.max(12, left)}px`;
876
+ popover.style.top = `${Math.max(12, top)}px`;
877
+ }
878
+
879
+ closeRuntimeDetails() {
880
+ this.closeRuntimeAssetPreview();
881
+ if (!this._detailsRoot) return;
882
+ if (this._detailsMotion) {
883
+ this._detailsMotion.dismiss({ restoreFocus: true });
884
+ return;
885
+ }
886
+ this._finalizeRuntimeDetails({
887
+ root: this._detailsRoot,
888
+ cleanup: this._detailsCleanup,
889
+ motion: null,
890
+ anchor: this._detailsAnchor,
891
+ restoreFocus: true,
892
+ restoreFocusVisible: this._detailsAnchorFocusVisible,
893
+ });
894
+ }
895
+
896
+ _disposeRuntimeDetails() {
897
+ this.closeRuntimeAssetPreview();
898
+ if (!this._detailsRoot) return;
899
+ if (this._detailsMotion) {
900
+ this._detailsMotion.dismiss({ immediate: true, restoreFocus: false });
901
+ return;
902
+ }
903
+ this._finalizeRuntimeDetails({
904
+ root: this._detailsRoot,
905
+ cleanup: this._detailsCleanup,
906
+ motion: null,
907
+ anchor: this._detailsAnchor,
908
+ restoreFocus: false,
909
+ restoreFocusVisible: this._detailsAnchorFocusVisible,
910
+ });
911
+ }
912
+
913
+ refresh() {
914
+ this._disposeRuntimeDetails();
915
+ this._rebuildProjection();
916
+ this._mountRenderer();
917
+ if (this.selection) this.renderer.setSelection?.(this.selection);
918
+ }
919
+
920
+ setModel(model) { this.model = model; this._svgExportAssetCache.clear(); this.refresh(); }
921
+ setRuntime(runtime) { this.runtime = runtime ? normalizeRuntime(runtime) : null; this._svgExportAssetCache.clear(); this.refresh(); }
922
+ setProjection(projection) {
923
+ if (!['auto', 'standard', 'approval', 'compact'].includes(projection)) return;
924
+ this.projection = projection;
925
+ this.refresh();
926
+ requestAnimationFrame(() => this.renderer.fitView());
927
+ }
928
+ setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver } = {}) {
929
+ let changed = false;
930
+ if (runtimeAssetResolver !== undefined) {
931
+ this.runtimeAssetResolver = runtimeAssetResolver || null;
932
+ this._svgExportAssetCache.clear();
933
+ changed = true;
934
+ }
935
+ if (runtimeTraceOptions !== undefined) {
936
+ this.runtimeTraceOptions = { ...this.runtimeTraceOptions, ...(runtimeTraceOptions || {}) };
937
+ changed = true;
938
+ }
939
+ if (timeline !== undefined) {
940
+ this.timelineOptions = mergeTimelineOptions(this.timelineOptions, timeline || {});
941
+ changed = true;
942
+ }
943
+ if (runtimeDetails !== undefined) {
944
+ this.runtimeDetailsOptions = mergeRuntimeDetailsOptions(this.runtimeDetailsOptions, runtimeDetails || {});
945
+ changed = true;
946
+ }
947
+ if (changed) this.refresh();
948
+ }
949
+ setTheme(theme) { return this.themeController.setTheme(theme); }
950
+ setThemeMode(mode) { return this.themeController.setMode(mode); }
951
+ getThemeState() { return this.themeController.getState(); }
952
+ setRuntimeAppearance(runtimeAppearance) {
953
+ this.runtimeAppearanceOptions = runtimeAppearance || {};
954
+ this.runtimeAppearance = createRuntimeAppearance(this.runtimeAppearanceOptions);
955
+ this.refresh();
956
+ }
957
+ exportSvg(options = {}) {
958
+ const isTimeline = Boolean(this.runtime && this.activeProjection === 'compact' && this.traceProjection);
959
+ const label = isTimeline ? '移动时间线' : this.activeProjection === 'approval' ? '实际路径' : this.runtime ? '完整 BPMN' : '流程展示';
960
+ const filename = options.filename || this.svgExportOptions.filename || `${this.model.name || this.model.id || 'process'}-${label}`;
961
+ if (isTimeline) {
962
+ const timeline = this._resolveTimelineOptions();
963
+ return exportRuntimeTimelineSvg({
964
+ document: this.container.ownerDocument,
965
+ root: this.container,
966
+ model: this.model,
967
+ runtime: this.runtime,
968
+ projection: this.traceProjection,
969
+ ...timeline,
970
+ themeController: this.themeController,
971
+ iconRegistry: this.options.iconRegistry,
972
+ runtimeAppearance: this.runtimeAppearance,
973
+ resolveAsset: (asset, purpose, action) => this._resolveRuntimeAsset(asset, purpose, action, options.signal),
974
+ assetCache: this._svgExportAssetCache,
975
+ timelineRenderer: this.svgExportOptions.runtimeTimelineRenderer,
976
+ label,
977
+ }, { ...this.svgExportOptions, ...options, filename });
978
+ }
979
+ return this.renderer.exportSvg({ ...this.svgExportOptions, ...options, filename, label });
980
+ }
981
+ openSvgExportPreview(options = {}) {
982
+ if (this._svgExportPreview && !this._svgExportPreview.closed) {
983
+ this._svgExportPreview.focus();
984
+ return this._svgExportPreview;
985
+ }
986
+ this._svgExportPreview = openSvgExportPreview({
987
+ container: this.container,
988
+ title: options.previewTitle || '导出 SVG',
989
+ initialTheme: options.theme || 'current',
990
+ createArtifact: (previewOptions) => this.exportSvg({ ...options, ...previewOptions }),
991
+ onClose: () => { this._svgExportPreview = null; },
992
+ onDownload: options.onDownload,
993
+ });
994
+ return this._svgExportPreview;
995
+ }
996
+ fitView() { this.renderer.fitView(); }
997
+ zoomBy(delta) { this.renderer.zoomBy(delta); }
998
+ destroy() {
999
+ this._svgExportPreview?.close?.({ immediate: true });
1000
+ this._svgExportPreview = null;
1001
+ this._svgExportAssetCache.clear();
1002
+ this._disposeRuntimeDetails();
1003
+ this._assetControllers.forEach((controller) => controller.abort());
1004
+ this._assetControllers.clear();
1005
+ this._resizeObserver?.disconnect?.();
1006
+ document.removeEventListener('pointerdown', this._outsideHandler, true);
1007
+ document.removeEventListener('keydown', this._keyHandler);
1008
+ this.renderer.destroy();
1009
+ if (this._ownsThemeController) this.themeController.destroy();
1010
+ }
1011
+ }