@bpmn-nova/studio 0.3.2-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 +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 +298 -58
- package/llms.txt +19 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计
|
|
|
4
4
|
|
|
5
5
|
> **English summary:** The complete framework-neutral BPMN Nova package for process design, viewing, approval traces, themes, properties, and pure SVG export.
|
|
6
6
|
|
|
7
|
-
> 当前版本为 `0.3.
|
|
7
|
+
> 当前版本为 `0.3.3-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -85,6 +85,10 @@ const shell = createStudioShell({
|
|
|
85
85
|
// 在 Nova Header 左侧挂载宿主的返回入口、业务图标、流程名称和类型。
|
|
86
86
|
// 完整替换 Header 时改用 slots.header,并继续调用 actions。
|
|
87
87
|
},
|
|
88
|
+
headerActions({ container, actions, getMode, subscribeMode }) {
|
|
89
|
+
// 替换默认“校验 / 导入 / 导出”,在这里挂载宿主的“校验 / 保存 / 发布”。
|
|
90
|
+
// 校验按钮调用 actions.validate();保存和发布继续调用宿主服务。
|
|
91
|
+
},
|
|
88
92
|
},
|
|
89
93
|
theme: 'auto',
|
|
90
94
|
})
|
|
@@ -99,8 +103,67 @@ const xml = shell.actions.exportXml('flowable')
|
|
|
99
103
|
|
|
100
104
|
`regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
|
|
101
105
|
|
|
106
|
+
`headerActions` 只替换 Header 最右侧动作组,`headerStart` 可与其同时使用,完整 `header` 的优先级更高。未提供 `headerActions` 时仍显示默认“校验 / 导入 / 导出”。顶部不再重复显示最佳视图;底部缩放区和 `fitView()` Interface 保持不变。
|
|
107
|
+
|
|
108
|
+
`shell.actions.validate()` 作为 Header 动作,以 `toolbar` 来源先更新 Nova 默认状态栏、再发送 Validation Event,最后返回 issues;程序化调用 `shell.validate()` 使用 `api` 来源。`valid` 表示没有 error,warning 不默认阻止发布:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const offValidation = shell.subscribeValidation((event) => {
|
|
112
|
+
console.log(event.source, event.valid, event.errorCount, event.warningCount, event.issues)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
shell.validate()
|
|
116
|
+
offValidation()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
宿主发布按钮可以复用 Toolbar 来源的完整流程;仅 error 默认阻止发布:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
async function publishProcess(actions) {
|
|
123
|
+
const issues = actions.validate()
|
|
124
|
+
if (issues.some((issue) => issue.level === 'error')) return
|
|
125
|
+
await publishXml(actions.exportXml())
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
102
129
|
`shell.actions` 只封装撤销、重做、布局、视图、校验和 BPMN/SVG 导入导出。宿主的保存草稿、发布、权限、文件选择和服务端事务不属于 Nova Actions。
|
|
103
130
|
|
|
131
|
+
## Mode 与节点副标题投影
|
|
132
|
+
|
|
133
|
+
`design`、`viewer`、`instance` 分别表示流程设计、流程展示和审批轨迹。Shell 是唯一 Mode 状态源:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const unsubscribe = shell.subscribeMode((event) => {
|
|
137
|
+
console.log(event.mode, event.previousMode, event.source, event.allowedModes)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
shell.getMode()
|
|
141
|
+
shell.setMode('viewer')
|
|
142
|
+
shell.setAllowedModes(['design', 'viewer'])
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`setMode()` 仅在实际成功切换时返回 `true`。`setAllowedModes()` 拒绝空数组、重复值和未知值;若移除当前 Mode,会回退到新数组首项并发送一次 `allowed-modes` 事件。切换只改变 Shell 展示,不进入 Model、History 或 Studio State。
|
|
146
|
+
|
|
147
|
+
标准任务与容器卡片支持定义态副标题 Resolver:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const summaries = new Map()
|
|
151
|
+
const shell = createStudioShell({
|
|
152
|
+
container,
|
|
153
|
+
studio,
|
|
154
|
+
nodeSubtitleResolver({ node, mode, surface, defaultSubtitle }) {
|
|
155
|
+
return summaries.has(node.id) ? summaries.get(node.id) : undefined
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
summaries.set('ServiceTask_Archive', '归档到采购系统')
|
|
160
|
+
shell.refreshPresentation()
|
|
161
|
+
summaries.set('ServiceTask_Archive', null)
|
|
162
|
+
shell.refreshPresentation()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`undefined` 保留默认副标题,`null` 移除副标题行,字符串(包括空字符串)作为覆盖值。Resolver 同步应用于 Design、Viewer 与标准 SVG 降级视觉;完整自定义 Renderer 优先,Instance 始终使用 Runtime Presentation。`refreshPresentation()` 不写 XML、不创建历史、不改变选择、Scope 或视口。
|
|
166
|
+
|
|
104
167
|
## 只读 Viewer
|
|
105
168
|
|
|
106
169
|
```js
|
package/dist/canvas.js
CHANGED
|
@@ -35,7 +35,7 @@ function toolbarIcon(id) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class BpmnCanvas {
|
|
38
|
-
constructor({ container, studio, rendererOptions = {}, interactions = null, selectionToolbar = null, contextMenu = null, contextMenuRegistry = createDefaultContextMenuRegistry(), pointerMode = 'select', themeController = null, theme = null, onThemeChange = null } = {}) {
|
|
38
|
+
constructor({ container, studio, rendererOptions = {}, nodeSubtitleResolver = rendererOptions.nodeSubtitleResolver, interactions = null, selectionToolbar = null, contextMenu = null, contextMenuRegistry = createDefaultContextMenuRegistry(), pointerMode = 'select', themeController = null, theme = null, onThemeChange = null } = {}) {
|
|
39
39
|
if (!container) throw new Error('BpmnCanvas requires a container.');
|
|
40
40
|
if (!studio) throw new Error('BpmnCanvas requires a studio controller.');
|
|
41
41
|
this.container = container;
|
|
@@ -51,6 +51,7 @@ export class BpmnCanvas {
|
|
|
51
51
|
const onViewportChange = rendererOptions.onViewportChange;
|
|
52
52
|
this.renderer = new DiagramRenderer(container, {
|
|
53
53
|
...rendererOptions,
|
|
54
|
+
nodeSubtitleResolver,
|
|
54
55
|
themeController,
|
|
55
56
|
theme,
|
|
56
57
|
onThemeChange,
|
|
@@ -905,6 +906,7 @@ export class BpmnCanvas {
|
|
|
905
906
|
startConnect(nodeId) { this.renderer.closeTransientOverlays?.(); return this.studio.startConnect(nodeId); }
|
|
906
907
|
editEdgeName(edgeId) { return this.renderer.editEdgeName?.(edgeId) || false; }
|
|
907
908
|
fitView(padding, options) { this.renderer.fitView(padding, { minZoom: 0.7, ...(options || {}) }); }
|
|
909
|
+
refreshPresentation() { this.renderer.refreshPresentation(); }
|
|
908
910
|
exportSvg(options) { return this.renderer.exportSvg(options); }
|
|
909
911
|
openSvgExportPreview(options) { return this.renderer.openSvgExportPreview(options); }
|
|
910
912
|
fitSelection(padding = 72) {
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
import type { IconRegistry } from './modules/icons/index.js'
|
|
27
27
|
import type { PaletteRegistry } from './modules/palette/index.js'
|
|
28
28
|
import type { PropertiesRegistry } from './modules/properties/index.js'
|
|
29
|
+
import type { NodeSubtitleResolver } from './modules/node-presentation/index.js'
|
|
29
30
|
import type { DiagramNodeRenderer, DiagramRendererOptions } from './modules/renderer-svg/index.js'
|
|
30
31
|
import type { RuntimeTimelineSvgRenderer, SvgExportArtifact, SvgExportOptions, SvgExportPreviewController, SvgNodeRenderer } from './modules/export-svg/index.js'
|
|
31
32
|
import type {
|
|
@@ -38,6 +39,8 @@ import type {
|
|
|
38
39
|
RuntimeTransitionDetailsRenderer,
|
|
39
40
|
} from './modules/viewer/index.js'
|
|
40
41
|
|
|
42
|
+
export type { NodeSubtitleResolver, NodeSubtitleResolverContext } from './modules/node-presentation/index.js'
|
|
43
|
+
|
|
41
44
|
// Aggregate compatibility surface. Studio re-exports the package-level types,
|
|
42
45
|
// and keeps these frequently consumed declarations visible at its own entry.
|
|
43
46
|
export interface ScopedElement { scopeId?: string }
|
|
@@ -141,6 +144,7 @@ export interface ViewerOptions {
|
|
|
141
144
|
iconRegistry?: IconRegistry
|
|
142
145
|
nodeRenderers?: Record<string, Function>
|
|
143
146
|
nodeRenderer?: Function
|
|
147
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
144
148
|
projection?: ViewerProjection
|
|
145
149
|
responsive?: boolean
|
|
146
150
|
timeline?: ViewerTimelineOptions
|
|
@@ -173,6 +177,7 @@ export class BpmnViewer {
|
|
|
173
177
|
traceProjection: RuntimeTraceProjection | null
|
|
174
178
|
setModel(model: ProcessModel): void
|
|
175
179
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
180
|
+
refreshPresentation(): void
|
|
176
181
|
setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: import('./modules/viewer/index.js').RuntimeAssetResolver | null }): void
|
|
177
182
|
setProjection(projection: ViewerProjection): void
|
|
178
183
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
@@ -414,6 +419,7 @@ export interface BpmnCanvasOptions {
|
|
|
414
419
|
container: HTMLElement
|
|
415
420
|
studio: BpmnStudioController
|
|
416
421
|
rendererOptions?: DiagramRendererOptions
|
|
422
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
417
423
|
interactions?: InteractionController | null
|
|
418
424
|
selectionToolbar?: HTMLElement | SelectionToolbarSlot | null
|
|
419
425
|
contextMenu?: HTMLElement | ContextMenuSlot | null
|
|
@@ -427,6 +433,7 @@ export class BpmnCanvas {
|
|
|
427
433
|
clientToWorld(clientX: number, clientY: number): Point
|
|
428
434
|
getViewportCenterWorld(): Point
|
|
429
435
|
fitView(padding?: number, options?: Record<string, unknown>): void
|
|
436
|
+
refreshPresentation(): void
|
|
430
437
|
fitSelection(padding?: number): boolean
|
|
431
438
|
setPointerMode(mode: 'select' | 'marquee' | 'pan'): boolean
|
|
432
439
|
getPointerMode(): 'select' | 'marquee' | 'pan'
|
|
@@ -458,6 +465,22 @@ export interface StudioShellActions {
|
|
|
458
465
|
onDownload?: (artifact: SvgExportArtifact) => void
|
|
459
466
|
}): SvgExportPreviewController | null
|
|
460
467
|
}
|
|
468
|
+
export type StudioMode = 'design' | 'viewer' | 'instance'
|
|
469
|
+
export type StudioModeChangeSource = 'toolbar' | 'api' | 'allowed-modes'
|
|
470
|
+
export interface StudioModeChangeEvent {
|
|
471
|
+
readonly mode: StudioMode
|
|
472
|
+
readonly previousMode: StudioMode
|
|
473
|
+
readonly source: StudioModeChangeSource
|
|
474
|
+
readonly allowedModes: readonly StudioMode[]
|
|
475
|
+
}
|
|
476
|
+
export type StudioValidationSource = 'toolbar' | 'api'
|
|
477
|
+
export interface StudioValidationEvent {
|
|
478
|
+
readonly source: StudioValidationSource
|
|
479
|
+
readonly valid: boolean
|
|
480
|
+
readonly errorCount: number
|
|
481
|
+
readonly warningCount: number
|
|
482
|
+
readonly issues: readonly StudioValidationIssue[]
|
|
483
|
+
}
|
|
461
484
|
export interface StudioShellSlotContext {
|
|
462
485
|
studio: BpmnStudioController
|
|
463
486
|
shell: BpmnStudioShell
|
|
@@ -465,6 +488,10 @@ export interface StudioShellSlotContext {
|
|
|
465
488
|
actions: StudioShellActions
|
|
466
489
|
getState(): StudioState
|
|
467
490
|
subscribe(listener: (event: StudioEvent) => void): () => void
|
|
491
|
+
getMode(): StudioMode
|
|
492
|
+
getAllowedModes(): readonly StudioMode[]
|
|
493
|
+
subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
|
|
494
|
+
subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
|
|
468
495
|
}
|
|
469
496
|
export type StudioShellDomSlot =
|
|
470
497
|
| HTMLElement
|
|
@@ -473,6 +500,7 @@ export interface StudioShellSlots {
|
|
|
473
500
|
left?: StudioShellDomSlot
|
|
474
501
|
right?: StudioShellDomSlot
|
|
475
502
|
headerStart?: StudioShellDomSlot
|
|
503
|
+
headerActions?: StudioShellDomSlot
|
|
476
504
|
header?: StudioShellDomSlot
|
|
477
505
|
footer?: StudioShellDomSlot
|
|
478
506
|
contextMenu?: ContextMenuSlot
|
|
@@ -507,12 +535,13 @@ export interface StudioShellOptions {
|
|
|
507
535
|
runtimeTraceOptions?: RuntimeTraceProjectionOptions
|
|
508
536
|
timeline?: ViewerTimelineOptions
|
|
509
537
|
}
|
|
538
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
510
539
|
slots?: StudioShellSlots
|
|
511
540
|
regions?: StudioShellRegions
|
|
512
541
|
layout?: (context: Record<string, unknown>) => void | (() => void)
|
|
513
542
|
runtime?: ProcessInstanceSnapshot | null
|
|
514
|
-
mode?:
|
|
515
|
-
allowedModes?:
|
|
543
|
+
mode?: StudioMode
|
|
544
|
+
allowedModes?: readonly StudioMode[]
|
|
516
545
|
projection?: ViewerProjection
|
|
517
546
|
responsive?: boolean
|
|
518
547
|
projectionOptions?: Array<{ value: ViewerProjection; label: string }>
|
|
@@ -528,9 +557,14 @@ export class BpmnStudioShell {
|
|
|
528
557
|
readonly actions: StudioShellActions
|
|
529
558
|
canvas?: BpmnCanvas
|
|
530
559
|
viewer?: BpmnViewer
|
|
560
|
+
getMode(): StudioMode
|
|
561
|
+
getAllowedModes(): readonly StudioMode[]
|
|
562
|
+
subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
|
|
563
|
+
subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
|
|
564
|
+
setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
|
|
531
565
|
getRegions(): Readonly<StudioShellRegionState>
|
|
532
566
|
setRegions(regions: StudioShellRegions): Readonly<StudioShellRegionState>
|
|
533
|
-
setMode(mode:
|
|
567
|
+
setMode(mode: StudioMode): boolean
|
|
534
568
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
535
569
|
setProjection(projection: ViewerProjection): void
|
|
536
570
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
@@ -538,6 +572,8 @@ export class BpmnStudioShell {
|
|
|
538
572
|
getThemeState(): NovaThemeState
|
|
539
573
|
subscribeTheme(listener: (state: NovaThemeState) => void): () => void
|
|
540
574
|
setRuntimeAppearance(options: RuntimeAppearanceOptions | null): void
|
|
575
|
+
refreshPresentation(): void
|
|
576
|
+
validate(): StudioValidationIssue[]
|
|
541
577
|
fitView(): void
|
|
542
578
|
zoomBy(factor: number): void
|
|
543
579
|
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact>
|
|
@@ -2,6 +2,7 @@ import type { BpmnNode, ProcessModel } from '../core/index.js'
|
|
|
2
2
|
import type { IconRegistry, NodeVisualResolution } from '../icons/index.js'
|
|
3
3
|
import type { NodeRuntimePresentation, ProcessInstanceSnapshot, RuntimeApprovalActionPresentation, RuntimePresentation, RuntimeAssetRef } from '../runtime/index.js'
|
|
4
4
|
import type { NovaResolvedTheme, NovaThemeSnapshot, RuntimeAppearanceOptions, RuntimeAppearanceResolver, ThemeController } from '../theme/index.js'
|
|
5
|
+
import type { NodeSubtitleResolver } from '../node-presentation/index.js'
|
|
5
6
|
|
|
6
7
|
export type SvgExportTheme = 'current' | NovaResolvedTheme
|
|
7
8
|
export interface SvgExportOptions {
|
|
@@ -65,6 +66,7 @@ export interface DiagramSvgExportContext {
|
|
|
65
66
|
nodeRenderers?: Record<string, SvgNodeRenderer>
|
|
66
67
|
htmlNodeRenderers?: Record<string, Function>
|
|
67
68
|
htmlNodeRenderer?: Function
|
|
69
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
68
70
|
mode?: 'design' | 'viewer' | 'instance'
|
|
69
71
|
label?: string
|
|
70
72
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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> }
|
|
@@ -98,6 +100,7 @@ export class DiagramRenderer {
|
|
|
98
100
|
setModel(model: ProcessModel): void
|
|
99
101
|
setMode(mode: 'design' | 'viewer' | 'instance'): void
|
|
100
102
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
103
|
+
refreshPresentation(): void
|
|
101
104
|
setSelection(selection: ElementSelection | null): void
|
|
102
105
|
setConnectingSource(nodeId: string | null): void
|
|
103
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 });
|