@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -20
- package/dist/canvas.js +7 -4
- package/dist/context-menu.js +2 -2
- package/dist/controller.js +84 -6
- package/dist/index.d.ts +138 -38
- package/dist/index.js +12 -11
- package/dist/interactions.js +1 -1
- package/dist/modules/bpmn-model/index.d.ts +11 -0
- package/dist/modules/bpmn-model/index.js +703 -0
- package/dist/modules/core/containment.js +590 -0
- package/dist/modules/core/gateway.js +72 -0
- package/dist/modules/core/history.js +32 -0
- package/dist/modules/core/index.d.ts +268 -0
- package/dist/modules/core/index.js +7 -0
- package/dist/modules/core/layout.js +644 -0
- package/dist/modules/core/model.js +287 -0
- package/dist/modules/core/runtime-transition-route.js +360 -0
- package/dist/modules/core/scope.js +99 -0
- package/dist/modules/designer/index.d.ts +79 -0
- package/dist/modules/designer/index.js +607 -0
- package/dist/modules/engine-activiti/index.d.ts +19 -0
- package/dist/modules/engine-activiti/index.js +160 -0
- package/dist/modules/engine-flowable/index.d.ts +19 -0
- package/dist/modules/engine-flowable/index.js +160 -0
- package/dist/modules/export-svg/index.d.ts +112 -0
- package/dist/modules/export-svg/index.js +2 -0
- package/dist/modules/export-svg/preview.js +327 -0
- package/dist/modules/export-svg/render.js +718 -0
- package/dist/modules/icons/index.d.ts +24 -0
- package/dist/modules/icons/index.js +264 -0
- package/dist/modules/palette/index.d.ts +74 -0
- package/dist/modules/palette/index.js +99 -0
- package/dist/modules/palette/panel.js +99 -0
- package/dist/modules/properties/index.d.ts +20 -0
- package/dist/modules/properties/index.js +19 -0
- package/dist/modules/properties-activiti/index.d.ts +3 -0
- package/dist/modules/properties-activiti/index.js +97 -0
- package/dist/modules/properties-bpmn/index.d.ts +3 -0
- package/dist/modules/properties-bpmn/index.js +518 -0
- package/dist/modules/properties-core/index.d.ts +124 -0
- package/dist/modules/properties-core/index.js +312 -0
- package/dist/modules/properties-flowable/index.d.ts +3 -0
- package/dist/modules/properties-flowable/index.js +114 -0
- package/dist/modules/properties-renderer/index.d.ts +25 -0
- package/dist/modules/properties-renderer/index.js +491 -0
- package/dist/modules/renderer-svg/index.d.ts +118 -0
- package/dist/modules/renderer-svg/index.js +1460 -0
- package/dist/modules/runtime/index.d.ts +169 -0
- package/dist/modules/runtime/index.js +535 -0
- package/dist/modules/theme/index.d.ts +95 -0
- package/dist/modules/theme/index.js +368 -0
- package/dist/modules/viewer/index.d.ts +265 -0
- package/dist/modules/viewer/index.js +1011 -0
- package/dist/modules/viewer/runtime-content.js +123 -0
- package/dist/modules/viewer/runtime-details-motion.js +228 -0
- package/dist/modules/viewer/runtime-trace.js +574 -0
- package/dist/modules/viewer/timeline.js +276 -0
- package/dist/selection-layout.js +1 -1
- package/dist/shell.js +210 -26
- package/dist/styles.css +116 -7
- package/llms-full.txt +3082 -0
- package/llms.txt +225 -0
- package/package.json +39 -16
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { downloadSvg } from './render.js';
|
|
2
|
+
|
|
3
|
+
function element(document, tag, className = '', text = '') {
|
|
4
|
+
const node = document.createElement(tag);
|
|
5
|
+
if (className) node.className = className;
|
|
6
|
+
if (text) node.textContent = text;
|
|
7
|
+
return node;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function button(document, label, className = '') {
|
|
11
|
+
const node = element(document, 'button', className, label);
|
|
12
|
+
node.type = 'button';
|
|
13
|
+
return node;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clamp(value, min, max) {
|
|
17
|
+
return Math.max(min, Math.min(max, value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SvgExportPreviewController {
|
|
21
|
+
constructor({ container, createArtifact, title = '导出 SVG', initialTheme = 'current', onClose = null, onDownload = null } = {}) {
|
|
22
|
+
if (!container) throw new Error('SVG export preview requires a container.');
|
|
23
|
+
if (typeof createArtifact !== 'function') throw new Error('SVG export preview requires createArtifact().');
|
|
24
|
+
this.container = container;
|
|
25
|
+
this.document = container.ownerDocument;
|
|
26
|
+
this.ownerWindow = this.document.defaultView || globalThis;
|
|
27
|
+
this.createArtifact = createArtifact;
|
|
28
|
+
this.onClose = onClose;
|
|
29
|
+
this.onDownload = onDownload;
|
|
30
|
+
this.theme = ['light', 'dark'].includes(initialTheme) ? initialTheme : 'current';
|
|
31
|
+
this.transparentBackground = false;
|
|
32
|
+
this.artifact = null;
|
|
33
|
+
this.abortController = null;
|
|
34
|
+
this.objectUrl = null;
|
|
35
|
+
this.zoom = 1;
|
|
36
|
+
this.pan = { x: 0, y: 0 };
|
|
37
|
+
this.drag = null;
|
|
38
|
+
this.closed = false;
|
|
39
|
+
this._restoreFocus = this.document.activeElement instanceof this.ownerWindow.HTMLElement ? this.document.activeElement : null;
|
|
40
|
+
this._build(title);
|
|
41
|
+
this._bind();
|
|
42
|
+
this._generate();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_build(title) {
|
|
46
|
+
const document = this.document;
|
|
47
|
+
const root = element(document, 'div', 'nova-svg-export-preview is-entering');
|
|
48
|
+
root.setAttribute('role', 'dialog');
|
|
49
|
+
root.setAttribute('aria-modal', 'true');
|
|
50
|
+
root.setAttribute('aria-label', title);
|
|
51
|
+
const dialog = element(document, 'section', 'nova-svg-export-dialog');
|
|
52
|
+
const header = element(document, 'header', 'nova-svg-export-header');
|
|
53
|
+
const heading = element(document, 'div', 'nova-svg-export-heading');
|
|
54
|
+
heading.append(element(document, 'strong', '', title), element(document, 'span', '', '预览确认后下载'));
|
|
55
|
+
const close = button(document, '×', 'nova-svg-export-close');
|
|
56
|
+
close.setAttribute('aria-label', '关闭 SVG 预览');
|
|
57
|
+
header.append(heading, close);
|
|
58
|
+
|
|
59
|
+
const toolbar = element(document, 'div', 'nova-svg-export-toolbar');
|
|
60
|
+
const themeGroup = element(document, 'div', 'nova-svg-export-theme');
|
|
61
|
+
themeGroup.setAttribute('role', 'group');
|
|
62
|
+
themeGroup.setAttribute('aria-label', 'SVG 主题');
|
|
63
|
+
this.themeButtons = new Map();
|
|
64
|
+
for (const option of [{ value: 'current', label: '当前' }, { value: 'light', label: '浅色' }, { value: 'dark', label: '深色' }]) {
|
|
65
|
+
const item = button(document, option.label);
|
|
66
|
+
item.dataset.exportTheme = option.value;
|
|
67
|
+
item.addEventListener('click', () => this.setTheme(option.value));
|
|
68
|
+
themeGroup.appendChild(item);
|
|
69
|
+
this.themeButtons.set(option.value, item);
|
|
70
|
+
}
|
|
71
|
+
const transparentLabel = element(document, 'label', 'nova-svg-export-transparent');
|
|
72
|
+
const transparent = element(document, 'input');
|
|
73
|
+
transparent.type = 'checkbox';
|
|
74
|
+
transparent.addEventListener('change', () => this.setTransparentBackground(transparent.checked));
|
|
75
|
+
transparentLabel.append(transparent, document.createTextNode('透明背景'));
|
|
76
|
+
const viewportTools = element(document, 'div', 'nova-svg-export-viewport-tools');
|
|
77
|
+
const zoomOut = button(document, '−');
|
|
78
|
+
zoomOut.title = '缩小';
|
|
79
|
+
const fit = button(document, '最佳视图');
|
|
80
|
+
const actual = button(document, '1:1');
|
|
81
|
+
const zoomIn = button(document, '+');
|
|
82
|
+
zoomIn.title = '放大';
|
|
83
|
+
this.zoomText = element(document, 'span', '', '100%');
|
|
84
|
+
viewportTools.append(zoomOut, this.zoomText, zoomIn, fit, actual);
|
|
85
|
+
toolbar.append(themeGroup, transparentLabel, viewportTools);
|
|
86
|
+
|
|
87
|
+
const body = element(document, 'div', 'nova-svg-export-body');
|
|
88
|
+
const viewport = element(document, 'div', 'nova-svg-export-viewport');
|
|
89
|
+
viewport.tabIndex = 0;
|
|
90
|
+
viewport.setAttribute('aria-label', 'SVG 预览画布,可拖动和缩放');
|
|
91
|
+
const stage = element(document, 'div', 'nova-svg-export-stage');
|
|
92
|
+
const image = element(document, 'img', 'nova-svg-export-image');
|
|
93
|
+
image.alt = 'SVG 导出预览';
|
|
94
|
+
stage.appendChild(image);
|
|
95
|
+
viewport.appendChild(stage);
|
|
96
|
+
const state = element(document, 'div', 'nova-svg-export-state');
|
|
97
|
+
state.append(element(document, 'span', 'nova-svg-export-spinner'), element(document, 'strong', '', '正在生成 SVG…'));
|
|
98
|
+
body.append(viewport, state);
|
|
99
|
+
|
|
100
|
+
const warning = element(document, 'div', 'nova-svg-export-warnings');
|
|
101
|
+
warning.hidden = true;
|
|
102
|
+
const footer = element(document, 'footer', 'nova-svg-export-footer');
|
|
103
|
+
const summary = element(document, 'span', 'nova-svg-export-summary', '准备导出');
|
|
104
|
+
const actions = element(document, 'div', 'nova-svg-export-actions');
|
|
105
|
+
const cancel = button(document, '取消');
|
|
106
|
+
const retry = button(document, '重试', 'nova-svg-export-retry');
|
|
107
|
+
retry.hidden = true;
|
|
108
|
+
const confirm = button(document, '确认下载', 'is-primary');
|
|
109
|
+
confirm.disabled = true;
|
|
110
|
+
actions.append(cancel, retry, confirm);
|
|
111
|
+
footer.append(summary, actions);
|
|
112
|
+
dialog.append(header, toolbar, body, warning, footer);
|
|
113
|
+
root.appendChild(dialog);
|
|
114
|
+
this.container.appendChild(root);
|
|
115
|
+
|
|
116
|
+
Object.assign(this, { root, dialog, closeButton: close, toolbar, transparentInput: transparent, viewport, stage, image, state, warning, summary, cancelButton: cancel, retryButton: retry, confirmButton: confirm, zoomOutButton: zoomOut, zoomInButton: zoomIn, fitButton: fit, actualButton: actual });
|
|
117
|
+
this._syncThemeButtons();
|
|
118
|
+
this.ownerWindow.requestAnimationFrame(() => {
|
|
119
|
+
if (!root.isConnected) return;
|
|
120
|
+
root.classList.remove('is-entering');
|
|
121
|
+
root.classList.add('is-open');
|
|
122
|
+
close.focus({ preventScroll: true });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_bind() {
|
|
127
|
+
this.root.addEventListener('pointerdown', (event) => {
|
|
128
|
+
if (event.target === this.root) this.close();
|
|
129
|
+
});
|
|
130
|
+
this.closeButton.addEventListener('click', () => this.close());
|
|
131
|
+
this.cancelButton.addEventListener('click', () => this.close());
|
|
132
|
+
this.retryButton.addEventListener('click', () => this._generate());
|
|
133
|
+
this.confirmButton.addEventListener('click', () => {
|
|
134
|
+
if (!this.artifact) return;
|
|
135
|
+
downloadSvg(this.artifact, { document: this.document });
|
|
136
|
+
this.onDownload?.(this.artifact);
|
|
137
|
+
this.close();
|
|
138
|
+
});
|
|
139
|
+
this.zoomOutButton.addEventListener('click', () => this._zoomBy(0.85));
|
|
140
|
+
this.zoomInButton.addEventListener('click', () => this._zoomBy(1.18));
|
|
141
|
+
this.fitButton.addEventListener('click', () => this.fit());
|
|
142
|
+
this.actualButton.addEventListener('click', () => this.actualSize());
|
|
143
|
+
this.viewport.addEventListener('wheel', (event) => {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
146
|
+
const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
147
|
+
this._setZoom(this.zoom * (event.deltaY < 0 ? 1.1 : 0.9), point);
|
|
148
|
+
}, { passive: false });
|
|
149
|
+
this.viewport.addEventListener('pointerdown', (event) => {
|
|
150
|
+
if (event.button !== 0) return;
|
|
151
|
+
this.drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, pan: { ...this.pan } };
|
|
152
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
153
|
+
this.viewport.classList.add('is-dragging');
|
|
154
|
+
});
|
|
155
|
+
this.viewport.addEventListener('pointermove', (event) => {
|
|
156
|
+
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
|
157
|
+
this.pan.x = this.drag.pan.x + event.clientX - this.drag.x;
|
|
158
|
+
this.pan.y = this.drag.pan.y + event.clientY - this.drag.y;
|
|
159
|
+
this._applyTransform();
|
|
160
|
+
});
|
|
161
|
+
const finishDrag = (event) => {
|
|
162
|
+
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
|
163
|
+
if (this.viewport.hasPointerCapture?.(event.pointerId)) this.viewport.releasePointerCapture?.(event.pointerId);
|
|
164
|
+
this.drag = null;
|
|
165
|
+
this.viewport.classList.remove('is-dragging');
|
|
166
|
+
};
|
|
167
|
+
this.viewport.addEventListener('pointerup', finishDrag);
|
|
168
|
+
this.viewport.addEventListener('pointercancel', finishDrag);
|
|
169
|
+
this._keyHandler = (event) => {
|
|
170
|
+
if (!this.root?.isConnected) return;
|
|
171
|
+
if (event.key === 'Escape') { event.preventDefault(); this.close(); return; }
|
|
172
|
+
if (event.key !== 'Tab') return;
|
|
173
|
+
const focusable = [...this.dialog.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]), [tabindex]:not([tabindex="-1"])')].filter((node) => !node.hidden);
|
|
174
|
+
if (!focusable.length) return;
|
|
175
|
+
const first = focusable[0];
|
|
176
|
+
const last = focusable.at(-1);
|
|
177
|
+
if (event.shiftKey && this.document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
178
|
+
else if (!event.shiftKey && this.document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
179
|
+
};
|
|
180
|
+
this.document.addEventListener('keydown', this._keyHandler);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_syncThemeButtons() {
|
|
184
|
+
this.themeButtons.forEach((button, value) => {
|
|
185
|
+
const active = value === this.theme;
|
|
186
|
+
button.classList.toggle('is-active', active);
|
|
187
|
+
button.setAttribute('aria-pressed', String(active));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async _generate() {
|
|
192
|
+
if (this.closed) return;
|
|
193
|
+
this.abortController?.abort();
|
|
194
|
+
this.abortController = new AbortController();
|
|
195
|
+
const signal = this.abortController.signal;
|
|
196
|
+
this.artifact = null;
|
|
197
|
+
this.confirmButton.disabled = true;
|
|
198
|
+
this.retryButton.hidden = true;
|
|
199
|
+
this.warning.hidden = true;
|
|
200
|
+
this.root.classList.add('is-loading');
|
|
201
|
+
this.state.hidden = false;
|
|
202
|
+
this.state.classList.remove('is-error');
|
|
203
|
+
this.state.querySelector('strong').textContent = '正在生成 SVG…';
|
|
204
|
+
this.summary.textContent = '正在准备完整内容';
|
|
205
|
+
this._revokePreviewUrl();
|
|
206
|
+
this.image.removeAttribute('src');
|
|
207
|
+
try {
|
|
208
|
+
const artifact = await this.createArtifact({ theme: this.theme, transparentBackground: this.transparentBackground, signal });
|
|
209
|
+
if (signal.aborted || this.closed) return;
|
|
210
|
+
this.artifact = artifact;
|
|
211
|
+
this.objectUrl = this.ownerWindow.URL.createObjectURL(artifact.blob);
|
|
212
|
+
this.image.addEventListener('load', () => {
|
|
213
|
+
if (!this.root?.isConnected) return;
|
|
214
|
+
this.state.hidden = true;
|
|
215
|
+
this.root.classList.remove('is-loading');
|
|
216
|
+
this.fit();
|
|
217
|
+
}, { once: true });
|
|
218
|
+
this.image.src = this.objectUrl;
|
|
219
|
+
this.confirmButton.disabled = false;
|
|
220
|
+
this.summary.textContent = `${artifact.filename} · ${Math.round(artifact.width)} × ${Math.round(artifact.height)}`;
|
|
221
|
+
this._renderWarnings(artifact.warnings || []);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (signal.aborted || this.closed) return;
|
|
224
|
+
this.root.classList.remove('is-loading');
|
|
225
|
+
this.state.hidden = false;
|
|
226
|
+
this.state.classList.add('is-error');
|
|
227
|
+
this.state.querySelector('strong').textContent = `生成失败:${error?.message || '未知错误'}`;
|
|
228
|
+
this.summary.textContent = 'SVG 尚未生成';
|
|
229
|
+
this.retryButton.hidden = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_renderWarnings(warnings) {
|
|
234
|
+
this.warning.replaceChildren();
|
|
235
|
+
if (!warnings.length) { this.warning.hidden = true; return; }
|
|
236
|
+
this.warning.hidden = false;
|
|
237
|
+
this.warning.appendChild(element(this.document, 'strong', '', `${warnings.length} 项内容已降级处理`));
|
|
238
|
+
const list = element(this.document, 'ul');
|
|
239
|
+
warnings.slice(0, 5).forEach((item) => list.appendChild(element(this.document, 'li', '', item.message)));
|
|
240
|
+
if (warnings.length > 5) list.appendChild(element(this.document, 'li', '', `另有 ${warnings.length - 5} 项`));
|
|
241
|
+
this.warning.appendChild(list);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_revokePreviewUrl() {
|
|
245
|
+
if (this.objectUrl) this.ownerWindow.URL.revokeObjectURL(this.objectUrl);
|
|
246
|
+
this.objectUrl = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_applyTransform() {
|
|
250
|
+
this.stage.style.transform = `translate(${this.pan.x}px, ${this.pan.y}px) scale(${this.zoom})`;
|
|
251
|
+
this.zoomText.textContent = `${Math.round(this.zoom * 100)}%`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_setZoom(value, anchor = null) {
|
|
255
|
+
const next = clamp(value, 0.08, 6);
|
|
256
|
+
if (anchor) {
|
|
257
|
+
this.pan.x = anchor.x - (anchor.x - this.pan.x) * (next / this.zoom);
|
|
258
|
+
this.pan.y = anchor.y - (anchor.y - this.pan.y) * (next / this.zoom);
|
|
259
|
+
}
|
|
260
|
+
this.zoom = next;
|
|
261
|
+
this._applyTransform();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_zoomBy(factor) {
|
|
265
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
266
|
+
this._setZoom(this.zoom * factor, { x: rect.width / 2, y: rect.height / 2 });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fit() {
|
|
270
|
+
if (!this.artifact) return;
|
|
271
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
272
|
+
const zoom = clamp(Math.min((rect.width - 48) / this.artifact.width, (rect.height - 48) / this.artifact.height), 0.08, 2);
|
|
273
|
+
this.zoom = zoom;
|
|
274
|
+
this.pan = { x: (rect.width - this.artifact.width * zoom) / 2, y: (rect.height - this.artifact.height * zoom) / 2 };
|
|
275
|
+
this._applyTransform();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
actualSize() {
|
|
279
|
+
if (!this.artifact) return;
|
|
280
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
281
|
+
this.zoom = 1;
|
|
282
|
+
this.pan = { x: (rect.width - this.artifact.width) / 2, y: (rect.height - this.artifact.height) / 2 };
|
|
283
|
+
this._applyTransform();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setTheme(theme) {
|
|
287
|
+
if (!['current', 'light', 'dark'].includes(theme) || theme === this.theme) return;
|
|
288
|
+
this.theme = theme;
|
|
289
|
+
this._syncThemeButtons();
|
|
290
|
+
this._generate();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
setTransparentBackground(value) {
|
|
294
|
+
const next = Boolean(value);
|
|
295
|
+
if (next === this.transparentBackground) return;
|
|
296
|
+
this.transparentBackground = next;
|
|
297
|
+
this.transparentInput.checked = next;
|
|
298
|
+
this._generate();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
focus() {
|
|
302
|
+
this.closeButton?.focus?.({ preventScroll: true });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
close({ immediate = false } = {}) {
|
|
306
|
+
if (this.closed) return;
|
|
307
|
+
this.closed = true;
|
|
308
|
+
this.abortController?.abort();
|
|
309
|
+
this.document.removeEventListener('keydown', this._keyHandler);
|
|
310
|
+
this._revokePreviewUrl();
|
|
311
|
+
const finalize = () => {
|
|
312
|
+
this.root?.remove();
|
|
313
|
+
this._restoreFocus?.focus?.({ preventScroll: true });
|
|
314
|
+
this.onClose?.();
|
|
315
|
+
};
|
|
316
|
+
if (immediate || this.ownerWindow.matchMedia?.('(prefers-reduced-motion: reduce)').matches) finalize();
|
|
317
|
+
else {
|
|
318
|
+
this.root.classList.remove('is-open');
|
|
319
|
+
this.root.classList.add('is-closing');
|
|
320
|
+
this.ownerWindow.setTimeout(finalize, 190);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function openSvgExportPreview(options) {
|
|
326
|
+
return new SvgExportPreviewController(options);
|
|
327
|
+
}
|