@tmagic/editor 1.8.0-beta.27 → 1.8.0-beta.29

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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.27",
2
+ "version": "1.8.0-beta.29",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -61,27 +61,27 @@
61
61
  "keycon": "^1.4.0",
62
62
  "lodash-es": "^4.18.1",
63
63
  "moveable": "^0.53.0",
64
- "serialize-javascript": "^7.0.7",
64
+ "serialize-javascript": "^7.1.1",
65
65
  "sortablejs": "^1.15.7",
66
- "@tmagic/design": "1.8.0-beta.27",
67
- "@tmagic/stage": "1.8.0-beta.27",
68
- "@tmagic/table": "1.8.0-beta.27",
69
- "@tmagic/form": "1.8.0-beta.27",
70
- "@tmagic/utils": "1.8.0-beta.27"
66
+ "@tmagic/design": "1.8.0-beta.29",
67
+ "@tmagic/form": "1.8.0-beta.29",
68
+ "@tmagic/stage": "1.8.0-beta.29",
69
+ "@tmagic/table": "1.8.0-beta.29",
70
+ "@tmagic/utils": "1.8.0-beta.29"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/events": "^3.0.3",
74
74
  "@types/lodash-es": "^4.17.12",
75
75
  "@types/serialize-javascript": "^5.0.4",
76
76
  "@types/sortablejs": "^1.15.9",
77
- "@vue/test-utils": "^2.4.11"
77
+ "@vue/test-utils": "^2.5.1"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "monaco-editor": "^0.55.1 ",
81
81
  "type-fest": "^5.2.0",
82
82
  "typescript": "^6.0.3",
83
- "vue": "^3.5.41",
84
- "@tmagic/core": "1.8.0-beta.27"
83
+ "vue": "^3.5.43",
84
+ "@tmagic/core": "1.8.0-beta.29"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "typescript": {
@@ -118,7 +118,7 @@ const emit = defineEmits<{
118
118
  change: [v: string[]];
119
119
  }>();
120
120
 
121
- const modelValue = defineModel<string[] | any>('modelValue', { default: [] });
121
+ const modelValue = defineModel<string[] | any>('modelValue', { default: () => [] });
122
122
 
123
123
  const optionComponent = getDesignConfig('components')?.option;
124
124
 
@@ -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,
@@ -427,12 +427,26 @@ export const initServiceEvents = (
427
427
  });
428
428
  };
429
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
+
430
440
  const updateStageDsl = async (value: MApp | null) => {
431
441
  const stage = await getStage();
432
442
 
433
443
  const runtime = await stage.renderer?.getRuntime();
434
444
  const app = await getTMagicApp();
435
445
 
446
+ // 等 stage / runtime 就绪期间 root 已被替换:新 root 的刷新可能已经完成,
447
+ // 再把旧 dsl 推给 runtime 会让画布回退到旧内容
448
+ if (isStaleRoot(value)) return;
449
+
436
450
  if (!app?.dataSourceManager) {
437
451
  runtime?.updateRootConfig?.(cloneDeep(toRaw(value))!);
438
452
  }
@@ -449,6 +463,8 @@ export const initServiceEvents = (
449
463
 
450
464
  await (typeof Worker === 'undefined' ? collectIdle(value.items, true) : depService.collectByWorker(value));
451
465
 
466
+ if (isStaleRoot(value)) return;
467
+
452
468
  const dsl = cloneDeep(toRaw(value));
453
469
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) {
454
470
  for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) {
@@ -466,7 +482,7 @@ export const initServiceEvents = (
466
482
  depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
467
483
  };
468
484
 
469
- const rootChangeHandler = (value: MApp | null, preValue?: MApp | null) => {
485
+ const rootChangeHandler = (value: MApp | null) => {
470
486
  if (!value) return;
471
487
 
472
488
  value.codeBlocks = value.codeBlocks || {};
@@ -509,7 +525,13 @@ export const initServiceEvents = (
509
525
  editorService.set('page', null);
510
526
  }
511
527
 
512
- 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)) {
513
535
  emit('update:modelValue', value);
514
536
  }
515
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 走差异确认弹窗,
@@ -56,7 +56,14 @@ import { calcValueByFontsize, getIdFromEl } from '@tmagic/utils';
56
56
  import ScrollViewer from '@editor/components/ScrollViewer.vue';
57
57
  import { useServices } from '@editor/hooks';
58
58
  import { useStage } from '@editor/hooks/use-stage';
59
- import type { CustomContentMenuFunction, MenuButton, MenuComponent, StageOptions, StageSlots } from '@editor/type';
59
+ import type {
60
+ AddMNode,
61
+ CustomContentMenuFunction,
62
+ MenuButton,
63
+ MenuComponent,
64
+ StageOptions,
65
+ StageSlots,
66
+ } from '@editor/type';
60
67
  import { DragType, Layout } from '@editor/type';
61
68
  import { getEditorConfig } from '@editor/utils/config';
62
69
  import { KeyBindingContainerKey } from '@editor/utils/keybinding-config';
@@ -270,11 +277,56 @@ const resizeObserver = new globalThis.ResizeObserver((entries) => {
270
277
  }
271
278
  });
272
279
 
280
+ const parseDSL = getEditorConfig('parseDSL');
281
+
282
+ /**
283
+ * 本次拖拽是否由编辑器文档内部发起
284
+ *
285
+ * drop 的 text/json 里可能带函数(组件配置的事件、钩子等),还原只能交给 parseDSL,
286
+ * 而 parseDSL 的默认实现是 eval;HTML 拖放又允许其他源的页面在 DataTransfer 中投递
287
+ * 自定义 MIME 数据,跨源页面只要诱导用户拖拽一次,就能让 eval 执行任意脚本。
288
+ *
289
+ * 跨源页面既不会在本文档触发 dragstart,也无法往本文档创建的 DataTransfer 中写数据,
290
+ * 因此只有起源于本文档的拖拽才交给 parseDSL 还原。
291
+ * 同源拖拽源写入的 text/json 由业务保证可信。
292
+ */
293
+ let isInternalDrag = false;
294
+ let internalDragSession = 0;
295
+ let clearInternalDragTimer: ReturnType<typeof setTimeout> | undefined;
296
+
297
+ const documentDragStartHandler = () => {
298
+ internalDragSession += 1;
299
+ isInternalDrag = true;
300
+ if (clearInternalDragTimer !== undefined) {
301
+ globalThis.clearTimeout(clearInternalDragTimer);
302
+ clearInternalDragTimer = undefined;
303
+ }
304
+ };
305
+
306
+ const documentDragEndHandler = () => {
307
+ // WebKit 可能先 dragend 再 drop;推迟到下一个宏任务再清标志,
308
+ // 让同一次拖拽的 drop 仍能消费。session 防止误清掉下一次 dragstart。
309
+ const session = internalDragSession;
310
+ if (clearInternalDragTimer !== undefined) {
311
+ globalThis.clearTimeout(clearInternalDragTimer);
312
+ }
313
+ clearInternalDragTimer = globalThis.setTimeout(() => {
314
+ clearInternalDragTimer = undefined;
315
+ if (session === internalDragSession) {
316
+ isInternalDrag = false;
317
+ }
318
+ }, 0);
319
+ };
320
+
273
321
  onMounted(() => {
274
322
  if (stageWrapRef.value?.container) {
275
323
  resizeObserver.observe(stageWrapRef.value.container);
276
324
  keybindingService.registerEl(KeyBindingContainerKey.STAGE, stageWrapRef.value.container);
277
325
  }
326
+
327
+ // 用捕获阶段监听,确保拖拽源自身的 dragstart 无论是否阻止冒泡都能被记录
328
+ globalThis.document.addEventListener('dragstart', documentDragStartHandler, true);
329
+ globalThis.document.addEventListener('dragend', documentDragEndHandler, true);
278
330
  });
279
331
 
280
332
  onBeforeUnmount(() => {
@@ -283,9 +335,15 @@ onBeforeUnmount(() => {
283
335
  resizeObserver.disconnect();
284
336
  editorService.set('stage', null);
285
337
  keybindingService.unregisterEl('stage');
286
- });
287
338
 
288
- const parseDSL = getEditorConfig('parseDSL');
339
+ if (clearInternalDragTimer !== undefined) {
340
+ globalThis.clearTimeout(clearInternalDragTimer);
341
+ clearInternalDragTimer = undefined;
342
+ }
343
+
344
+ globalThis.document.removeEventListener('dragstart', documentDragStartHandler, true);
345
+ globalThis.document.removeEventListener('dragend', documentDragEndHandler, true);
346
+ });
289
347
 
290
348
  const contextmenuHandler = (e: MouseEvent) => {
291
349
  e.preventDefault();
@@ -299,15 +357,27 @@ const dragoverHandler = (e: DragEvent) => {
299
357
  };
300
358
 
301
359
  const dropHandler = async (e: DragEvent) => {
360
+ // 画布上的任意 drop 都消费本次内部拖拽标记(含空 text/json),避免残留到下一次 drop
361
+ const allowed = isInternalDrag;
362
+ isInternalDrag = false;
363
+
302
364
  if (!e.dataTransfer) return;
303
365
 
304
366
  const data = e.dataTransfer.getData('text/json');
305
367
 
306
- if (!data) return;
368
+ // 外部源投递的拖拽数据不可信,不能交给 parseDSL(默认实现为 eval)
369
+ if (!data || !allowed) return;
370
+
371
+ let config: { dragType?: string; data?: AddMNode } | undefined;
372
+ try {
373
+ config = parseDSL(`(${data})`);
374
+ } catch {
375
+ return;
376
+ }
307
377
 
308
- const config = parseDSL(`(${data})`);
378
+ if (!config || config.dragType !== DragType.COMPONENT_LIST || !config.data) return;
309
379
 
310
- if (!config || config.dragType !== DragType.COMPONENT_LIST) return;
380
+ const dragData = config.data;
311
381
 
312
382
  e.preventDefault();
313
383
 
@@ -349,7 +419,7 @@ const dropHandler = async (e: DragEvent) => {
349
419
 
350
420
  const containerRect = stageContainerEl.value.getBoundingClientRect();
351
421
  const { scrollTop, scrollLeft } = stage.mask!;
352
- const { style = {} } = config.data;
422
+ const { style = {} } = dragData;
353
423
 
354
424
  let top = 0;
355
425
  let left = 0;
@@ -371,16 +441,16 @@ const dropHandler = async (e: DragEvent) => {
371
441
  }
372
442
  }
373
443
 
374
- config.data.style = {
444
+ dragData.style = {
375
445
  ...style,
376
446
  position,
377
447
  top: calcValueByFontsize(doc, top / zoom.value),
378
448
  left: calcValueByFontsize(doc, left / zoom.value),
379
449
  };
380
450
 
381
- config.data.inputEvent = e;
451
+ dragData.inputEvent = e;
382
452
 
383
- editorService.add(config.data, parent, { historySource: 'component-panel' });
453
+ editorService.add(dragData, parent, { historySource: 'component-panel' });
384
454
  }
385
455
  };
386
456
  </script>
@@ -13,6 +13,5 @@ import { HeadlessFieldOptions } from "@tmagic/form/headless";
13
13
  * innerConfig 返回的 config / model / prop 与组件模板里传给内部容器的那一组保持一一对应,
14
14
  * 且配置本身与组件共用同一份工厂(`fields/configs/`)。
15
15
  */
16
- declare const editorFields: Record<string, HeadlessFieldOptions>;
17
- //#endregion
18
- export { editorFields };
16
+ export declare const editorFields: Record<string, HeadlessFieldOptions>;
17
+ //#endregion