@tmagic/editor 1.8.0-beta.26 → 1.8.0-beta.28

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.
@@ -1,11 +1,12 @@
1
1
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
2
2
  /**
3
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
3
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
4
4
  *
5
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
5
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
6
6
  */
7
7
  var stickyAddButton = (text) => ({
8
8
  scrollLastItemIntoView: true,
9
+ header: { sticky: true },
9
10
  addButtonConfig: {
10
11
  sticky: true,
11
12
  text,
@@ -1,4 +1,5 @@
1
- import { computed } from "vue";
1
+ import { FORM_CONTEXT_KEY, mergeFormContexts } from "@tmagic/form";
2
+ import { computed, inject, unref } from "vue";
2
3
  //#region packages/editor/src/hooks/use-form-context.ts
3
4
  /**
4
5
  * 编辑器注入给表单的业务上下文:`services` 与当前画布 `stage`。
@@ -6,14 +7,23 @@ import { computed } from "vue";
6
7
  * 由 `Editor.vue`(provide `FORM_CONTEXT_KEY`)、`FormPanel.vue` 与 `useCompareForm`
7
8
  * 共用。`stage` 走 computed 而非快照,保证切换画布后配置回调读到的是最新实例。
8
9
  *
10
+ * 宿主可能在 `<MEditor>` 外层 provide 了自己的业务字段,这里必须把那一层合并进来:
11
+ * 否则编辑器再 provide 一次会把宿主整层遮蔽掉,属性面板、对比表单里的配置回调就
12
+ * 读不到宿主字段了。`services` / `stage` 由编辑器兜底,优先级高于宿主同名字段。
13
+ *
9
14
  * 字段类型见 `@editor/type` 里对 `@tmagic/form-schema` 的 `FormContext` 模块增强。
10
15
  */
11
- var useEditorFormContext = (getServices) => computed(() => {
12
- const services = getServices();
13
- return {
14
- services,
15
- stage: services?.editorService.get("stage")
16
- };
17
- });
16
+ var useEditorFormContext = (getServices) => {
17
+ const hostContext = inject(FORM_CONTEXT_KEY, void 0);
18
+ return computed(() => {
19
+ const services = getServices();
20
+ const host = unref(hostContext);
21
+ if (host && host.services === services) return host;
22
+ return mergeFormContexts(host, {
23
+ services,
24
+ stage: services?.editorService.get("stage")
25
+ });
26
+ });
27
+ };
18
28
  //#endregion
19
29
  export { useEditorFormContext };
@@ -73,6 +73,7 @@ var initServiceState = (props, { editorService, historyService, componentListSer
73
73
  uiService.resetState();
74
74
  componentListService.resetState();
75
75
  codeBlockService.resetState();
76
+ eventsService.resetState();
76
77
  keybindingService.reset();
77
78
  depService.reset();
78
79
  });
@@ -167,10 +168,20 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
167
168
  });
168
169
  });
169
170
  };
171
+ /**
172
+ * root 是否已经不是编辑器当前的 root。
173
+ *
174
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
175
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
176
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
177
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
178
+ */
179
+ const isStaleRoot = (value) => toRaw(editorService.get("root")) !== toRaw(value);
170
180
  const updateStageDsl = async (value) => {
171
181
  const stage = await getStage();
172
182
  const runtime = await stage.renderer?.getRuntime();
173
183
  const app = await getTMagicApp();
184
+ if (isStaleRoot(value)) return;
174
185
  if (!app?.dataSourceManager) runtime?.updateRootConfig?.(cloneDeep$1(toRaw(value)));
175
186
  const page = editorService.get("page");
176
187
  const node = editorService.get("node");
@@ -181,6 +192,7 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
181
192
  if (value) {
182
193
  depService.clearIdleTasks();
183
194
  await (typeof Worker === "undefined" ? collectIdle(value.items, true) : depService.collectByWorker(value));
195
+ if (isStaleRoot(value)) return;
184
196
  const dsl = cloneDeep$1(toRaw(value));
185
197
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) updateNode(app.dataSourceManager.compiledNode(node), dsl);
186
198
  runtime?.updateRootConfig?.(dsl);
@@ -191,7 +203,7 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
191
203
  depService.addTarget(createDataSourceMethodTarget(ds, reactive({})));
192
204
  depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
193
205
  };
194
- const rootChangeHandler = (value, preValue) => {
206
+ const rootChangeHandler = (value) => {
195
207
  if (!value) return;
196
208
  value.codeBlocks = value.codeBlocks || {};
197
209
  value.dataSources = value.dataSources || [];
@@ -217,7 +229,8 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
217
229
  editorService.set("parent", null);
218
230
  editorService.set("page", null);
219
231
  }
220
- if (toRaw(value) !== toRaw(preValue)) emit("update:modelValue", value);
232
+ if (isStaleRoot(value)) return;
233
+ if (toRaw(props.modelValue) !== toRaw(value)) emit("update:modelValue", value);
221
234
  })();
222
235
  };
223
236
  const nodeAddHandler = (nodes) => {
@@ -135,7 +135,7 @@ var useHistoryRevert = (options = {}, services) => {
135
135
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
136
136
  * 用户取消时返回 false,调用方据此中止回滚。
137
137
  */
138
- const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。");
138
+ const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?");
139
139
  /**
140
140
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,
141
141
  * 无法对比的步骤(add / remove / 多节点更新)回退到普通二次确认框。
@@ -47,8 +47,8 @@ var Events = class extends BaseService {
47
47
  return cloneDeep(methodMap[toLine(type)]) || [];
48
48
  }
49
49
  resetState() {
50
- eventMap = reactive({});
51
- methodMap = reactive({});
50
+ Object.keys(eventMap).forEach((type) => delete eventMap[type]);
51
+ Object.keys(methodMap).forEach((type) => delete methodMap[type]);
52
52
  }
53
53
  destroy() {
54
54
  this.resetState();
package/dist/es/style.css CHANGED
@@ -478,18 +478,48 @@ fieldset.m-fieldset .m-form-tip {
478
478
  flex-shrink: 0;
479
479
  gap: 8px;
480
480
  }
481
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
482
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
481
+ .m-fields-group-list > .m-fields-group-list-item {
482
+ --m-group-list-item-footer-bottom: var(
483
+ --m-group-list-nested-footer-bottom,
484
+ 0px
485
+ );
486
+ }
487
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
488
+ --m-group-list-item-footer-bottom: calc(
489
+ var(--m-group-list-nested-footer-bottom, 0px) +
490
+ var(--m-group-list-footer-height)
491
+ );
492
+ }
493
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
494
+ --m-group-list-nested-footer-bottom: var(
495
+ --m-group-list-item-footer-bottom,
496
+ 0px
497
+ );
498
+ }
499
+ .m-fields-group-list.is-header-sticky {
500
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
501
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
502
+ }
503
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
504
+ --m-group-list-item-sticky-top: var(
505
+ --m-group-list-nested-sticky-top,
506
+ var(--m-group-list-header-sticky-top, 0px)
507
+ );
508
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
509
+ --m-group-list-child-list-sticky-top: calc(
510
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
511
+ );
512
+ --m-group-list-child-list-header-z: calc(
513
+ var(--m-group-list-item-header-z) - 1
514
+ );
515
+ }
516
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
517
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
483
518
  position: sticky;
484
- top: var(--m-group-list-header-sticky-top, 0px);
485
- z-index: 7;
519
+ top: var(--m-group-list-item-sticky-top);
520
+ z-index: var(--m-group-list-item-header-z);
486
521
  background-color: var(--m-group-list-header-bg, #fff);
487
522
  }
488
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
489
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
490
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
491
- z-index: 6;
492
- }
493
523
  .m-fields-group-list .el-table__empty-block {
494
524
  width: 100%;
495
525
  min-height: 0;
@@ -507,7 +537,7 @@ fieldset.m-fieldset .m-form-tip {
507
537
  }
508
538
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
509
539
  position: sticky;
510
- bottom: 0;
540
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
511
541
  z-index: 7;
512
542
  margin-bottom: 0;
513
543
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -521,10 +551,6 @@ fieldset.m-fieldset .m-form-tip {
521
551
  width: 100%;
522
552
  height: var(--m-group-list-footer-button-height);
523
553
  }
524
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
525
- bottom: var(--m-group-list-footer-height);
526
- z-index: 7;
527
- }
528
554
 
529
555
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
530
556
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
@@ -55,13 +55,20 @@ var getCodeBlockFormConfig = (options = {}) => {
55
55
  name: "timing",
56
56
  type: "select",
57
57
  options: () => {
58
- const list = [{
59
- text: "初始化前",
60
- value: "beforeInit"
61
- }, {
62
- text: "初始化后",
63
- value: "afterInit"
64
- }];
58
+ const list = [
59
+ {
60
+ text: "初始化前",
61
+ value: "beforeInit"
62
+ },
63
+ {
64
+ text: "初始化后",
65
+ value: "afterInit"
66
+ },
67
+ {
68
+ text: "页面渲染后",
69
+ value: "mounted"
70
+ }
71
+ ];
65
72
  if (dataSourceType?.() !== "base") {
66
73
  list.push({
67
74
  text: "请求前",
package/dist/style.css CHANGED
@@ -478,18 +478,48 @@ fieldset.m-fieldset .m-form-tip {
478
478
  flex-shrink: 0;
479
479
  gap: 8px;
480
480
  }
481
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
482
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
481
+ .m-fields-group-list > .m-fields-group-list-item {
482
+ --m-group-list-item-footer-bottom: var(
483
+ --m-group-list-nested-footer-bottom,
484
+ 0px
485
+ );
486
+ }
487
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
488
+ --m-group-list-item-footer-bottom: calc(
489
+ var(--m-group-list-nested-footer-bottom, 0px) +
490
+ var(--m-group-list-footer-height)
491
+ );
492
+ }
493
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
494
+ --m-group-list-nested-footer-bottom: var(
495
+ --m-group-list-item-footer-bottom,
496
+ 0px
497
+ );
498
+ }
499
+ .m-fields-group-list.is-header-sticky {
500
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
501
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
502
+ }
503
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
504
+ --m-group-list-item-sticky-top: var(
505
+ --m-group-list-nested-sticky-top,
506
+ var(--m-group-list-header-sticky-top, 0px)
507
+ );
508
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
509
+ --m-group-list-child-list-sticky-top: calc(
510
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
511
+ );
512
+ --m-group-list-child-list-header-z: calc(
513
+ var(--m-group-list-item-header-z) - 1
514
+ );
515
+ }
516
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
517
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
483
518
  position: sticky;
484
- top: var(--m-group-list-header-sticky-top, 0px);
485
- z-index: 7;
519
+ top: var(--m-group-list-item-sticky-top);
520
+ z-index: var(--m-group-list-item-header-z);
486
521
  background-color: var(--m-group-list-header-bg, #fff);
487
522
  }
488
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
489
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
490
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
491
- z-index: 6;
492
- }
493
523
  .m-fields-group-list .el-table__empty-block {
494
524
  width: 100%;
495
525
  min-height: 0;
@@ -507,7 +537,7 @@ fieldset.m-fieldset .m-form-tip {
507
537
  }
508
538
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
509
539
  position: sticky;
510
- bottom: 0;
540
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
511
541
  z-index: 7;
512
542
  margin-bottom: 0;
513
543
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -521,10 +551,6 @@ fieldset.m-fieldset .m-form-tip {
521
551
  width: 100%;
522
552
  height: var(--m-group-list-footer-button-height);
523
553
  }
524
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
525
- bottom: var(--m-group-list-footer-height);
526
- z-index: 7;
527
- }
528
554
 
529
555
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
530
556
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
@@ -509,18 +509,48 @@ fieldset.m-fieldset .m-form-tip {
509
509
  flex-shrink: 0;
510
510
  gap: 8px;
511
511
  }
512
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
513
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
512
+ .m-fields-group-list > .m-fields-group-list-item {
513
+ --m-group-list-item-footer-bottom: var(
514
+ --m-group-list-nested-footer-bottom,
515
+ 0px
516
+ );
517
+ }
518
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
519
+ --m-group-list-item-footer-bottom: calc(
520
+ var(--m-group-list-nested-footer-bottom, 0px) +
521
+ var(--m-group-list-footer-height)
522
+ );
523
+ }
524
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
525
+ --m-group-list-nested-footer-bottom: var(
526
+ --m-group-list-item-footer-bottom,
527
+ 0px
528
+ );
529
+ }
530
+ .m-fields-group-list.is-header-sticky {
531
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
532
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
533
+ }
534
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
535
+ --m-group-list-item-sticky-top: var(
536
+ --m-group-list-nested-sticky-top,
537
+ var(--m-group-list-header-sticky-top, 0px)
538
+ );
539
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
540
+ --m-group-list-child-list-sticky-top: calc(
541
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
542
+ );
543
+ --m-group-list-child-list-header-z: calc(
544
+ var(--m-group-list-item-header-z) - 1
545
+ );
546
+ }
547
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
548
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
514
549
  position: sticky;
515
- top: var(--m-group-list-header-sticky-top, 0px);
516
- z-index: 7;
550
+ top: var(--m-group-list-item-sticky-top);
551
+ z-index: var(--m-group-list-item-header-z);
517
552
  background-color: var(--m-group-list-header-bg, #fff);
518
553
  }
519
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
520
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
521
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
522
- z-index: 6;
523
- }
524
554
  .m-fields-group-list .el-table__empty-block {
525
555
  width: 100%;
526
556
  min-height: 0;
@@ -538,7 +568,7 @@ fieldset.m-fieldset .m-form-tip {
538
568
  }
539
569
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
540
570
  position: sticky;
541
- bottom: 0;
571
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
542
572
  z-index: 7;
543
573
  margin-bottom: 0;
544
574
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -552,10 +582,6 @@ fieldset.m-fieldset .m-form-tip {
552
582
  width: 100%;
553
583
  height: var(--m-group-list-footer-button-height);
554
584
  }
555
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
556
- bottom: var(--m-group-list-footer-height);
557
- z-index: 7;
558
- }
559
585
 
560
586
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
561
587
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
@@ -8813,12 +8813,13 @@
8813
8813
  //#endregion
8814
8814
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
8815
8815
  /**
8816
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
8816
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
8817
8817
  *
8818
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
8818
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
8819
8819
  */
8820
8820
  var stickyAddButton = (text) => ({
8821
8821
  scrollLastItemIntoView: true,
8822
+ header: { sticky: true },
8822
8823
  addButtonConfig: {
8823
8824
  sticky: true,
8824
8825
  text,
@@ -9120,8 +9121,8 @@
9120
9121
  return cloneDeep(methodMap[(0, _tmagic_utils.toLine)(type)]) || [];
9121
9122
  }
9122
9123
  resetState() {
9123
- eventMap = (0, vue.reactive)({});
9124
- methodMap = (0, vue.reactive)({});
9124
+ Object.keys(eventMap).forEach((type) => delete eventMap[type]);
9125
+ Object.keys(methodMap).forEach((type) => delete methodMap[type]);
9125
9126
  }
9126
9127
  destroy() {
9127
9128
  this.resetState();
@@ -5057,13 +5057,18 @@
5057
5057
  //#region packages/editor/src/hooks/use-form-context.ts
5058
5058
  var useEditorFormContext;
5059
5059
  var init_use_form_context = __esmMin((() => {
5060
- useEditorFormContext = (getServices) => (0, vue.computed)(() => {
5061
- const services = getServices();
5062
- return {
5063
- services,
5064
- stage: services?.editorService.get("stage")
5065
- };
5066
- });
5060
+ useEditorFormContext = (getServices) => {
5061
+ const hostContext = (0, vue.inject)(_tmagic_form.FORM_CONTEXT_KEY, void 0);
5062
+ return (0, vue.computed)(() => {
5063
+ const services = getServices();
5064
+ const host = (0, vue.unref)(hostContext);
5065
+ if (host && host.services === services) return host;
5066
+ return (0, _tmagic_form.mergeFormContexts)(host, {
5067
+ services,
5068
+ stage: services?.editorService.get("stage")
5069
+ });
5070
+ });
5071
+ };
5067
5072
  }));
5068
5073
  //#endregion
5069
5074
  //#region packages/editor/src/utils/config.ts
@@ -5130,13 +5135,20 @@
5130
5135
  name: "timing",
5131
5136
  type: "select",
5132
5137
  options: () => {
5133
- const list = [{
5134
- text: "初始化前",
5135
- value: "beforeInit"
5136
- }, {
5137
- text: "初始化后",
5138
- value: "afterInit"
5139
- }];
5138
+ const list = [
5139
+ {
5140
+ text: "初始化前",
5141
+ value: "beforeInit"
5142
+ },
5143
+ {
5144
+ text: "初始化后",
5145
+ value: "afterInit"
5146
+ },
5147
+ {
5148
+ text: "页面渲染后",
5149
+ value: "mounted"
5150
+ }
5151
+ ];
5140
5152
  if (dataSourceType?.() !== "base") {
5141
5153
  list.push({
5142
5154
  text: "请求前",
@@ -11208,8 +11220,8 @@
11208
11220
  return cloneDeep$1(methodMap[(0, _tmagic_utils.toLine)(type)]) || [];
11209
11221
  }
11210
11222
  resetState() {
11211
- eventMap = (0, vue.reactive)({});
11212
- methodMap = (0, vue.reactive)({});
11223
+ Object.keys(eventMap).forEach((type) => delete eventMap[type]);
11224
+ Object.keys(methodMap).forEach((type) => delete methodMap[type]);
11213
11225
  }
11214
11226
  destroy() {
11215
11227
  this.resetState();
@@ -13929,7 +13941,7 @@
13929
13941
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
13930
13942
  * 用户取消时返回 false,调用方据此中止回滚。
13931
13943
  */
13932
- const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。");
13944
+ const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?");
13933
13945
  /**
13934
13946
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,
13935
13947
  * 无法对比的步骤(add / remove / 多节点更新)回退到普通二次确认框。
@@ -20074,6 +20086,7 @@
20074
20086
  uiService.resetState();
20075
20087
  componentListService.resetState();
20076
20088
  codeBlockService.resetState();
20089
+ eventsService.resetState();
20077
20090
  keybindingService.reset();
20078
20091
  depService.reset();
20079
20092
  });
@@ -20168,10 +20181,20 @@
20168
20181
  });
20169
20182
  });
20170
20183
  };
20184
+ /**
20185
+ * root 是否已经不是编辑器当前的 root。
20186
+ *
20187
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
20188
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
20189
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
20190
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
20191
+ */
20192
+ const isStaleRoot = (value) => (0, vue.toRaw)(editorService.get("root")) !== (0, vue.toRaw)(value);
20171
20193
  const updateStageDsl = async (value) => {
20172
20194
  const stage = await getStage();
20173
20195
  const runtime = await stage.renderer?.getRuntime();
20174
20196
  const app = await getTMagicApp();
20197
+ if (isStaleRoot(value)) return;
20175
20198
  if (!app?.dataSourceManager) runtime?.updateRootConfig?.(cloneDeep$1((0, vue.toRaw)(value)));
20176
20199
  const page = editorService.get("page");
20177
20200
  const node = editorService.get("node");
@@ -20182,6 +20205,7 @@
20182
20205
  if (value) {
20183
20206
  depService.clearIdleTasks();
20184
20207
  await (typeof Worker === "undefined" ? collectIdle(value.items, true) : depService.collectByWorker(value));
20208
+ if (isStaleRoot(value)) return;
20185
20209
  const dsl = cloneDeep$1((0, vue.toRaw)(value));
20186
20210
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) for (const node of (0, _tmagic_utils.getNodes)((0, _tmagic_utils.getDepNodeIds)(dsl.dataSourceDeps), dsl.items)) (0, _tmagic_core.updateNode)(app.dataSourceManager.compiledNode(node), dsl);
20187
20211
  runtime?.updateRootConfig?.(dsl);
@@ -20192,7 +20216,7 @@
20192
20216
  depService.addTarget((0, _tmagic_core.createDataSourceMethodTarget)(ds, (0, vue.reactive)({})));
20193
20217
  depService.addTarget((0, _tmagic_core.createDataSourceCondTarget)(ds, (0, vue.reactive)({})));
20194
20218
  };
20195
- const rootChangeHandler = (value, preValue) => {
20219
+ const rootChangeHandler = (value) => {
20196
20220
  if (!value) return;
20197
20221
  value.codeBlocks = value.codeBlocks || {};
20198
20222
  value.dataSources = value.dataSources || [];
@@ -20218,7 +20242,8 @@
20218
20242
  editorService.set("parent", null);
20219
20243
  editorService.set("page", null);
20220
20244
  }
20221
- if ((0, vue.toRaw)(value) !== (0, vue.toRaw)(preValue)) emit("update:modelValue", value);
20245
+ if (isStaleRoot(value)) return;
20246
+ if ((0, vue.toRaw)(props.modelValue) !== (0, vue.toRaw)(value)) emit("update:modelValue", value);
20222
20247
  })();
20223
20248
  };
20224
20249
  const nodeAddHandler = (nodes) => {
@@ -20667,12 +20692,13 @@
20667
20692
  //#endregion
20668
20693
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
20669
20694
  /**
20670
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
20695
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
20671
20696
  *
20672
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
20697
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
20673
20698
  */
20674
20699
  var stickyAddButton = (text) => ({
20675
20700
  scrollLastItemIntoView: true,
20701
+ header: { sticky: true },
20676
20702
  addButtonConfig: {
20677
20703
  sticky: true,
20678
20704
  text,
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.26",
2
+ "version": "1.8.0-beta.28",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -63,11 +63,11 @@
63
63
  "moveable": "^0.53.0",
64
64
  "serialize-javascript": "^7.0.7",
65
65
  "sortablejs": "^1.15.7",
66
- "@tmagic/design": "1.8.0-beta.26",
67
- "@tmagic/form": "1.8.0-beta.26",
68
- "@tmagic/stage": "1.8.0-beta.26",
69
- "@tmagic/table": "1.8.0-beta.26",
70
- "@tmagic/utils": "1.8.0-beta.26"
66
+ "@tmagic/design": "1.8.0-beta.28",
67
+ "@tmagic/stage": "1.8.0-beta.28",
68
+ "@tmagic/utils": "1.8.0-beta.28",
69
+ "@tmagic/form": "1.8.0-beta.28",
70
+ "@tmagic/table": "1.8.0-beta.28"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/events": "^3.0.3",
@@ -81,7 +81,7 @@
81
81
  "type-fest": "^5.2.0",
82
82
  "typescript": "^6.0.3",
83
83
  "vue": "^3.5.41",
84
- "@tmagic/core": "1.8.0-beta.26"
84
+ "@tmagic/core": "1.8.0-beta.28"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "typescript": {
@@ -17,12 +17,13 @@
17
17
  */
18
18
 
19
19
  /**
20
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
20
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
21
21
  *
22
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
22
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
23
23
  */
24
24
  export const stickyAddButton = (text: string) => ({
25
25
  scrollLastItemIntoView: true as const,
26
+ header: { sticky: true as const },
26
27
  addButtonConfig: {
27
28
  sticky: true as const,
28
29
  text,
@@ -16,9 +16,9 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { computed, type ComputedRef } from 'vue';
19
+ import { computed, type ComputedRef, inject, unref } from 'vue';
20
20
 
21
- import type { FormContext } from '@tmagic/form';
21
+ import { FORM_CONTEXT_KEY, type FormContext, mergeFormContexts } from '@tmagic/form';
22
22
 
23
23
  import type { Services } from '@editor/type';
24
24
 
@@ -28,13 +28,29 @@ import type { Services } from '@editor/type';
28
28
  * 由 `Editor.vue`(provide `FORM_CONTEXT_KEY`)、`FormPanel.vue` 与 `useCompareForm`
29
29
  * 共用。`stage` 走 computed 而非快照,保证切换画布后配置回调读到的是最新实例。
30
30
  *
31
+ * 宿主可能在 `<MEditor>` 外层 provide 了自己的业务字段,这里必须把那一层合并进来:
32
+ * 否则编辑器再 provide 一次会把宿主整层遮蔽掉,属性面板、对比表单里的配置回调就
33
+ * 读不到宿主字段了。`services` / `stage` 由编辑器兜底,优先级高于宿主同名字段。
34
+ *
31
35
  * 字段类型见 `@editor/type` 里对 `@tmagic/form-schema` 的 `FormContext` 模块增强。
32
36
  */
33
- export const useEditorFormContext = (getServices: () => Services | undefined): ComputedRef<FormContext> =>
34
- computed(() => {
37
+ export const useEditorFormContext = (getServices: () => Services | undefined): ComputedRef<FormContext> => {
38
+ // inject 只能在 setup 期取值,因此本 hook 必须在 setup 中调用(`use` 前缀即此约定)。
39
+ // 返回的 computed 可以随便传递,但 hook 本身不能延迟到事件回调或 onMounted 里再调。
40
+ const hostContext = inject(FORM_CONTEXT_KEY, undefined);
41
+
42
+ return computed(() => {
35
43
  const services = getServices();
36
- return {
44
+ const host = unref(hostContext);
45
+
46
+ // 上层(通常是 Editor.vue)已经用同一份 services 合并过了。FormPanel / useCompareForm
47
+ // 都是它的后代,再合并一次只会多套一层 Proxy,让每次属性 miss 多一轮线性查找。
48
+ // services 相同即可判定 stage 也相同——两边都是从同一个 editorService 读的。
49
+ if (host && host.services === services) return host;
50
+
51
+ return mergeFormContexts(host, {
37
52
  services,
38
53
  stage: services?.editorService.get('stage'),
39
- };
54
+ });
40
55
  });
56
+ };
@@ -254,6 +254,7 @@ export const initServiceState = (
254
254
  uiService.resetState();
255
255
  componentListService.resetState();
256
256
  codeBlockService.resetState();
257
+ eventsService.resetState();
257
258
  keybindingService.reset();
258
259
  depService.reset();
259
260
  });
@@ -426,12 +427,26 @@ export const initServiceEvents = (
426
427
  });
427
428
  };
428
429
 
430
+ /**
431
+ * root 是否已经不是编辑器当前的 root。
432
+ *
433
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
434
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
435
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
436
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
437
+ */
438
+ const isStaleRoot = (value: MApp | null) => toRaw(editorService.get('root')) !== toRaw(value);
439
+
429
440
  const updateStageDsl = async (value: MApp | null) => {
430
441
  const stage = await getStage();
431
442
 
432
443
  const runtime = await stage.renderer?.getRuntime();
433
444
  const app = await getTMagicApp();
434
445
 
446
+ // 等 stage / runtime 就绪期间 root 已被替换:新 root 的刷新可能已经完成,
447
+ // 再把旧 dsl 推给 runtime 会让画布回退到旧内容
448
+ if (isStaleRoot(value)) return;
449
+
435
450
  if (!app?.dataSourceManager) {
436
451
  runtime?.updateRootConfig?.(cloneDeep(toRaw(value))!);
437
452
  }
@@ -448,6 +463,8 @@ export const initServiceEvents = (
448
463
 
449
464
  await (typeof Worker === 'undefined' ? collectIdle(value.items, true) : depService.collectByWorker(value));
450
465
 
466
+ if (isStaleRoot(value)) return;
467
+
451
468
  const dsl = cloneDeep(toRaw(value));
452
469
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) {
453
470
  for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) {
@@ -465,7 +482,7 @@ export const initServiceEvents = (
465
482
  depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
466
483
  };
467
484
 
468
- const rootChangeHandler = (value: MApp | null, preValue?: MApp | null) => {
485
+ const rootChangeHandler = (value: MApp | null) => {
469
486
  if (!value) return;
470
487
 
471
488
  value.codeBlocks = value.codeBlocks || {};
@@ -508,7 +525,13 @@ export const initServiceEvents = (
508
525
  editorService.set('page', null);
509
526
  }
510
527
 
511
- if (toRaw(value) !== toRaw(preValue)) {
528
+ // 上面的 select 是异步的,期间 root 可能已被替换,过期快照不能再回写给外部:
529
+ // 两次整体替换各自持有一个快照时,回写会把对方的 root 顶掉,外部 modelValue 变化又会
530
+ // 重新 set root,两条链路无休止地交替下去(表现为编辑器卡死)
531
+ if (isStaleRoot(value)) return;
532
+
533
+ // 外部已经持有这个 root(如本次变化就是 modelValue 传进来的)时无需回写
534
+ if (toRaw(props.modelValue) !== toRaw(value)) {
512
535
  emit('update:modelValue', value);
513
536
  }
514
537
  })();
@@ -204,10 +204,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
204
204
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
205
205
  * 用户取消时返回 false,调用方据此中止回滚。
206
206
  */
207
- const confirmRevert = (): Promise<boolean> =>
208
- confirmHistoryAction(
209
- '确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。',
210
- );
207
+ const confirmRevert = (): Promise<boolean> => confirmHistoryAction('确定回滚该步骤吗?');
211
208
 
212
209
  /**
213
210
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,
@@ -35,8 +35,8 @@ const canUsePluginMethods = {
35
35
  type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
36
36
  type SyncMethodName = Writable<(typeof canUsePluginMethods)['sync']>;
37
37
 
38
- let eventMap: Record<string, EventOption[]> = reactive({});
39
- let methodMap: Record<string, EventOption[]> = reactive({});
38
+ const eventMap: Record<string, EventOption[]> = reactive({});
39
+ const methodMap: Record<string, EventOption[]> = reactive({});
40
40
 
41
41
  class Events extends BaseService {
42
42
  constructor() {
@@ -75,8 +75,9 @@ class Events extends BaseService {
75
75
  }
76
76
 
77
77
  public resetState() {
78
- eventMap = reactive({});
79
- methodMap = reactive({});
78
+ // 原地清空而不是重新赋值,保留 reactive 代理身份,已建立的 computed / 渲染副作用才能收到更新
79
+ Object.keys(eventMap).forEach((type) => delete eventMap[type]);
80
+ Object.keys(methodMap).forEach((type) => delete methodMap[type]);
80
81
  }
81
82
 
82
83
  public destroy() {
package/src/type.ts CHANGED
@@ -225,6 +225,7 @@ export interface StageOptions {
225
225
  beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
226
226
  }
227
227
 
228
+ // #region NodeInvalidInfo
228
229
  /**
229
230
  * 节点校验错误信息,按来源(属性表单 / 样式表单)分别保存错误文案。
230
231
  * 属性表单与样式表单是两个独立的 FormPanel,均指向同一节点,故以来源为键,
@@ -237,9 +238,12 @@ export interface NodeInvalidInfo {
237
238
  /** 样式表单校验错误文案(可能为包含 <br> 的 HTML) */
238
239
  style?: string;
239
240
  }
241
+ // #endregion NodeInvalidInfo
240
242
 
243
+ // #region NodeInvalidSource
241
244
  /** 节点校验错误来源 */
242
245
  export type NodeInvalidSource = keyof NodeInvalidInfo;
246
+ // #endregion NodeInvalidSource
243
247
 
244
248
  export interface StoreState {
245
249
  root: MApp | null;
@@ -312,6 +316,7 @@ export interface StageRect {
312
316
  height: number | string;
313
317
  }
314
318
 
319
+ // #region UiState
315
320
  export interface UiState {
316
321
  /** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
317
322
  uiSelectMode: boolean;
@@ -369,6 +374,7 @@ export interface UiState {
369
374
  height: number;
370
375
  };
371
376
  }
377
+ // #endregion UiState
372
378
 
373
379
  // #region EditorNodeInfo
374
380
  export interface EditorNodeInfo {
@@ -103,6 +103,7 @@ export const getCodeBlockFormConfig = (options: GetCodeBlockFormConfigOptions =
103
103
  const list = [
104
104
  { text: '初始化前', value: 'beforeInit' },
105
105
  { text: '初始化后', value: 'afterInit' },
106
+ { text: '页面渲染后', value: 'mounted' },
106
107
  ];
107
108
  if (dataSourceType?.() !== 'base') {
108
109
  list.push({ text: '请求前', value: 'beforeRequest' });