@huanban/rulego-editor-ui 1.0.0

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.
@@ -0,0 +1,742 @@
1
+ class u {
2
+ constructor(e, t) {
3
+ this.collapsedGroups = /* @__PURE__ */ new Set(), this.searchKeyword = "", this.rootEl = null, this.unsubscribe = null, this.container = e, this.core = t.core, this.options = {
4
+ core: t.core,
5
+ searchable: t.searchable ?? !0,
6
+ defaultCollapsed: t.defaultCollapsed ?? !1,
7
+ searchPlaceholder: t.searchPlaceholder || this.core.i18n.t("sidebar.searchPlaceholder") || "搜索组件...",
8
+ width: t.width ?? "220px"
9
+ }, this.mount(), this.unsubscribe = this.core.store.select(
10
+ (n) => n.componentGroups
11
+ ).subscribe(
12
+ (n) => {
13
+ this.renderGroups(n);
14
+ }
15
+ );
16
+ }
17
+ // ============================================================
18
+ // 生命周期
19
+ // ============================================================
20
+ /**
21
+ * 挂载侧边栏 DOM 到容器
22
+ */
23
+ mount() {
24
+ if (this.rootEl = document.createElement("div"), this.rootEl.className = "rulego-sidebar", this.rootEl.style.width = this.options.width, this.options.searchable) {
25
+ const n = document.createElement("div");
26
+ n.className = "rulego-sidebar__search-wrapper";
27
+ const o = document.createElement("span");
28
+ o.className = "rulego-sidebar__search-icon", o.innerHTML = "🔍";
29
+ const s = document.createElement("input");
30
+ s.type = "text", s.className = "rulego-sidebar__search", s.placeholder = this.options.searchPlaceholder, s.addEventListener("input", (i) => {
31
+ this.searchKeyword = i.target.value.toLowerCase();
32
+ const l = this.core.store.getState().componentGroups;
33
+ this.renderGroups(l);
34
+ }), n.appendChild(o), n.appendChild(s), this.rootEl.appendChild(n);
35
+ }
36
+ const e = document.createElement("div");
37
+ e.className = "rulego-sidebar__groups", this.rootEl.appendChild(e), this.container.appendChild(this.rootEl);
38
+ const t = this.core.store.getState().componentGroups;
39
+ t && t.length > 0 && this.renderGroups(t);
40
+ }
41
+ /**
42
+ * 销毁侧边栏, 清理所有 DOM 和事件
43
+ */
44
+ destroy() {
45
+ this.unsubscribe && (this.unsubscribe(), this.unsubscribe = null), this.rootEl && this.rootEl.parentNode && this.rootEl.parentNode.removeChild(this.rootEl), this.rootEl = null;
46
+ }
47
+ // ============================================================
48
+ // 渲染
49
+ // ============================================================
50
+ /**
51
+ * 渲染所有组件分组
52
+ * @param groups - 原始组件分组列表
53
+ */
54
+ renderGroups(e) {
55
+ if (!this.rootEl) return;
56
+ const t = this.rootEl.querySelector(".rulego-sidebar__groups");
57
+ if (!t) return;
58
+ t.innerHTML = "";
59
+ const n = this.filterGroups(e);
60
+ if (n.length === 0) {
61
+ const o = document.createElement("div");
62
+ o.className = "rulego-sidebar__empty", o.textContent = this.core.i18n.t("sidebar.noResults") || "无匹配组件", t.appendChild(o);
63
+ return;
64
+ }
65
+ for (const o of n) {
66
+ const s = this.createGroupElement(o);
67
+ t.appendChild(s);
68
+ }
69
+ }
70
+ /**
71
+ * 创建单个分组 DOM 元素
72
+ * @param group - 组件分组定义
73
+ * @returns 分组 HTMLElement
74
+ */
75
+ createGroupElement(e) {
76
+ const t = document.createElement("div");
77
+ t.className = "rulego-sidebar__group";
78
+ const n = document.createElement("div");
79
+ n.className = "rulego-sidebar__group-header", n.style.borderLeftColor = e.color || "#ccc";
80
+ const o = this.collapsedGroups.has(e.category), s = document.createElement("span");
81
+ s.className = `rulego-sidebar__arrow ${o ? "rulego-sidebar__arrow--collapsed" : ""}`, s.textContent = "▼";
82
+ const i = document.createElement("span");
83
+ i.className = "rulego-sidebar__group-label", i.textContent = e.label;
84
+ const l = document.createElement("span");
85
+ if (l.className = "rulego-sidebar__group-count", l.textContent = `${e.components.length}`, n.appendChild(s), n.appendChild(i), n.appendChild(l), n.addEventListener("click", () => {
86
+ this.collapsedGroups.has(e.category) ? this.collapsedGroups.delete(e.category) : this.collapsedGroups.add(e.category);
87
+ const r = this.core.store.getState().componentGroups;
88
+ this.renderGroups(r);
89
+ }), t.appendChild(n), !o) {
90
+ const r = document.createElement("div");
91
+ r.className = "rulego-sidebar__component-list";
92
+ for (const c of e.components) {
93
+ const d = this.createComponentElement(c, e.color);
94
+ r.appendChild(d);
95
+ }
96
+ t.appendChild(r);
97
+ }
98
+ return t;
99
+ }
100
+ /**
101
+ * 创建单个组件 DOM 元素 (可拖拽)
102
+ * @param comp - 组件定义
103
+ * @param groupColor - 分组颜色
104
+ * @returns 组件 HTMLElement
105
+ */
106
+ createComponentElement(e, t) {
107
+ const n = document.createElement("div");
108
+ n.className = "rulego-sidebar__component", n.setAttribute("draggable", "true"), n.title = e.desc || e.label;
109
+ const o = document.createElement("span");
110
+ o.className = "rulego-sidebar__component-color", o.style.backgroundColor = e.color || t || "#ccc";
111
+ const s = document.createElement("span");
112
+ return s.className = "rulego-sidebar__component-name", s.textContent = e.label, n.appendChild(o), n.appendChild(s), n.addEventListener("dragstart", (i) => {
113
+ i.dataTransfer && (i.dataTransfer.setData("application/rulego-component", JSON.stringify({
114
+ type: e.type,
115
+ category: e.category,
116
+ label: e.label,
117
+ color: e.color || t
118
+ })), i.dataTransfer.effectAllowed = "copy"), n.classList.add("rulego-sidebar__component--dragging");
119
+ }), n.addEventListener("dragend", () => {
120
+ n.classList.remove("rulego-sidebar__component--dragging");
121
+ }), n;
122
+ }
123
+ // ============================================================
124
+ // 搜索过滤
125
+ // ============================================================
126
+ /**
127
+ * 根据搜索关键字过滤组件分组
128
+ * @param groups - 原始分组列表
129
+ * @returns 过滤后的分组列表 (空分组会被移除)
130
+ */
131
+ filterGroups(e) {
132
+ return this.searchKeyword ? e.map((t) => ({
133
+ ...t,
134
+ components: t.components.filter(
135
+ (n) => n.label.toLowerCase().includes(this.searchKeyword) || n.type.toLowerCase().includes(this.searchKeyword) || n.desc && n.desc.toLowerCase().includes(this.searchKeyword)
136
+ )
137
+ })).filter((t) => t.components.length > 0) : e;
138
+ }
139
+ // ============================================================
140
+ // 公共 API
141
+ // ============================================================
142
+ /**
143
+ * 手动触发刷新
144
+ */
145
+ refresh() {
146
+ const e = this.core.store.getState().componentGroups;
147
+ this.renderGroups(e);
148
+ }
149
+ /**
150
+ * 展开所有分组
151
+ */
152
+ expandAll() {
153
+ this.collapsedGroups.clear(), this.refresh();
154
+ }
155
+ /**
156
+ * 折叠所有分组
157
+ */
158
+ collapseAll() {
159
+ const e = this.core.store.getState().componentGroups;
160
+ for (const t of e)
161
+ this.collapsedGroups.add(t.category);
162
+ this.refresh();
163
+ }
164
+ /**
165
+ * 获取根 DOM 元素
166
+ */
167
+ getElement() {
168
+ return this.rootEl;
169
+ }
170
+ }
171
+ class h {
172
+ constructor(e, t) {
173
+ this.rootEl = null, this.unsubscribe = null, this.buttonElements = /* @__PURE__ */ new Map(), this.container = e, this.core = t.core, this.options = {
174
+ core: t.core,
175
+ extraButtons: t.extraButtons ?? [],
176
+ showZoom: t.showZoom ?? !0,
177
+ height: t.height ?? "40px"
178
+ }, this.mount(), this.unsubscribe = this.core.store.subscribe(() => {
179
+ this.updateButtonStates();
180
+ });
181
+ }
182
+ // ============================================================
183
+ // 生命周期
184
+ // ============================================================
185
+ /**
186
+ * 挂载工具栏 DOM
187
+ */
188
+ mount() {
189
+ this.rootEl = document.createElement("div"), this.rootEl.className = "rulego-toolbar", this.rootEl.style.height = this.options.height;
190
+ const e = this.getBuiltinButtons();
191
+ this.options.extraButtons.length > 0 && (e.push({
192
+ id: "__divider_extra",
193
+ icon: "",
194
+ title: "",
195
+ divider: !0,
196
+ onClick: () => {
197
+ }
198
+ }), e.push(...this.options.extraButtons));
199
+ for (const t of e) {
200
+ if (t.divider) {
201
+ const o = document.createElement("span");
202
+ o.className = "rulego-toolbar__divider", this.rootEl.appendChild(o);
203
+ }
204
+ if (!t.icon && t.divider) continue;
205
+ const n = document.createElement("button");
206
+ if (n.className = "rulego-toolbar__button", n.title = t.title, n.innerHTML = t.icon, t.label) {
207
+ const o = document.createElement("span");
208
+ o.className = "rulego-toolbar__button-label", o.textContent = t.label, n.appendChild(o);
209
+ }
210
+ n.addEventListener("click", (o) => {
211
+ o.preventDefault(), o.stopPropagation(), t.onClick();
212
+ }), this.buttonElements.set(t.id, n), this.rootEl.appendChild(n);
213
+ }
214
+ this.container.appendChild(this.rootEl), this.updateButtonStates();
215
+ }
216
+ /**
217
+ * 销毁工具栏
218
+ */
219
+ destroy() {
220
+ this.unsubscribe && (this.unsubscribe(), this.unsubscribe = null), this.rootEl && this.rootEl.parentNode && this.rootEl.parentNode.removeChild(this.rootEl), this.rootEl = null, this.buttonElements.clear();
221
+ }
222
+ // ============================================================
223
+ // 按钮定义
224
+ // ============================================================
225
+ /**
226
+ * 获取内置按钮列表
227
+ * @returns 内置按钮定义数组
228
+ */
229
+ getBuiltinButtons() {
230
+ const e = this.core.i18n, t = [
231
+ {
232
+ id: "undo",
233
+ icon: "↩",
234
+ title: e.t("toolbar.undo") || "撤销",
235
+ disabled: () => !this.core.store.getState().canUndo,
236
+ onClick: () => this.core.eventBus.emit("editor:reset")
237
+ },
238
+ {
239
+ id: "redo",
240
+ icon: "↪",
241
+ title: e.t("toolbar.redo") || "重做",
242
+ disabled: () => !this.core.store.getState().canRedo,
243
+ onClick: () => {
244
+ }
245
+ },
246
+ {
247
+ id: "delete",
248
+ icon: "🗑",
249
+ title: e.t("toolbar.delete") || "删除选中",
250
+ divider: !0,
251
+ onClick: () => this.core.eventBus.emit("editor:delete-selected")
252
+ },
253
+ {
254
+ id: "save",
255
+ icon: "💾",
256
+ title: e.t("toolbar.save") || "保存",
257
+ onClick: () => this.core.eventBus.emit("editor:save", {
258
+ data: {},
259
+ success: !1
260
+ })
261
+ },
262
+ {
263
+ id: "fullscreen",
264
+ icon: "⛶",
265
+ title: e.t("toolbar.fullscreen") || "全屏",
266
+ onClick: () => this.core.eventBus.emit("editor:fullscreen")
267
+ }
268
+ ];
269
+ return this.options.showZoom && t.push(
270
+ {
271
+ id: "zoom-in",
272
+ icon: "🔍+",
273
+ title: e.t("toolbar.zoomIn") || "放大",
274
+ divider: !0,
275
+ onClick: () => this.core.eventBus.emit("canvas:zoom", { scale: 1.1 })
276
+ },
277
+ {
278
+ id: "zoom-out",
279
+ icon: "🔍-",
280
+ title: e.t("toolbar.zoomOut") || "缩小",
281
+ onClick: () => this.core.eventBus.emit("canvas:zoom", { scale: 0.9 })
282
+ },
283
+ {
284
+ id: "zoom-fit",
285
+ icon: "🔲",
286
+ title: e.t("toolbar.fitView") || "适应画布",
287
+ onClick: () => this.core.eventBus.emit("canvas:zoom", { scale: 1 })
288
+ }
289
+ ), t;
290
+ }
291
+ // ============================================================
292
+ // 状态更新
293
+ // ============================================================
294
+ /**
295
+ * 根据 EditorCore 的状态更新按钮的 disabled 属性
296
+ */
297
+ updateButtonStates() {
298
+ for (const [e, t] of this.buttonElements) {
299
+ const n = this.getBuiltinButtons().find((o) => o.id === e);
300
+ n && typeof n.disabled == "function" && (t.disabled = n.disabled(), t.classList.toggle("rulego-toolbar__button--disabled", t.disabled));
301
+ }
302
+ }
303
+ // ============================================================
304
+ // 公共 API
305
+ // ============================================================
306
+ /**
307
+ * 获取根 DOM 元素
308
+ */
309
+ getElement() {
310
+ return this.rootEl;
311
+ }
312
+ /**
313
+ * 动态添加自定义按钮
314
+ * @param button - 按钮定义
315
+ */
316
+ addButton(e) {
317
+ if (!this.rootEl) return;
318
+ const t = document.createElement("button");
319
+ t.className = "rulego-toolbar__button", t.title = e.title, t.innerHTML = e.icon, t.addEventListener("click", (n) => {
320
+ n.preventDefault(), e.onClick();
321
+ }), this.buttonElements.set(e.id, t), this.rootEl.appendChild(t);
322
+ }
323
+ }
324
+ class p {
325
+ constructor(e, t) {
326
+ this.rootEl = null, this.formContainer = null, this.titleEl = null, this.unsubscribe = null, this.eventCleanups = [], this.currentNodeId = null, this.container = e, this.core = t.core, this.options = {
327
+ core: t.core,
328
+ title: t.title || this.core.i18n.t("panel.title") || "属性编辑",
329
+ width: t.width ?? "320px",
330
+ position: t.position ?? "right"
331
+ }, this.mount(), this.bindEvents();
332
+ }
333
+ // ============================================================
334
+ // 生命周期
335
+ // ============================================================
336
+ /**
337
+ * 创建面板 DOM
338
+ */
339
+ mount() {
340
+ this.rootEl = document.createElement("div"), this.rootEl.className = `rulego-property-panel rulego-property-panel--${this.options.position}`, this.rootEl.style.width = this.options.width;
341
+ const e = document.createElement("div");
342
+ e.className = "rulego-property-panel__header", this.titleEl = document.createElement("h3"), this.titleEl.className = "rulego-property-panel__title", this.titleEl.textContent = this.options.title;
343
+ const t = document.createElement("button");
344
+ t.className = "rulego-property-panel__close", t.textContent = "✕", t.addEventListener("click", () => this.clearPanel()), e.appendChild(this.titleEl), e.appendChild(t), this.rootEl.appendChild(e), this.formContainer = document.createElement("div"), this.formContainer.className = "rulego-property-panel__form", this.rootEl.appendChild(this.formContainer), this.rootEl.style.display = "none", this.container.appendChild(this.rootEl);
345
+ }
346
+ /**
347
+ * 绑定事件监听
348
+ */
349
+ bindEvents() {
350
+ this.unsubscribe = this.core.store.subscribe((t) => {
351
+ const n = t.currentNodeView, o = t.currentNodeModel;
352
+ n && o && Object.keys(o).length > 0 && this.showPanel(n, o);
353
+ });
354
+ const e = this.core.eventBus.on("blank:click", () => {
355
+ this.clearPanel();
356
+ });
357
+ this.eventCleanups.push(e);
358
+ }
359
+ /**
360
+ * 销毁面板
361
+ */
362
+ destroy() {
363
+ this.unsubscribe && (this.unsubscribe(), this.unsubscribe = null);
364
+ for (const e of this.eventCleanups)
365
+ e();
366
+ this.eventCleanups = [], this.rootEl && this.rootEl.parentNode && this.rootEl.parentNode.removeChild(this.rootEl), this.rootEl = null, this.formContainer = null;
367
+ }
368
+ // ============================================================
369
+ // 面板展示 / 清空
370
+ // ============================================================
371
+ /**
372
+ * 显示面板并渲染表单
373
+ * @param view - 组件定义 (含字段列表)
374
+ * @param model - 当前节点模型数据
375
+ */
376
+ showPanel(e, t) {
377
+ if (!(!this.rootEl || !this.formContainer || !this.titleEl) && (this.rootEl.style.display = "flex", this.titleEl.textContent = e.label || this.options.title, this.formContainer.innerHTML = "", this.renderBasicInfo(t), e.fields && e.fields.length > 0))
378
+ for (const n of e.fields) {
379
+ const o = this.createFieldElement(n, t);
380
+ this.formContainer.appendChild(o);
381
+ }
382
+ }
383
+ /**
384
+ * 清空面板并隐藏
385
+ */
386
+ clearPanel() {
387
+ !this.rootEl || !this.formContainer || (this.formContainer.innerHTML = "", this.rootEl.style.display = "none", this.currentNodeId = null);
388
+ }
389
+ // ============================================================
390
+ // 表单渲染
391
+ // ============================================================
392
+ /**
393
+ * 渲染基础信息区 (节点名称等)
394
+ * @param model - 节点模型
395
+ */
396
+ renderBasicInfo(e) {
397
+ if (!this.formContainer) return;
398
+ const t = document.createElement("div");
399
+ t.className = "rulego-property-panel__section";
400
+ const n = this.createTextField({
401
+ name: "name",
402
+ type: "string",
403
+ defaultValue: "",
404
+ label: this.core.i18n.t("panel.nodeName") || "节点名称",
405
+ desc: "",
406
+ validate: "",
407
+ fields: null
408
+ }, e);
409
+ t.appendChild(n), this.formContainer.appendChild(t);
410
+ }
411
+ /**
412
+ * 创建单个表单字段元素
413
+ * @param field - 字段定义
414
+ * @param model - 节点模型数据
415
+ * @returns 字段 wrapper HTMLElement
416
+ */
417
+ createFieldElement(e, t) {
418
+ const n = document.createElement("div");
419
+ n.className = "rulego-property-panel__field";
420
+ const o = document.createElement("label");
421
+ o.className = "rulego-property-panel__label", o.textContent = e.label || e.name, e.desc && (o.title = e.desc), n.appendChild(o);
422
+ let s;
423
+ switch (e.type) {
424
+ case "bool":
425
+ s = this.createBoolField(e, t);
426
+ break;
427
+ case "select":
428
+ s = this.createSelectField(e, t);
429
+ break;
430
+ case "code":
431
+ case "json":
432
+ s = this.createTextareaField(e, t);
433
+ break;
434
+ case "int":
435
+ s = this.createNumberField(e, t);
436
+ break;
437
+ default:
438
+ s = this.createTextField(e, t);
439
+ break;
440
+ }
441
+ if (n.appendChild(s), e.desc) {
442
+ const i = document.createElement("span");
443
+ i.className = "rulego-property-panel__desc", i.textContent = e.desc, n.appendChild(i);
444
+ }
445
+ return n;
446
+ }
447
+ /**
448
+ * 文本输入框
449
+ */
450
+ createTextField(e, t) {
451
+ const n = document.createElement("input");
452
+ return n.type = "text", n.className = "rulego-property-panel__input", n.name = e.name, n.value = String(this.getModelValue(e, t) ?? ""), n.placeholder = e.desc || "", n.addEventListener("change", () => {
453
+ this.handleFieldChange(e.name, n.value);
454
+ }), n;
455
+ }
456
+ /**
457
+ * 数值输入框
458
+ */
459
+ createNumberField(e, t) {
460
+ const n = document.createElement("input");
461
+ return n.type = "number", n.className = "rulego-property-panel__input", n.name = e.name, n.value = String(this.getModelValue(e, t) ?? 0), n.addEventListener("change", () => {
462
+ this.handleFieldChange(e.name, Number(n.value));
463
+ }), n;
464
+ }
465
+ /**
466
+ * 布尔开关
467
+ */
468
+ createBoolField(e, t) {
469
+ const n = document.createElement("div");
470
+ n.className = "rulego-property-panel__switch-wrapper";
471
+ const o = `field-${e.name}-${Date.now()}`, s = document.createElement("input");
472
+ s.type = "checkbox", s.className = "rulego-property-panel__checkbox", s.id = o, s.checked = !!this.getModelValue(e, t);
473
+ const i = document.createElement("label");
474
+ return i.className = "rulego-property-panel__switch", i.htmlFor = o, s.addEventListener("change", () => {
475
+ this.handleFieldChange(e.name, s.checked);
476
+ }), n.appendChild(s), n.appendChild(i), n;
477
+ }
478
+ /**
479
+ * 下拉选择
480
+ */
481
+ createSelectField(e, t) {
482
+ const n = document.createElement("select");
483
+ n.className = "rulego-property-panel__select", n.name = e.name;
484
+ const o = String(this.getModelValue(e, t) ?? "");
485
+ if (e.options && e.options.length > 0)
486
+ for (const s of e.options) {
487
+ const i = document.createElement("option");
488
+ i.value = s.value, i.textContent = s.label, s.value === o && (i.selected = !0), n.appendChild(i);
489
+ }
490
+ return n.addEventListener("change", () => {
491
+ this.handleFieldChange(e.name, n.value);
492
+ }), n;
493
+ }
494
+ /**
495
+ * 多行文本 / 代码编辑器
496
+ */
497
+ createTextareaField(e, t) {
498
+ const n = document.createElement("textarea");
499
+ return n.className = "rulego-property-panel__textarea", n.name = e.name, n.rows = 6, n.value = String(this.getModelValue(e, t) ?? ""), n.placeholder = e.desc || "", (e.type === "code" || e.type === "json") && (n.spellcheck = !1, n.style.fontFamily = "monospace"), n.addEventListener("change", () => {
500
+ this.handleFieldChange(e.name, n.value);
501
+ }), n;
502
+ }
503
+ // ============================================================
504
+ // 数据处理
505
+ // ============================================================
506
+ /**
507
+ * 从模型中获取字段当前值
508
+ * @param field - 字段定义
509
+ * @param model - 节点模型
510
+ * @returns 字段值
511
+ */
512
+ getModelValue(e, t) {
513
+ const n = t.configuration;
514
+ return n && e.name in n ? n[e.name] : e.name in t ? t[e.name] : e.defaultValue;
515
+ }
516
+ /**
517
+ * 处理字段变更, 通知 EditorCore
518
+ * @param fieldName - 字段名
519
+ * @param value - 新值
520
+ */
521
+ handleFieldChange(e, t) {
522
+ this.core.eventBus.emit("node:property-update", {
523
+ id: this.currentNodeId || "",
524
+ properties: { [e]: t }
525
+ }), this.core.store.setState({ isDirty: !0 });
526
+ }
527
+ // ============================================================
528
+ // 公共 API
529
+ // ============================================================
530
+ /**
531
+ * 获取根 DOM 元素
532
+ */
533
+ getElement() {
534
+ return this.rootEl;
535
+ }
536
+ /**
537
+ * 手动设置当前编辑的节点 ID
538
+ * @param nodeId - LogicFlow 节点 ID
539
+ */
540
+ setCurrentNodeId(e) {
541
+ this.currentNodeId = e;
542
+ }
543
+ /**
544
+ * 手动显示面板
545
+ * @param view - 组件定义
546
+ * @param model - 节点模型
547
+ * @param nodeId - 节点 ID
548
+ */
549
+ show(e, t, n) {
550
+ n && (this.currentNodeId = n), this.showPanel(e, t);
551
+ }
552
+ /**
553
+ * 手动隐藏面板
554
+ */
555
+ hide() {
556
+ this.clearPanel();
557
+ }
558
+ }
559
+ class m {
560
+ constructor(e, t) {
561
+ this.menuEl = null, this.currentContext = null, this.outsideClickHandler = null, this.container = e, this.core = t.core;
562
+ const n = t.includeBuiltin !== !1 ? this.getBuiltinItems() : [];
563
+ this.items = [...n, ...t.items ?? []], this.mount(), this.bindEvents();
564
+ }
565
+ // ============================================================
566
+ // 生命周期
567
+ // ============================================================
568
+ /**
569
+ * 创建菜单 DOM (初始隐藏)
570
+ */
571
+ mount() {
572
+ this.menuEl = document.createElement("div"), this.menuEl.className = "rulego-context-menu", this.menuEl.style.display = "none", document.body.appendChild(this.menuEl);
573
+ }
574
+ /**
575
+ * 绑定事件
576
+ */
577
+ bindEvents() {
578
+ this.container.addEventListener("contextmenu", (e) => {
579
+ e.preventDefault(), this.handleContextMenu(e);
580
+ }), this.outsideClickHandler = (e) => {
581
+ this.menuEl && !this.menuEl.contains(e.target) && this.hide();
582
+ }, document.addEventListener("click", this.outsideClickHandler), document.addEventListener("keydown", (e) => {
583
+ e.key === "Escape" && this.hide();
584
+ });
585
+ }
586
+ /**
587
+ * 销毁菜单
588
+ */
589
+ destroy() {
590
+ this.outsideClickHandler && (document.removeEventListener("click", this.outsideClickHandler), this.outsideClickHandler = null), this.menuEl && this.menuEl.parentNode && this.menuEl.parentNode.removeChild(this.menuEl), this.menuEl = null;
591
+ }
592
+ // ============================================================
593
+ // 右键处理
594
+ // ============================================================
595
+ /**
596
+ * 处理右键事件
597
+ */
598
+ handleContextMenu(e) {
599
+ const t = e.target, n = this.resolveContext(t, e);
600
+ this.currentContext = n;
601
+ const o = this.items.filter((s) => {
602
+ const i = Array.isArray(s.target) ? s.target : [s.target];
603
+ return i.includes(n.type) || i.includes("all");
604
+ });
605
+ o.length !== 0 && this.renderMenu(o, e.clientX, e.clientY);
606
+ }
607
+ /**
608
+ * 解析右键目标的上下文
609
+ */
610
+ resolveContext(e, t) {
611
+ const n = e.closest(".lf-node");
612
+ if (n)
613
+ return {
614
+ type: "node",
615
+ id: n.getAttribute("data-node-id") || void 0,
616
+ position: { x: t.clientX, y: t.clientY }
617
+ };
618
+ const o = e.closest(".lf-edge");
619
+ return o ? {
620
+ type: "edge",
621
+ id: o.getAttribute("data-edge-id") || void 0,
622
+ position: { x: t.clientX, y: t.clientY }
623
+ } : {
624
+ type: "canvas",
625
+ position: { x: t.clientX, y: t.clientY }
626
+ };
627
+ }
628
+ // ============================================================
629
+ // 菜单渲染
630
+ // ============================================================
631
+ /**
632
+ * 渲染并显示菜单
633
+ */
634
+ renderMenu(e, t, n) {
635
+ if (this.menuEl) {
636
+ this.menuEl.innerHTML = "";
637
+ for (const o of e) {
638
+ const s = document.createElement("div");
639
+ s.className = "rulego-context-menu__item";
640
+ const i = typeof o.disabled == "function" ? o.disabled() : o.disabled;
641
+ if (i && s.classList.add("rulego-context-menu__item--disabled"), o.icon) {
642
+ const r = document.createElement("span");
643
+ r.className = "rulego-context-menu__icon", r.innerHTML = o.icon, s.appendChild(r);
644
+ }
645
+ const l = document.createElement("span");
646
+ if (l.className = "rulego-context-menu__label", l.textContent = o.label, s.appendChild(l), i || s.addEventListener("click", () => {
647
+ this.currentContext && o.onClick(this.currentContext), this.hide();
648
+ }), this.menuEl.appendChild(s), o.separator) {
649
+ const r = document.createElement("div");
650
+ r.className = "rulego-context-menu__separator", this.menuEl.appendChild(r);
651
+ }
652
+ }
653
+ this.menuEl.style.left = `${t}px`, this.menuEl.style.top = `${n}px`, this.menuEl.style.display = "block", requestAnimationFrame(() => {
654
+ if (!this.menuEl) return;
655
+ const o = this.menuEl.getBoundingClientRect();
656
+ o.right > window.innerWidth && (this.menuEl.style.left = `${t - o.width}px`), o.bottom > window.innerHeight && (this.menuEl.style.top = `${n - o.height}px`);
657
+ });
658
+ }
659
+ }
660
+ /**
661
+ * 隐藏菜单
662
+ */
663
+ hide() {
664
+ this.menuEl && (this.menuEl.style.display = "none"), this.currentContext = null;
665
+ }
666
+ // ============================================================
667
+ // 内置菜单项
668
+ // ============================================================
669
+ /**
670
+ * 获取内置菜单项
671
+ */
672
+ getBuiltinItems() {
673
+ const e = this.core.i18n;
674
+ return [
675
+ {
676
+ id: "edit",
677
+ label: e.t("contextMenu.edit") || "编辑",
678
+ icon: "✏️",
679
+ target: "node",
680
+ onClick: (t) => {
681
+ t.id && this.core.eventBus.emit("editor:show-edit-panel");
682
+ }
683
+ },
684
+ {
685
+ id: "delete",
686
+ label: e.t("contextMenu.delete") || "删除",
687
+ icon: "🗑️",
688
+ target: ["node", "edge"],
689
+ separator: !0,
690
+ onClick: (t) => {
691
+ t.id && this.core.eventBus.emit("node:delete", { id: t.id });
692
+ }
693
+ },
694
+ {
695
+ id: "copy",
696
+ label: e.t("contextMenu.copy") || "复制",
697
+ icon: "📋",
698
+ target: "node",
699
+ onClick: () => {
700
+ }
701
+ },
702
+ {
703
+ id: "select-all",
704
+ label: e.t("contextMenu.selectAll") || "全选",
705
+ icon: "⬜",
706
+ target: "canvas",
707
+ onClick: () => {
708
+ }
709
+ }
710
+ ];
711
+ }
712
+ // ============================================================
713
+ // 公共 API
714
+ // ============================================================
715
+ /**
716
+ * 动态添加菜单项
717
+ * @param item - 菜单项定义
718
+ */
719
+ addItem(e) {
720
+ this.items.push(e);
721
+ }
722
+ /**
723
+ * 移除菜单项
724
+ * @param id - 菜单项 ID
725
+ */
726
+ removeItem(e) {
727
+ this.items = this.items.filter((t) => t.id !== e);
728
+ }
729
+ /**
730
+ * 获取菜单 DOM 元素
731
+ */
732
+ getElement() {
733
+ return this.menuEl;
734
+ }
735
+ }
736
+ export {
737
+ m as ContextMenu,
738
+ p as PropertyPanel,
739
+ u as Sidebar,
740
+ h as Toolbar
741
+ };
742
+ //# sourceMappingURL=index.esm.js.map