@bpmn-nova/studio 0.3.1-preview → 0.3.3-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 +109 -4
- package/dist/canvas.js +5 -2
- package/dist/context-menu.js +1 -1
- package/dist/controller.js +82 -4
- package/dist/index.d.ts +100 -5
- 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/palette/index.d.ts +14 -2
- package/dist/modules/palette/index.js +3 -3
- package/dist/modules/properties-bpmn/index.js +3 -1
- package/dist/modules/renderer-svg/index.d.ts +4 -0
- package/dist/modules/renderer-svg/index.js +80 -19
- package/dist/modules/viewer/index.d.ts +3 -0
- package/dist/modules/viewer/index.js +21 -4
- package/dist/shell.js +494 -171
- package/dist/styles.css +12 -3
- package/llms-full.txt +3322 -0
- package/llms.txt +236 -0
- package/package.json +4 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NODE_DEFINITIONS, edgeWaypoints, roundedPath, routeRuntimeTransition, smoothPath } from '../core/index.js';
|
|
2
2
|
import { createDefaultIconRegistry, resolveNodeVisual } from '../icons/index.js';
|
|
3
|
+
import { resolveNodeSubtitle, supportsNodeSubtitle } from '../node-presentation/index.js';
|
|
3
4
|
import { createRuntimePresentation } from '../runtime/index.js';
|
|
4
5
|
import { createRuntimeAppearance } from '../theme/index.js';
|
|
5
6
|
|
|
@@ -304,7 +305,7 @@ function nodeToneName(node) {
|
|
|
304
305
|
return ({ userTask: 'user-task', serviceTask: 'service-task', scriptTask: 'script-task', businessRuleTask: 'rule-task', sendTask: 'message-task', receiveTask: 'message-task', manualTask: 'manual-task' })[node.type] || 'primary';
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
function renderDefaultNode({ document, group, node, definition, visual, presentation, theme, iconRegistry }) {
|
|
308
|
+
function renderDefaultNode({ document, group, node, definition, visual, presentation, subtitle: definitionSubtitle, theme, iconRegistry }) {
|
|
308
309
|
const colors = theme.colors;
|
|
309
310
|
const statusTone = tone(theme, presentation?.status === 'rejected' ? 'danger' : presentation?.status === 'active' ? 'primary' : presentation?.status === 'completed' ? 'success' : 'neutral');
|
|
310
311
|
const typeTone = tone(theme, nodeToneName(node));
|
|
@@ -360,7 +361,10 @@ function renderDefaultNode({ document, group, node, definition, visual, presenta
|
|
|
360
361
|
const iconY = y + (height - iconSize) / 2;
|
|
361
362
|
const copyX = iconX + iconSize + 12;
|
|
362
363
|
const title = node.name || definition.label;
|
|
363
|
-
const subtitle =
|
|
364
|
+
const subtitle = definitionSubtitle === undefined
|
|
365
|
+
? presentation?.actionSummary || presentation?.summary || (node.properties?.assignee || node.properties?.candidateGroups || node.properties?.candidateUsers || '')
|
|
366
|
+
: definitionSubtitle;
|
|
367
|
+
const hasSubtitleRow = subtitle !== null;
|
|
364
368
|
const hasStatus = Boolean(presentation?.status && presentation.status !== 'idle');
|
|
365
369
|
const label = hasStatus ? String(presentation.statusLabel || presentation.status) : '';
|
|
366
370
|
const statusTypography = { fontSize: 10.5, fontWeight: 650, fontFamily: theme.fontFamily };
|
|
@@ -371,18 +375,24 @@ function renderDefaultNode({ document, group, node, definition, visual, presenta
|
|
|
371
375
|
const titleTypography = { fontSize: 14, fontWeight: 650, fontFamily: theme.fontFamily };
|
|
372
376
|
const subtitleTypography = { fontSize: 11.5, fontWeight: 400, fontFamily: theme.fontFamily };
|
|
373
377
|
const fittedTitle = fitText(document, title, titleMaxWidth, titleTypography);
|
|
374
|
-
const fittedSubtitle = fitText(document, subtitle, subtitleMaxWidth, subtitleTypography);
|
|
375
|
-
const tooltip =
|
|
378
|
+
const fittedSubtitle = hasSubtitleRow ? fitText(document, subtitle, subtitleMaxWidth, subtitleTypography) : '';
|
|
379
|
+
const tooltip = subtitle === null || subtitle === '' ? title : `${title}\n${subtitle}`;
|
|
380
|
+
group.setAttribute('role', 'group');
|
|
381
|
+
group.setAttribute('aria-label', tooltip);
|
|
376
382
|
if (tooltip && (fittedTitle !== title || fittedSubtitle !== String(subtitle || ''))) group.appendChild(svgElement(document, 'title', {}, tooltip));
|
|
377
383
|
|
|
378
384
|
const clipId = safeSvgId(node.id);
|
|
379
|
-
const
|
|
380
|
-
const
|
|
385
|
+
const titleY = hasSubtitleRow ? y + 27 : y + height / 2 + 5;
|
|
386
|
+
const titleClipY = hasSubtitleRow ? y + 9 : y + (height - 23) / 2;
|
|
387
|
+
const titleClip = appendClipPath(document, group, `nova-node-${clipId}-title-clip`, { x: copyX, y: titleClipY, width: titleMaxWidth, height: 23 });
|
|
388
|
+
const subtitleClip = hasSubtitleRow
|
|
389
|
+
? appendClipPath(document, group, `nova-node-${clipId}-subtitle-clip`, { x: copyX, y: y + 33, width: subtitleMaxWidth, height: 21 })
|
|
390
|
+
: null;
|
|
381
391
|
group.appendChild(svgElement(document, 'rect', { x, y, width, height, rx: 12, fill: colors.surface, stroke: hasStatus ? statusTone.border : colors.border, 'stroke-width': presentation?.status === 'active' ? 2 : 1.2, filter: 'url(#nova-export-shadow)' }));
|
|
382
392
|
group.appendChild(svgElement(document, 'rect', { x, y: y + 12, width: 3, height: Math.max(12, height - 24), rx: 1.5, fill: typeTone.strong }));
|
|
383
393
|
group.appendChild(svgElement(document, 'rect', { x: iconX, y: iconY, width: iconSize, height: iconSize, rx: 9, fill: typeTone.background }));
|
|
384
394
|
appendIcon(document, group, icon, { x: iconX + 8, y: iconY + 8, width: iconSize - 16, height: iconSize - 16 }, typeTone.foreground);
|
|
385
|
-
if (fittedTitle) group.appendChild(svgElement(document, 'text', { x: copyX, y:
|
|
395
|
+
if (fittedTitle) group.appendChild(svgElement(document, 'text', { x: copyX, y: titleY, fill: colors.text, 'font-size': titleTypography.fontSize, 'font-weight': titleTypography.fontWeight, 'font-family': titleTypography.fontFamily, 'clip-path': titleClip }, fittedTitle));
|
|
386
396
|
if (fittedSubtitle) group.appendChild(svgElement(document, 'text', { x: copyX, y: y + 49, fill: colors.textSecondary, 'font-size': subtitleTypography.fontSize, 'font-family': subtitleTypography.fontFamily, 'clip-path': subtitleClip }, fittedSubtitle));
|
|
387
397
|
if (hasStatus) {
|
|
388
398
|
const fittedLabel = fitText(document, label, labelWidth - 16, statusTypography);
|
|
@@ -479,6 +489,8 @@ export async function exportDiagramSvg(context = {}, options = {}) {
|
|
|
479
489
|
}
|
|
480
490
|
|
|
481
491
|
const nodeLayer = svgElement(document, 'g', { 'data-layer': 'nodes' });
|
|
492
|
+
const runtimeMode = context.mode === 'instance' || Boolean(context.runtime);
|
|
493
|
+
const definitionMode = context.mode === 'viewer' ? 'viewer' : 'design';
|
|
482
494
|
const order = { participant: 0, lane: 1, group: 2, container: 3, data: 4, dataStore: 4, annotation: 4, task: 5, gateway: 6, event: 7, boundary: 8 };
|
|
483
495
|
const nodes = [...(model.nodes || [])].sort((a, b) => (order[NODE_DEFINITIONS[a.type]?.kind] ?? 5) - (order[NODE_DEFINITIONS[b.type]?.kind] ?? 5));
|
|
484
496
|
for (const node of nodes) {
|
|
@@ -500,7 +512,19 @@ export async function exportDiagramSvg(context = {}, options = {}) {
|
|
|
500
512
|
} else if (context.htmlNodeRenderers?.[node.type] || context.htmlNodeRenderers?.[definition.kind] || context.htmlNodeRenderer) {
|
|
501
513
|
warnings.push({ code: 'custom-node-renderer-fallback', message: `节点“${node.name || node.id}”没有 SVG Renderer,已使用标准视觉。`, elementId: node.id });
|
|
502
514
|
}
|
|
503
|
-
if (!rendered)
|
|
515
|
+
if (!rendered) {
|
|
516
|
+
const subtitle = !runtimeMode && supportsNodeSubtitle(definition)
|
|
517
|
+
? resolveNodeSubtitle({
|
|
518
|
+
node,
|
|
519
|
+
definition,
|
|
520
|
+
model,
|
|
521
|
+
mode: definitionMode,
|
|
522
|
+
surface: 'svg-export',
|
|
523
|
+
resolver: context.nodeSubtitleResolver,
|
|
524
|
+
})
|
|
525
|
+
: undefined;
|
|
526
|
+
renderDefaultNode({ document, group, node, definition, visual, presentation: nodePresentation, subtitle, theme, iconRegistry });
|
|
527
|
+
}
|
|
504
528
|
nodeLayer.appendChild(group);
|
|
505
529
|
}
|
|
506
530
|
svg.appendChild(nodeLayer);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BpmnNode, NodeDefinition, ProcessModel } from '../core/index.js'
|
|
2
|
+
|
|
3
|
+
export interface NodeSubtitleResolverContext {
|
|
4
|
+
readonly node: BpmnNode
|
|
5
|
+
readonly definition: NodeDefinition
|
|
6
|
+
readonly model: ProcessModel
|
|
7
|
+
readonly mode: 'design' | 'viewer'
|
|
8
|
+
readonly surface: 'canvas' | 'svg-export'
|
|
9
|
+
readonly defaultSubtitle: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NodeSubtitleResolver = (
|
|
13
|
+
context: Readonly<NodeSubtitleResolverContext>,
|
|
14
|
+
) => string | null | undefined
|
|
15
|
+
|
|
16
|
+
export function resolveDefaultNodeSubtitle(node: BpmnNode): string
|
|
17
|
+
export function supportsNodeSubtitle(definition: NodeDefinition): boolean
|
|
18
|
+
export function resolveNodeSubtitle(context: {
|
|
19
|
+
node: BpmnNode
|
|
20
|
+
definition: NodeDefinition
|
|
21
|
+
model: ProcessModel
|
|
22
|
+
mode: 'design' | 'viewer'
|
|
23
|
+
surface: 'canvas' | 'svg-export'
|
|
24
|
+
resolver?: NodeSubtitleResolver | null
|
|
25
|
+
}): string | null
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function resolverError(node, reason) {
|
|
2
|
+
const nodeId = node?.id || '<unknown>';
|
|
3
|
+
const detail = reason instanceof Error ? reason.message : String(reason);
|
|
4
|
+
const error = new Error(`Node subtitle resolver failed for node "${nodeId}": ${detail}`);
|
|
5
|
+
if (reason instanceof Error) error.cause = reason;
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveDefaultNodeSubtitle(node) {
|
|
10
|
+
const properties = node?.properties || {};
|
|
11
|
+
if (node?.type === 'userTask') return properties.assignee || properties.candidateGroups || properties.candidateUsers || '待配置审批人';
|
|
12
|
+
if (['serviceTask', 'sendTask', 'businessRuleTask'].includes(node?.type)) return properties.implementation || '待配置执行实现';
|
|
13
|
+
if (node?.type === 'scriptTask') return properties.scriptFormat || 'Script';
|
|
14
|
+
if (node?.type === 'callActivity') return properties.calledElement || '待配置调用流程';
|
|
15
|
+
if (node?.type === 'receiveTask') return '等待消息或外部触发';
|
|
16
|
+
if (node?.type === 'manualTask') return '人工线下处理';
|
|
17
|
+
if (node?.type === 'subProcess') return '可折叠子流程';
|
|
18
|
+
if (node?.type === 'eventSubProcess') return '事件触发子流程';
|
|
19
|
+
if (node?.type === 'transaction') return '事务边界';
|
|
20
|
+
return node?.bpmnType?.replace('bpmn:', '') || '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function supportsNodeSubtitle(definition) {
|
|
24
|
+
return definition?.kind === 'task' || definition?.kind === 'container';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveNodeSubtitle({ node, definition, model, mode, surface, resolver }) {
|
|
28
|
+
const defaultSubtitle = resolveDefaultNodeSubtitle(node);
|
|
29
|
+
if (typeof resolver !== 'function') return defaultSubtitle;
|
|
30
|
+
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = resolver(Object.freeze({
|
|
34
|
+
node,
|
|
35
|
+
definition,
|
|
36
|
+
model,
|
|
37
|
+
mode,
|
|
38
|
+
surface,
|
|
39
|
+
defaultSubtitle,
|
|
40
|
+
}));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw resolverError(node, error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result === undefined) return defaultSubtitle;
|
|
46
|
+
if (result === null || typeof result === 'string') return result;
|
|
47
|
+
if (result && typeof result.then === 'function') {
|
|
48
|
+
throw resolverError(node, 'Promise results are not supported; return a string, null, or undefined.');
|
|
49
|
+
}
|
|
50
|
+
throw resolverError(node, `invalid result type "${typeof result}"; return a string, null, or undefined.`);
|
|
51
|
+
}
|
|
@@ -39,6 +39,18 @@ export const defaultBpmnPaletteProvider: PaletteProvider
|
|
|
39
39
|
export function createPaletteRegistry(): PaletteRegistry
|
|
40
40
|
export function createDefaultPaletteRegistry(options?: { providers?: PaletteProvider[] }): PaletteRegistry
|
|
41
41
|
|
|
42
|
+
export type PaletteItemRenderer = (context: {
|
|
43
|
+
container: HTMLElement
|
|
44
|
+
item: PaletteItem
|
|
45
|
+
panel: PalettePanel
|
|
46
|
+
beginCreate(): unknown
|
|
47
|
+
}) => void | (() => void)
|
|
48
|
+
export type PaletteSectionRenderer = (context: {
|
|
49
|
+
container: HTMLElement
|
|
50
|
+
section: PaletteSection
|
|
51
|
+
panel: PalettePanel
|
|
52
|
+
}) => void | (() => void)
|
|
53
|
+
|
|
42
54
|
export interface PalettePanelOptions {
|
|
43
55
|
container: HTMLElement
|
|
44
56
|
registry: PaletteRegistry
|
|
@@ -46,8 +58,8 @@ export interface PalettePanelOptions {
|
|
|
46
58
|
interactions: { beginCreate(intent: PaletteCreateIntent): unknown; serialize(session: unknown): string }
|
|
47
59
|
canvas?: unknown
|
|
48
60
|
iconRegistry?: IconRegistry
|
|
49
|
-
renderItem?:
|
|
50
|
-
renderSection?:
|
|
61
|
+
renderItem?: PaletteItemRenderer | null
|
|
62
|
+
renderSection?: PaletteSectionRenderer | null
|
|
51
63
|
themeController?: ThemeController | null
|
|
52
64
|
theme?: NovaThemeInput | null
|
|
53
65
|
onThemeChange?: ((state: NovaThemeState) => void) | null
|
|
@@ -56,10 +56,10 @@ export class PaletteRegistry {
|
|
|
56
56
|
export const defaultBpmnPaletteProvider = {
|
|
57
57
|
id: 'bpmn-default-palette',
|
|
58
58
|
priority: 100,
|
|
59
|
-
getSections() {
|
|
59
|
+
getSections(context = {}) {
|
|
60
60
|
return PALETTE_GROUPS.map((group) => {
|
|
61
61
|
const items = [];
|
|
62
|
-
for (const nodeType of group.types) {
|
|
62
|
+
for (const nodeType of group.types.filter((type) => context.studio?.allowsNodeType?.(type) !== false)) {
|
|
63
63
|
if (nodeType === 'parallelGateway') {
|
|
64
64
|
items.push({
|
|
65
65
|
id: 'node-parallelGateway',
|
|
@@ -85,7 +85,7 @@ export const defaultBpmnPaletteProvider = {
|
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
return { id: group.id, label: group.label, collapsed: group.id !== 'favorites', items };
|
|
88
|
-
});
|
|
88
|
+
}).filter((section) => section.items.length);
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
91
|
|
|
@@ -17,7 +17,9 @@ function nodeTypeOptions(context) {
|
|
|
17
17
|
? ['task', 'container']
|
|
18
18
|
: [currentKind];
|
|
19
19
|
return Object.entries(NODE_DEFINITIONS)
|
|
20
|
-
.filter(([type, def]) => type !== 'generic'
|
|
20
|
+
.filter(([type, def]) => type !== 'generic'
|
|
21
|
+
&& compatible.includes(def.kind)
|
|
22
|
+
&& context.studio?.allowsNodeType?.(type) !== false)
|
|
21
23
|
.map(([value, def]) => ({ value, label: def.label }));
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -3,6 +3,7 @@ import type { IconRegistry, NodeVisualResolution } from '../icons/index.js'
|
|
|
3
3
|
import type { NodeRuntimePresentation, ProcessInstanceSnapshot, RuntimePresentation } from '../runtime/index.js'
|
|
4
4
|
import type { NovaThemeInput, NovaThemeMode, NovaThemeState, RuntimeAppearanceResolver, ThemeController } from '../theme/index.js'
|
|
5
5
|
import type { SvgExportArtifact, SvgExportOptions, SvgExportPreviewController, SvgNodeRenderer } from '../export-svg/index.js'
|
|
6
|
+
import type { NodeSubtitleResolver } from '../node-presentation/index.js'
|
|
6
7
|
|
|
7
8
|
export interface SceneBounds {
|
|
8
9
|
left: number
|
|
@@ -73,6 +74,7 @@ export interface DiagramRendererOptions {
|
|
|
73
74
|
iconRegistry?: IconRegistry
|
|
74
75
|
nodeRenderers?: Record<string, DiagramNodeRenderer>
|
|
75
76
|
nodeRenderer?: DiagramNodeRenderer
|
|
77
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
76
78
|
runtimePresenter?: (context: { model: ProcessModel; runtime: ProcessInstanceSnapshot | null; appearance: RuntimeAppearanceResolver | null }) => RuntimePresentation
|
|
77
79
|
runtimeAppearance?: RuntimeAppearanceResolver | null
|
|
78
80
|
svgExport?: SvgExportOptions & { label?: string; nodeRenderers?: Record<string, SvgNodeRenderer> }
|
|
@@ -84,6 +86,7 @@ export interface DiagramRendererOptions {
|
|
|
84
86
|
onEdgeClick?: (edge: BpmnEdge, event: Event) => void
|
|
85
87
|
onCanvasClick?: (event: Event) => void
|
|
86
88
|
onViewportChange?: (state: { zoom: number; pan: Point }) => void
|
|
89
|
+
canCreateNodeType?: (nodeType: string) => boolean
|
|
87
90
|
[key: string]: unknown
|
|
88
91
|
}
|
|
89
92
|
export class DiagramRenderer {
|
|
@@ -97,6 +100,7 @@ export class DiagramRenderer {
|
|
|
97
100
|
setModel(model: ProcessModel): void
|
|
98
101
|
setMode(mode: 'design' | 'viewer' | 'instance'): void
|
|
99
102
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
103
|
+
refreshPresentation(): void
|
|
100
104
|
setSelection(selection: ElementSelection | null): void
|
|
101
105
|
setConnectingSource(nodeId: string | null): void
|
|
102
106
|
setSpacePressed(active: boolean): void
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NODE_DEFINITIONS, PALETTE_GROUPS, PARALLEL_GATEWAY_PRESETS, edgeWaypoints, resolveGatewayRole, resolveSwimlaneLabelPlacement, roundedPath, routeRuntimeTransition, smoothPath } from '../core/index.js';
|
|
2
2
|
import { createRuntimePresentation } from '../runtime/index.js';
|
|
3
3
|
import { createDefaultIconRegistry, createIconElement, resolveNodeVisual } from '../icons/index.js';
|
|
4
|
+
import { resolveNodeSubtitle, supportsNodeSubtitle } from '../node-presentation/index.js';
|
|
4
5
|
import { ThemeController, applyRuntimeTone } from '../theme/index.js';
|
|
5
6
|
import { exportDiagramSvg, openSvgExportPreview } from '../export-svg/index.js';
|
|
6
7
|
|
|
@@ -16,6 +17,15 @@ function quantizeZoom(value, step = 0.05) {
|
|
|
16
17
|
return Math.round(Math.round(clamped / step) * step * 1000) / 1000;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function eventShapeDiameter(node, definition) {
|
|
21
|
+
const maximum = definition.kind === 'boundary' ? 40 : 46;
|
|
22
|
+
const width = Number(node?.width);
|
|
23
|
+
const height = Number(node?.height);
|
|
24
|
+
const availableWidth = Number.isFinite(width) ? Math.max(0, width) : maximum;
|
|
25
|
+
const availableHeight = Number.isFinite(height) ? Math.max(0, height) : maximum;
|
|
26
|
+
return Math.min(maximum, availableWidth, availableHeight);
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
function estimateLabelTextWidth(value) {
|
|
20
30
|
let width = 0;
|
|
21
31
|
for (const ch of Array.from(String(value ?? ''))) {
|
|
@@ -262,18 +272,8 @@ function routePathData(points, routeStyle = 'rounded', cornerRadius = 14) {
|
|
|
262
272
|
return roundedPath(points, cornerRadius);
|
|
263
273
|
}
|
|
264
274
|
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
if (node.type === 'userTask') return p.assignee || p.candidateGroups || p.candidateUsers || '待配置审批人';
|
|
268
|
-
if (['serviceTask', 'sendTask', 'businessRuleTask'].includes(node.type)) return p.implementation || '待配置执行实现';
|
|
269
|
-
if (node.type === 'scriptTask') return p.scriptFormat || 'Script';
|
|
270
|
-
if (node.type === 'callActivity') return p.calledElement || '待配置调用流程';
|
|
271
|
-
if (node.type === 'receiveTask') return '等待消息或外部触发';
|
|
272
|
-
if (node.type === 'manualTask') return '人工线下处理';
|
|
273
|
-
if (node.type === 'subProcess') return '可折叠子流程';
|
|
274
|
-
if (node.type === 'eventSubProcess') return '事件触发子流程';
|
|
275
|
-
if (node.type === 'transaction') return '事务边界';
|
|
276
|
-
return node.bpmnType?.replace('bpmn:', '') || '';
|
|
275
|
+
function nodeAccessibleLabel(title, subtitle) {
|
|
276
|
+
return subtitle === null || subtitle === '' ? title : `${title}\n${subtitle}`;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
function runtimeStatusIconId(presentation) {
|
|
@@ -599,6 +599,23 @@ export class DiagramRenderer {
|
|
|
599
599
|
setModel(model) { this.model = model; this.render(); }
|
|
600
600
|
setMode(mode) { this.mode = mode; this.render(); }
|
|
601
601
|
setRuntime(runtime) { this.runtime = runtime; this.render(); }
|
|
602
|
+
refreshPresentation() {
|
|
603
|
+
if (this.mode === 'instance') return;
|
|
604
|
+
const subtitles = this._resolveDefinitionSubtitles();
|
|
605
|
+
const nodeElements = new Map(
|
|
606
|
+
[...this.nodeLayer.querySelectorAll('[data-node-id]')]
|
|
607
|
+
.map((nodeEl) => [nodeEl.dataset.nodeId, nodeEl]),
|
|
608
|
+
);
|
|
609
|
+
const updates = [];
|
|
610
|
+
for (const node of this.model.nodes) {
|
|
611
|
+
if (!subtitles.has(node.id)) continue;
|
|
612
|
+
const nodeEl = nodeElements.get(node.id);
|
|
613
|
+
if (!nodeEl || nodeEl.dataset.nodePresentation !== 'standard') continue;
|
|
614
|
+
const definition = NODE_DEFINITIONS[node.type] || NODE_DEFINITIONS.generic;
|
|
615
|
+
updates.push({ nodeEl, node, definition, subtitle: subtitles.get(node.id) });
|
|
616
|
+
}
|
|
617
|
+
updates.forEach((update) => this._applyDefinitionPresentation(update));
|
|
618
|
+
}
|
|
602
619
|
setSelection(selection) {
|
|
603
620
|
this.selection = selection;
|
|
604
621
|
if (selection?.kind !== 'node' || selection.id !== this.quickMenuNodeId) this.quickMenuNodeId = null;
|
|
@@ -645,6 +662,7 @@ export class DiagramRenderer {
|
|
|
645
662
|
nodeRenderers: options.nodeRenderers || this.svgExportOptions.nodeRenderers,
|
|
646
663
|
htmlNodeRenderers: this.options.nodeRenderers,
|
|
647
664
|
htmlNodeRenderer: this.options.nodeRenderer,
|
|
665
|
+
nodeSubtitleResolver: this.options.nodeSubtitleResolver,
|
|
648
666
|
mode: this.mode,
|
|
649
667
|
label: options.label || this.svgExportOptions.label,
|
|
650
668
|
}, { ...this.svgExportOptions, ...options });
|
|
@@ -748,16 +766,38 @@ export class DiagramRenderer {
|
|
|
748
766
|
this.runtimePresentation = this.runtime
|
|
749
767
|
? presenter({ model: this.model, runtime: this.runtime, appearance: this.options.runtimeAppearance })
|
|
750
768
|
: createRuntimePresentation({ model: this.model, runtime: null, appearance: this.options.runtimeAppearance });
|
|
769
|
+
const subtitles = this._resolveDefinitionSubtitles();
|
|
751
770
|
this._syncSceneBounds();
|
|
752
771
|
this._renderEdges();
|
|
753
772
|
this._renderRuntimeTransitions();
|
|
754
773
|
this._renderGuides();
|
|
755
|
-
this._renderNodes();
|
|
774
|
+
this._renderNodes(subtitles);
|
|
756
775
|
this._applyTransform();
|
|
757
776
|
this.container.dataset.mode = this.mode;
|
|
758
777
|
if (this.emptyState) this.emptyState.hidden = this.model.nodes.length > 0 || !this.options.showEmptyState;
|
|
759
778
|
}
|
|
760
779
|
|
|
780
|
+
_resolveDefinitionSubtitles() {
|
|
781
|
+
const subtitles = new Map();
|
|
782
|
+
if (this.mode === 'instance') return subtitles;
|
|
783
|
+
const mode = this.mode === 'viewer' ? 'viewer' : 'design';
|
|
784
|
+
for (const node of this.model.nodes) {
|
|
785
|
+
const definition = NODE_DEFINITIONS[node.type] || NODE_DEFINITIONS.generic;
|
|
786
|
+
if (!supportsNodeSubtitle(definition)) continue;
|
|
787
|
+
const renderers = this.options.nodeRenderers || {};
|
|
788
|
+
if (typeof (renderers[node.type] || renderers[definition.kind] || this.options.nodeRenderer) === 'function') continue;
|
|
789
|
+
subtitles.set(node.id, resolveNodeSubtitle({
|
|
790
|
+
node,
|
|
791
|
+
definition,
|
|
792
|
+
model: this.model,
|
|
793
|
+
mode,
|
|
794
|
+
surface: 'canvas',
|
|
795
|
+
resolver: this.options.nodeSubtitleResolver,
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
return subtitles;
|
|
799
|
+
}
|
|
800
|
+
|
|
761
801
|
_renderGuides() {
|
|
762
802
|
this.guideSvg.replaceChildren();
|
|
763
803
|
if (this.model.settings?.alignmentGuides === false) return;
|
|
@@ -977,7 +1017,7 @@ export class DiagramRenderer {
|
|
|
977
1017
|
requestAnimationFrame(() => { input.focus(); input.select(); });
|
|
978
1018
|
}
|
|
979
1019
|
|
|
980
|
-
_renderNodes() {
|
|
1020
|
+
_renderNodes(subtitles = new Map()) {
|
|
981
1021
|
this._nodeContentCleanups.splice(0).forEach((cleanup) => cleanup?.());
|
|
982
1022
|
this.nodeLayer.innerHTML = '';
|
|
983
1023
|
const zOrder = { participant: 0, lane: 1, group: 2, container: 3, data: 4, dataStore: 4, annotation: 4, task: 5, gateway: 6, event: 7, boundary: 8 };
|
|
@@ -1018,7 +1058,7 @@ export class DiagramRenderer {
|
|
|
1018
1058
|
nodeEl.tabIndex = 0;
|
|
1019
1059
|
|
|
1020
1060
|
const visual = this._resolveNodeVisual(node);
|
|
1021
|
-
this._renderNodeShape(nodeEl, node, def, runtimeState, visual);
|
|
1061
|
+
this._renderNodeShape(nodeEl, node, def, runtimeState, visual, subtitles.get(node.id));
|
|
1022
1062
|
if (['task', 'container'].includes(def.kind)) addActivityMarkers(nodeEl, visual, this.iconRegistry);
|
|
1023
1063
|
|
|
1024
1064
|
if (this.mode === 'design') this._renderDesignControls(nodeEl, node, def, selected && this.selection?.kind !== 'multi' && this.interactionMode !== 'pan');
|
|
@@ -1048,9 +1088,10 @@ export class DiagramRenderer {
|
|
|
1048
1088
|
if (this.mode === 'design' && this.quickMenuNodeId) this._renderQuickMenu();
|
|
1049
1089
|
}
|
|
1050
1090
|
|
|
1051
|
-
_renderNodeShape(nodeEl, node, def, runtimeState, visual) {
|
|
1091
|
+
_renderNodeShape(nodeEl, node, def, runtimeState, visual, subtitle) {
|
|
1052
1092
|
if (def.kind === 'event' || def.kind === 'boundary') {
|
|
1053
1093
|
const shape = el('div', 'mb-event-shape');
|
|
1094
|
+
shape.style.setProperty('--mb-event-shape-size', `${eventShapeDiameter(node, def)}px`);
|
|
1054
1095
|
shape.dataset.stage = def.eventStage || 'intermediate';
|
|
1055
1096
|
shape.dataset.role = def.eventRole || '';
|
|
1056
1097
|
if (def.kind === 'boundary' && node.properties?.cancelActivity === false) shape.classList.add('is-noninterrupting');
|
|
@@ -1136,6 +1177,7 @@ export class DiagramRenderer {
|
|
|
1136
1177
|
nodeEl.appendChild(customHost);
|
|
1137
1178
|
return;
|
|
1138
1179
|
}
|
|
1180
|
+
nodeEl.dataset.nodePresentation = 'standard';
|
|
1139
1181
|
const header = el('div', 'mb-node-header');
|
|
1140
1182
|
const icon = visual.iconId ? el('span', 'mb-node-icon') : null;
|
|
1141
1183
|
const iconNode = visual.iconId
|
|
@@ -1185,14 +1227,33 @@ export class DiagramRenderer {
|
|
|
1185
1227
|
titleWrap.appendChild(action);
|
|
1186
1228
|
}
|
|
1187
1229
|
} else {
|
|
1188
|
-
|
|
1189
|
-
titleWrap.appendChild(el('div', 'mb-node-
|
|
1230
|
+
const title = node.name || def.label;
|
|
1231
|
+
titleWrap.appendChild(el('div', 'mb-node-title', title));
|
|
1232
|
+
if (subtitle !== null) titleWrap.appendChild(el('div', 'mb-node-subtitle', subtitle));
|
|
1233
|
+
const label = nodeAccessibleLabel(title, subtitle);
|
|
1234
|
+
nodeEl.title = label;
|
|
1235
|
+
nodeEl.setAttribute('aria-label', label);
|
|
1190
1236
|
}
|
|
1191
1237
|
if (icon) header.append(icon);
|
|
1192
1238
|
header.append(titleWrap);
|
|
1193
1239
|
nodeEl.appendChild(header);
|
|
1194
1240
|
}
|
|
1195
1241
|
|
|
1242
|
+
_applyDefinitionPresentation({ nodeEl, node, definition, subtitle }) {
|
|
1243
|
+
const title = node.name || definition.label;
|
|
1244
|
+
const titleWrap = nodeEl.querySelector('.mb-node-title-wrap');
|
|
1245
|
+
const titleEl = titleWrap?.querySelector('.mb-node-title');
|
|
1246
|
+
if (!titleWrap || !titleEl) return;
|
|
1247
|
+
titleEl.textContent = title;
|
|
1248
|
+
const currentSubtitle = titleWrap.querySelector('.mb-node-subtitle');
|
|
1249
|
+
if (subtitle === null) currentSubtitle?.remove();
|
|
1250
|
+
else if (currentSubtitle) currentSubtitle.textContent = subtitle;
|
|
1251
|
+
else titleWrap.appendChild(el('div', 'mb-node-subtitle', subtitle));
|
|
1252
|
+
const label = nodeAccessibleLabel(title, subtitle);
|
|
1253
|
+
nodeEl.title = label;
|
|
1254
|
+
nodeEl.setAttribute('aria-label', label);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1196
1257
|
_resolveNodeVisual(node, surface = this.mode === 'instance' ? 'viewer' : 'canvas') {
|
|
1197
1258
|
const visualModel = this.options.getVisualModel?.() || this.options.visualModel || this.model;
|
|
1198
1259
|
return resolveNodeVisual(node, { surface, model: visualModel });
|
|
@@ -1424,7 +1485,7 @@ export class DiagramRenderer {
|
|
|
1424
1485
|
const type = quickItem.nodeType;
|
|
1425
1486
|
if (type === 'startEvent') continue;
|
|
1426
1487
|
const def = NODE_DEFINITIONS[type];
|
|
1427
|
-
if (!def) continue;
|
|
1488
|
+
if (!def || this.options.canCreateNodeType?.(type) === false) continue;
|
|
1428
1489
|
const item = el('button', 'mb-quick-menu-item');
|
|
1429
1490
|
item.dataset.search = `${quickItem.id} ${type} ${quickItem.label}`.toLowerCase();
|
|
1430
1491
|
const miniIcon = el('span', `mb-mini-type mb-mini-kind-${def.kind}`);
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
RuntimeAppearanceResolver,
|
|
21
21
|
} from '../theme/index.js'
|
|
22
22
|
import type { RuntimeTimelineSvgRenderer, SvgExportArtifact, SvgExportOptions, SvgExportPreviewController, SvgNodeRenderer } from '../export-svg/index.js'
|
|
23
|
+
import type { NodeSubtitleResolver } from '../node-presentation/index.js'
|
|
23
24
|
|
|
24
25
|
export type ViewerProjection = 'auto' | 'standard' | 'approval' | 'compact'
|
|
25
26
|
export interface RuntimeTraceItem {
|
|
@@ -200,6 +201,7 @@ export interface ViewerOptions {
|
|
|
200
201
|
iconRegistry?: IconRegistry
|
|
201
202
|
nodeRenderers?: Record<string, Function>
|
|
202
203
|
nodeRenderer?: Function
|
|
204
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
203
205
|
projection?: ViewerProjection
|
|
204
206
|
responsive?: boolean
|
|
205
207
|
runtimeTraceOptions?: RuntimeTraceProjectionOptions
|
|
@@ -245,6 +247,7 @@ export class BpmnViewer {
|
|
|
245
247
|
traceProjection: RuntimeTraceProjection | null
|
|
246
248
|
setModel(model: ProcessModel): void
|
|
247
249
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
250
|
+
refreshPresentation(): void
|
|
248
251
|
setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: RuntimeAssetResolver | null }): void
|
|
249
252
|
setProjection(projection: ViewerProjection): void
|
|
250
253
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
@@ -367,7 +367,7 @@ export class BpmnViewer {
|
|
|
367
367
|
this.responsive = options.responsive === true;
|
|
368
368
|
this.projection = options.projection || (this.runtime && this.responsive ? 'auto' : this.runtime ? 'approval' : 'standard');
|
|
369
369
|
this.activeProjection = 'standard';
|
|
370
|
-
this.mode = this.runtime ? 'instance' : 'viewer';
|
|
370
|
+
this.mode = options.mode === 'instance' || this.runtime ? 'instance' : 'viewer';
|
|
371
371
|
this.onElementClick = options.onElementClick || (() => {});
|
|
372
372
|
this.onTraceClick = options.onTraceClick || (() => {});
|
|
373
373
|
this.runtimePresenter = options.runtimePresenter || ((context) => createRuntimePresentation(context));
|
|
@@ -414,8 +414,15 @@ export class BpmnViewer {
|
|
|
414
414
|
};
|
|
415
415
|
document.addEventListener('pointerdown', this._outsideHandler, true);
|
|
416
416
|
document.addEventListener('keydown', this._keyHandler);
|
|
417
|
-
|
|
418
|
-
|
|
417
|
+
try {
|
|
418
|
+
this._rebuildProjection();
|
|
419
|
+
this._mountRenderer();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
document.removeEventListener('pointerdown', this._outsideHandler, true);
|
|
422
|
+
document.removeEventListener('keydown', this._keyHandler);
|
|
423
|
+
if (this._ownsThemeController) this.themeController.destroy();
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
419
426
|
this._resizeObserver = typeof ResizeObserver === 'function'
|
|
420
427
|
? new ResizeObserver(() => {
|
|
421
428
|
if (this.projection !== 'auto') return;
|
|
@@ -576,6 +583,7 @@ export class BpmnViewer {
|
|
|
576
583
|
iconRegistry: this.options.iconRegistry,
|
|
577
584
|
nodeRenderers: this.options.nodeRenderers,
|
|
578
585
|
nodeRenderer: this.options.nodeRenderer,
|
|
586
|
+
nodeSubtitleResolver: this.options.nodeSubtitleResolver,
|
|
579
587
|
svgExport: this.svgExportOptions,
|
|
580
588
|
runtimeAppearance: this.runtimeAppearance,
|
|
581
589
|
onNodeClick: (node, event) => this._select('node', node, event),
|
|
@@ -918,7 +926,12 @@ export class BpmnViewer {
|
|
|
918
926
|
}
|
|
919
927
|
|
|
920
928
|
setModel(model) { this.model = model; this._svgExportAssetCache.clear(); this.refresh(); }
|
|
921
|
-
setRuntime(runtime) {
|
|
929
|
+
setRuntime(runtime) {
|
|
930
|
+
this.runtime = runtime ? normalizeRuntime(runtime) : null;
|
|
931
|
+
this.mode = this.options.mode === 'instance' || this.runtime ? 'instance' : 'viewer';
|
|
932
|
+
this._svgExportAssetCache.clear();
|
|
933
|
+
this.refresh();
|
|
934
|
+
}
|
|
922
935
|
setProjection(projection) {
|
|
923
936
|
if (!['auto', 'standard', 'approval', 'compact'].includes(projection)) return;
|
|
924
937
|
this.projection = projection;
|
|
@@ -954,6 +967,10 @@ export class BpmnViewer {
|
|
|
954
967
|
this.runtimeAppearance = createRuntimeAppearance(this.runtimeAppearanceOptions);
|
|
955
968
|
this.refresh();
|
|
956
969
|
}
|
|
970
|
+
refreshPresentation() {
|
|
971
|
+
if (this.mode !== 'viewer' || (this.runtime && this.activeProjection === 'compact')) return;
|
|
972
|
+
this.renderer?.refreshPresentation?.();
|
|
973
|
+
}
|
|
957
974
|
exportSvg(options = {}) {
|
|
958
975
|
const isTimeline = Boolean(this.runtime && this.activeProjection === 'compact' && this.traceProjection);
|
|
959
976
|
const label = isTimeline ? '移动时间线' : this.activeProjection === 'approval' ? '实际路径' : this.runtime ? '完整 BPMN' : '流程展示';
|