@tmagic/editor 1.8.0-beta.11 → 1.8.0-beta.13

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.
Files changed (119) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +7 -4
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +5 -1
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +2 -1
  4. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +18 -123
  5. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +60 -14
  6. package/dist/es/components/ScrollBar.js +0 -1
  7. package/dist/es/components/ViewForm.js +5 -0
  8. package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +58 -0
  9. package/dist/es/editorProps.js +6 -1
  10. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
  11. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
  12. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
  13. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
  14. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
  15. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
  16. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
  17. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
  18. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
  19. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
  20. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
  21. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
  22. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
  23. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
  24. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
  25. package/dist/es/fields/UISelect.js +0 -1
  26. package/dist/es/hooks/use-compare-form.js +119 -0
  27. package/dist/es/index.js +13 -8
  28. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
  29. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
  30. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +4 -1
  31. package/dist/es/layouts/history-list/composables.js +1 -1
  32. package/dist/es/layouts/history-list/useHistoryRevert.js +7 -3
  33. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
  34. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
  35. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  36. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
  37. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  38. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
  39. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
  40. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  41. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
  42. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  43. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  44. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
  45. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
  46. package/dist/es/plugin.js +3 -1
  47. package/dist/es/services/codeBlock.js +1 -1
  48. package/dist/es/services/editor.js +104 -5
  49. package/dist/es/services/history.js +1 -1
  50. package/dist/es/services/props.js +3 -2
  51. package/dist/es/services/storage.js +2 -2
  52. package/dist/es/services/ui.js +3 -0
  53. package/dist/es/style.css +104 -55
  54. package/dist/es/utils/data-source/index.js +30 -11
  55. package/dist/es/utils/event.js +123 -0
  56. package/dist/es/utils/monaco-editor.js +22 -6
  57. package/dist/es/utils/props.js +119 -8
  58. package/dist/es/utils/type-match-rules.js +360 -0
  59. package/dist/style.css +104 -55
  60. package/dist/tmagic-editor.umd.cjs +16996 -15806
  61. package/package.json +8 -8
  62. package/src/Editor.vue +3 -1
  63. package/src/components/CodeBlockEditor.vue +3 -0
  64. package/src/components/CompareForm.vue +16 -196
  65. package/src/components/FloatingBox.vue +77 -8
  66. package/src/components/ScrollBar.vue +0 -67
  67. package/src/components/ViewForm.vue +55 -0
  68. package/src/editorProps.ts +13 -0
  69. package/src/fields/CodeSelect.vue +4 -1
  70. package/src/fields/CondOpSelect.vue +2 -24
  71. package/src/fields/DataSourceFields.vue +5 -0
  72. package/src/fields/DataSourceInput.vue +7 -25
  73. package/src/fields/DataSourceMocks.vue +3 -0
  74. package/src/fields/DisplayConds.vue +14 -0
  75. package/src/fields/EventSelect.vue +84 -129
  76. package/src/fields/StyleSetter/Index.vue +2 -8
  77. package/src/fields/StyleSetter/pro/Background.vue +94 -55
  78. package/src/fields/StyleSetter/pro/Border.vue +9 -11
  79. package/src/fields/StyleSetter/pro/Font.vue +66 -64
  80. package/src/fields/StyleSetter/pro/Layout.vue +140 -138
  81. package/src/fields/StyleSetter/pro/Position.vue +69 -67
  82. package/src/fields/StyleSetter/pro/Transform.vue +23 -25
  83. package/src/fields/UISelect.vue +0 -12
  84. package/src/hooks/index.ts +1 -0
  85. package/src/hooks/use-compare-form.ts +187 -0
  86. package/src/index.ts +2 -0
  87. package/src/layouts/CodeEditor.vue +32 -6
  88. package/src/layouts/Framework.vue +7 -6
  89. package/src/layouts/history-list/HistoryDiffDialog.vue +11 -1
  90. package/src/layouts/history-list/useHistoryRevert.ts +4 -0
  91. package/src/layouts/props-panel/FormPanel.vue +66 -5
  92. package/src/layouts/props-panel/PropsPanel.vue +20 -6
  93. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  94. package/src/layouts/sidebar/Sidebar.vue +2 -0
  95. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
  96. package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
  97. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  98. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
  99. package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
  100. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  101. package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
  102. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  103. package/src/plugin.ts +3 -1
  104. package/src/services/editor.ts +168 -3
  105. package/src/services/ui.ts +6 -0
  106. package/src/theme/event.scss +8 -0
  107. package/src/theme/floating-box.scss +9 -1
  108. package/src/theme/layer-node-content.scss +14 -0
  109. package/src/theme/scroll-bar.scss +64 -0
  110. package/src/theme/theme.scss +3 -0
  111. package/src/theme/ui-select.scss +9 -0
  112. package/src/type.ts +105 -2
  113. package/src/utils/data-source/index.ts +30 -12
  114. package/src/utils/event.ts +224 -0
  115. package/src/utils/index.ts +2 -0
  116. package/src/utils/monaco-editor.ts +27 -0
  117. package/src/utils/props.ts +189 -2
  118. package/src/utils/type-match-rules.ts +678 -0
  119. package/types/index.d.ts +558 -213
@@ -27,6 +27,9 @@ import {
27
27
  DEFAULT_LEFT_COLUMN_WIDTH,
28
28
  DEFAULT_RIGHT_COLUMN_WIDTH,
29
29
  LEFT_COLUMN_WIDTH_STORAGE_KEY,
30
+ MIN_CENTER_COLUMN_WIDTH,
31
+ MIN_LEFT_COLUMN_WIDTH,
32
+ MIN_RIGHT_COLUMN_WIDTH,
30
33
  RIGHT_COLUMN_WIDTH_STORAGE_KEY,
31
34
  } from '@editor/utils/const';
32
35
 
@@ -54,6 +57,9 @@ const state = shallowReactive<UiState>({
54
57
  storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
55
58
  DEFAULT_RIGHT_COLUMN_WIDTH,
56
59
  },
60
+ minLeftColumnWidth: MIN_LEFT_COLUMN_WIDTH,
61
+ minCenterColumnWidth: MIN_CENTER_COLUMN_WIDTH,
62
+ minRightColumnWidth: MIN_RIGHT_COLUMN_WIDTH,
57
63
  showGuides: true,
58
64
  hasGuides: false,
59
65
  showRule: true,
@@ -35,3 +35,11 @@
35
35
  margin-right: 5px;
36
36
  }
37
37
  }
38
+
39
+ .el-form-item {
40
+ .el-form-item {
41
+ &.is-error {
42
+ margin-bottom: 18px;
43
+ }
44
+ }
45
+ }
@@ -25,10 +25,18 @@
25
25
  .m-editor-float-box-body {
26
26
  overflow: auto;
27
27
  flex: 1;
28
- padding: 0 16px;
29
28
  }
30
29
  }
31
30
 
32
31
  .m-editor-floating-box-moveable {
33
32
  opacity: 0;
34
33
  }
34
+
35
+ .m-editor-float-box-drag-mask {
36
+ position: fixed;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ background-color: transparent;
42
+ }
@@ -0,0 +1,14 @@
1
+ .m-editor-layer-node-content {
2
+ display: inline-flex;
3
+ align-items: center;
4
+
5
+ &.is-invalid .m-editor-layer-node-label {
6
+ color: var(--el-color-danger, #f56c6c);
7
+ }
8
+
9
+ .m-editor-layer-node-error-icon {
10
+ margin-left: 4px;
11
+ color: var(--el-color-danger, #f56c6c);
12
+ cursor: help;
13
+ }
14
+ }
@@ -0,0 +1,64 @@
1
+ .m-editor-scroll-bar {
2
+ position: absolute;
3
+ background-color: transparent;
4
+ opacity: 0.3;
5
+ transition:
6
+ background-color 0.2s linear,
7
+ opacity 0.2s linear;
8
+
9
+ .m-editor-scroll-bar-thumb {
10
+ background-color: #aaa;
11
+ border-radius: 6px;
12
+ position: absolute;
13
+ }
14
+
15
+ &.horizontal {
16
+ width: 100%;
17
+ height: 15px;
18
+ bottom: 0;
19
+
20
+ .m-editor-scroll-bar-thumb {
21
+ height: 6px;
22
+ transition:
23
+ background-color 0.2s linear,
24
+ height 0.2s ease-in-out;
25
+ bottom: 2px;
26
+ }
27
+ }
28
+
29
+ &.vertical {
30
+ height: 100%;
31
+ width: 15px;
32
+ right: 5px;
33
+
34
+ .m-editor-scroll-bar-thumb {
35
+ width: 6px;
36
+ transition:
37
+ background-color 0.2s linear,
38
+ width 0.2s ease-in-out;
39
+ right: 2px;
40
+ }
41
+ }
42
+
43
+ &:hover,
44
+ &:focus {
45
+ background-color: #eee;
46
+ opacity: 0.9;
47
+
48
+ .m-editor-scroll-bar-thumb {
49
+ background-color: #999;
50
+ }
51
+
52
+ &.horizontal {
53
+ .m-editor-scroll-bar-thumb {
54
+ height: 11px;
55
+ }
56
+ }
57
+
58
+ &.vertical {
59
+ .m-editor-scroll-bar-thumb {
60
+ width: 11px;
61
+ }
62
+ }
63
+ }
64
+ }
@@ -27,4 +27,7 @@
27
27
  @use "./page-fragment-select.scss";
28
28
  @use "./data-source-field.scss";
29
29
  @use "./data-source-field-select.scss";
30
+ @use "./scroll-bar.scss";
31
+ @use "./ui-select.scss";
32
+ @use "./layer-node-content.scss";
30
33
  @use "./style-setter/index.scss";
@@ -0,0 +1,9 @@
1
+ .m-fields-ui-select {
2
+ cursor: pointer;
3
+ i {
4
+ margin-right: 3px;
5
+ }
6
+ span {
7
+ color: #2882e0;
8
+ }
9
+ }
package/src/type.ts CHANGED
@@ -33,6 +33,7 @@ import type {
33
33
  MPage,
34
34
  MPageFragment,
35
35
  } from '@tmagic/core';
36
+ import type { FieldSize } from '@tmagic/design';
36
37
  import type { ChangeRecord, FormConfig, FormState, TableColumnConfig, TypeFunction } from '@tmagic/form';
37
38
  import type StageCore from '@tmagic/stage';
38
39
  import type {
@@ -76,7 +77,7 @@ export interface FrameworkSlots {
76
77
  empty(props: {}): any;
77
78
  workspace(props: {}): any;
78
79
  'props-panel'(props: {}): any;
79
- 'footer'(props: {}): any;
80
+ footer(props: {}): any;
80
81
  'page-bar'(props: {}): any;
81
82
  'page-bar-add-button'(props: {}): any;
82
83
  'page-bar-title'(props: { page: MPage | MPageFragment }): any;
@@ -209,6 +210,22 @@ export interface StageOptions {
209
210
  beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
210
211
  }
211
212
 
213
+ /**
214
+ * 节点校验错误信息,按来源(属性表单 / 样式表单)分别保存错误文案。
215
+ * 属性表单与样式表单是两个独立的 FormPanel,均指向同一节点,故以来源为键,
216
+ * 避免某个面板校验通过时误清另一个面板记录的错误。
217
+ * 节点视为存在错误当且仅当任一来源存在非空文本。
218
+ */
219
+ export interface NodeInvalidInfo {
220
+ /** 属性表单校验错误文案(可能为包含 <br> 的 HTML) */
221
+ props?: string;
222
+ /** 样式表单校验错误文案(可能为包含 <br> 的 HTML) */
223
+ style?: string;
224
+ }
225
+
226
+ /** 节点校验错误来源 */
227
+ export type NodeInvalidSource = keyof NodeInvalidInfo;
228
+
212
229
  export interface StoreState {
213
230
  root: MApp | null;
214
231
  page: MPage | MPageFragment | null;
@@ -219,6 +236,8 @@ export interface StoreState {
219
236
  stage: StageCore | null;
220
237
  stageLoading: boolean;
221
238
  modifiedNodeIds: Map<Id, Id>;
239
+ /** 校验失败的节点错误信息,按节点 id 存储,供组件树标记与保存拦截读取 */
240
+ invalidNodeIds: Map<Id, NodeInvalidInfo>;
222
241
  pageLength: number;
223
242
  pageFragmentLength: number;
224
243
  disabledMultiSelect: boolean;
@@ -296,6 +315,12 @@ export interface UiState {
296
315
  stageRect: StageRect;
297
316
  /** 编辑器列布局每一列的宽度,分为左中右三列 */
298
317
  columnWidth: GetColumnWidth;
318
+ /** 编辑器列布局左侧列最小宽度 */
319
+ minLeftColumnWidth: number;
320
+ /** 编辑器列布局中间列最小宽度 */
321
+ minCenterColumnWidth: number;
322
+ /** 编辑器列布局右侧列最小宽度 */
323
+ minRightColumnWidth: number;
299
324
  /** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
300
325
  showGuides: boolean;
301
326
  /** 画布上是否存在参考线 */
@@ -541,6 +566,58 @@ export interface CompareFormLoadConfigContext {
541
566
  * 可通过 `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
542
567
  */
543
568
  export type CompareFormLoadConfig = (ctx: CompareFormLoadConfigContext) => FormConfig | Promise<FormConfig>;
569
+
570
+ /**
571
+ * CompareForm / ViewForm 共用的基础 props。
572
+ * 两者都基于同一套「按 category 加载 FormConfig + 注入 services/stage」的逻辑(见 useCompareForm),
573
+ * 差异仅在于是否做新旧值对比。这里抽出公共字段避免重复定义。
574
+ */
575
+ export interface CompareFormBaseProps {
576
+ /** 当前值(对比场景下为修改后的值) */
577
+ value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
578
+ /**
579
+ * 类型说明:
580
+ * - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
581
+ * - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
582
+ * - `category` 为 `code-block` 时,`type` 可不传
583
+ */
584
+ type?: string;
585
+ /** 表单配置类别,决定从哪里取 FormConfig */
586
+ category?: CompareCategory;
587
+ /** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
588
+ dataSourceType?: string;
589
+ labelWidth?: string;
590
+ /**
591
+ * 外层容器高度。设置后表单内容超出时会在组件内部出现滚动条,
592
+ * 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
593
+ */
594
+ height?: string;
595
+ /**
596
+ * 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
597
+ * PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
598
+ * filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
599
+ * 因此在对比 / 展示场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
600
+ */
601
+ extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
602
+ /**
603
+ * 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
604
+ * 组件会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
605
+ */
606
+ baseFormState?: FormState;
607
+ /**
608
+ * 表单内组件的尺寸(透传给 MForm 的 `size`),可选 'large' | 'default' | 'small'。
609
+ * 缺省时使用 MForm 内置默认尺寸。
610
+ */
611
+ size?: FieldSize;
612
+ /**
613
+ * 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
614
+ * 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
615
+ * `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
616
+ */
617
+ loadConfig?: CompareFormLoadConfig;
618
+ /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
619
+ services?: Services;
620
+ }
544
621
  // #endregion CompareForm
545
622
 
546
623
  // #region SideItemKey
@@ -844,6 +921,10 @@ export interface StepExtra {
844
921
  selectedAfter?: Id[];
845
922
  /** 本次操作涉及的节点 id 集合(page 类型) */
846
923
  modifiedNodeIds?: Map<Id, Id>;
924
+ /** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
925
+ invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
926
+ /** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
927
+ invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
847
928
  [key: string]: any;
848
929
  }
849
930
  // #endregion StepExtra
@@ -1200,9 +1281,24 @@ export interface PageBarSortOptions extends PartSortableOptions {
1200
1281
  }
1201
1282
  // #endregion PageBarSortOptions
1202
1283
 
1284
+ /**
1285
+ * 右键菜单当前目标(侧栏树节点等)。
1286
+ * 数据源 / 代码块面板通过 `customContentMenu` 的 `getTarget` 暴露,业务在 handler 内自行读取。
1287
+ */
1288
+ export interface ContentMenuTarget {
1289
+ /** 目标 ID */
1290
+ id: string;
1291
+ /** 原始节点数据(树节点等) */
1292
+ data?: TreeNodeData;
1293
+ }
1294
+
1295
+ export type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block';
1296
+
1203
1297
  export type CustomContentMenuFunction = (
1204
1298
  menus: (MenuButton | MenuComponent)[],
1205
- type: 'layer' | 'data-source' | 'viewer' | 'code-block',
1299
+ type: ContentMenuType,
1300
+ /** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */
1301
+ getTarget?: () => ContentMenuTarget | null,
1206
1302
  ) => (MenuButton | MenuComponent)[];
1207
1303
 
1208
1304
  export interface EditorEvents {
@@ -1225,6 +1321,8 @@ export interface EditorEvents {
1225
1321
  * add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 `history-change`。
1226
1322
  */
1227
1323
  change: [event: EditorChangeEvent];
1324
+ /** 节点校验错误状态发生变化时触发,携带当前完整的错误 Map(供非响应式消费方订阅) */
1325
+ 'invalid-node-change': [invalidNodeIds: Map<Id, NodeInvalidInfo>];
1228
1326
  }
1229
1327
 
1230
1328
  // #region EditorChangeEvent
@@ -1447,6 +1545,11 @@ export interface CustomDiffFormOptions {
1447
1545
  * 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
1448
1546
  */
1449
1547
  width?: string;
1548
+ /**
1549
+ * 差异 / 确认回滚弹窗内 form 表单的尺寸(透传给 CompareForm 的 `size`),
1550
+ * 可选 `'large' | 'default' | 'small'`,缺省时使用表单内置默认尺寸。
1551
+ */
1552
+ size?: FieldSize;
1450
1553
  }
1451
1554
 
1452
1555
  /**
@@ -235,19 +235,37 @@ export const getCascaderOptionsFromFields = (
235
235
  return result;
236
236
  };
237
237
 
238
- export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
239
- let fields = ds?.fields;
240
- let type = '';
241
-
242
- for (const fieldName of fieldNames) {
243
- if (!fields?.length) return '';
244
-
245
- const field = fields.find((f) => f.name === fieldName);
246
- if (!field) return '';
238
+ /**
239
+ * 按字段名路径下钻 DataSchema。
240
+ * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
241
+ */
242
+ export const resolveFieldByPath = (
243
+ fields: DataSchema[] | undefined,
244
+ fieldNames: string[],
245
+ options: { skipNumberIndices?: boolean } = {},
246
+ ): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => {
247
+ let currentFields = fields || [];
248
+ let field: DataSchema | undefined;
247
249
 
248
- type = field.type || '';
249
- fields = field.fields;
250
+ for (const name of fieldNames) {
251
+ if (options.skipNumberIndices && isNumber(name)) {
252
+ continue;
253
+ }
254
+ if (!currentFields.length) {
255
+ return { ok: false, fields: currentFields };
256
+ }
257
+ field = currentFields.find((item) => item.name === name);
258
+ if (!field) {
259
+ return { ok: false, fields: currentFields };
260
+ }
261
+ currentFields = field.fields || [];
250
262
  }
251
263
 
252
- return type;
264
+ return { field, ok: true, fields: currentFields };
265
+ };
266
+
267
+ export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
268
+ const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
269
+ if (!ok) return '';
270
+ return field?.type || '';
253
271
  };
@@ -0,0 +1,224 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import type { EventOption, Id, MComponent, MContainer } from '@tmagic/core';
20
+ import type { CascaderOption } from '@tmagic/form';
21
+ import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
22
+
23
+ import dataSourceService from '@editor/services/dataSource';
24
+ import editorService from '@editor/services/editor';
25
+ import eventsService from '@editor/services/events';
26
+ import { getCascaderOptionsFromFields } from '@editor/utils/data-source';
27
+
28
+ export const EVENT_NAME_VALUE_SEPARATOR = '.';
29
+
30
+ export type EventNameSelectOption = { text: string; value: string };
31
+ export type EventNameOption = EventOption | CascaderOption | EventNameSelectOption;
32
+
33
+ /** 与 EventSelect 中 checkStrictly 一致:component 为 false,其余为 true */
34
+ export const isEventNameCheckStrictly = (src?: string) => src !== 'component';
35
+
36
+ /** 将动作 method 值规范为与 collectEventNameOptionValues 一致的字符串(cascader 无 valueSeparator 时存数组) */
37
+ export const normalizeCompActionValue = (value: unknown): string => {
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
40
+ }
41
+ if (value === null || value === undefined || value === '') {
42
+ return '';
43
+ }
44
+ return `${value}`;
45
+ };
46
+
47
+ /**
48
+ * 组装事件名称下拉/级联 options,与 EventSelect 默认 eventNameConfig.options 逻辑一致。
49
+ */
50
+ export const getEventNameOptions = (
51
+ src: string | undefined,
52
+ formValue: Record<string, any> = {},
53
+ ): EventNameOption[] => {
54
+ if (!formValue.type) {
55
+ return [];
56
+ }
57
+
58
+ if (src === 'component') {
59
+ let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type) || [];
60
+
61
+ if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
62
+ const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
63
+ if (!pageFragment) {
64
+ return [];
65
+ }
66
+
67
+ events = [
68
+ {
69
+ label: pageFragment.name || '页面片容器',
70
+ value: pageFragment.id,
71
+ children: events,
72
+ },
73
+ ];
74
+
75
+ (pageFragment.items || []).forEach((node) => {
76
+ traverseNode<MComponent | MContainer>(node, (current) => {
77
+ const nodeEvents = (current.type && eventsService.getEvent(current.type)) || [];
78
+ (events as CascaderOption[]).push({
79
+ label: `${current.name}_${current.id}`,
80
+ value: `${current.id}`,
81
+ children: nodeEvents,
82
+ });
83
+ });
84
+ });
85
+
86
+ return events;
87
+ }
88
+
89
+ return (events as EventOption[]).map((option) => ({
90
+ text: option.label,
91
+ value: option.value,
92
+ }));
93
+ }
94
+
95
+ if (src === 'datasource') {
96
+ const events: EventNameOption[] = dataSourceService.getFormEvent(formValue.type) || [];
97
+ const dataSource = dataSourceService.getDataSourceById(formValue.id);
98
+ const fields = dataSource?.fields || [];
99
+
100
+ if (fields.length > 0) {
101
+ return [
102
+ ...events,
103
+ {
104
+ label: '数据变化',
105
+ value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
106
+ children: getCascaderOptionsFromFields(fields),
107
+ },
108
+ ];
109
+ }
110
+
111
+ return events;
112
+ }
113
+
114
+ return [];
115
+ };
116
+
117
+ /** 将 select / cascader options 展平为最终写入 name 的字符串集合(cascader 用 `.` 拼接) */
118
+ export const collectEventNameOptionValues = (
119
+ options: EventNameOption[],
120
+ checkStrictly: boolean,
121
+ prefix: any[] = [],
122
+ result: Set<string> = new Set(),
123
+ ): Set<string> => {
124
+ options.forEach((option) => {
125
+ if (typeof option?.value === 'undefined') {
126
+ return;
127
+ }
128
+
129
+ const path = [...prefix, option.value];
130
+ const joined = path.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
131
+ const children = 'children' in option ? option.children : undefined;
132
+
133
+ if (Array.isArray(children) && children.length) {
134
+ if (checkStrictly) {
135
+ result.add(joined);
136
+ }
137
+ collectEventNameOptionValues(children as EventNameOption[], checkStrictly, path, result);
138
+ return;
139
+ }
140
+
141
+ result.add(joined);
142
+ });
143
+
144
+ return result;
145
+ };
146
+
147
+ /**
148
+ * 解析 event-select 允许的 name 集合。
149
+ * 自定义 eventNameConfig.options 时返回 null(跳过枚举校验)。
150
+ */
151
+ export const getEventNameAllowedValues = (
152
+ config: { src?: string; eventNameConfig?: { options?: unknown } } = {},
153
+ formValue: Record<string, any> = {},
154
+ ): Set<string> | null => {
155
+ if (typeof config.eventNameConfig?.options !== 'undefined') {
156
+ return null;
157
+ }
158
+
159
+ if (config.src !== 'component' && config.src !== 'datasource') {
160
+ return null;
161
+ }
162
+
163
+ return collectEventNameOptionValues(getEventNameOptions(config.src, formValue), isEventNameCheckStrictly(config.src));
164
+ };
165
+
166
+ /**
167
+ * 组装联动组件动作下拉/级联 options,与 EventSelect 默认 compActionConfig.options 逻辑一致。
168
+ */
169
+ export const getCompActionOptions = (toId?: Id): EventNameOption[] => {
170
+ if (typeof toId === 'undefined' || toId === null || toId === '') {
171
+ return [];
172
+ }
173
+
174
+ const node = editorService.getNodeById(toId);
175
+ if (!node?.type) {
176
+ return [];
177
+ }
178
+
179
+ let methods: EventOption[] | CascaderOption[] = eventsService.getMethod(node.type, toId) || [];
180
+
181
+ if (node.type === 'page-fragment-container' && node.pageFragmentId) {
182
+ const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
183
+ if (!pageFragment) {
184
+ return [];
185
+ }
186
+
187
+ methods = [];
188
+ (pageFragment.items || []).forEach((item: MComponent | MContainer) => {
189
+ traverseNode<MComponent | MContainer>(item, (current) => {
190
+ const nodeMethods = (current.type && eventsService.getMethod(current.type, current.id)) || [];
191
+
192
+ if (nodeMethods.length) {
193
+ (methods as CascaderOption[]).push({
194
+ label: `${current.name}_${current.id}`,
195
+ value: `${current.id}`,
196
+ children: nodeMethods,
197
+ });
198
+ }
199
+ });
200
+ });
201
+
202
+ return methods;
203
+ }
204
+
205
+ return (methods as EventOption[]).map((method) => ({
206
+ text: method.label,
207
+ value: method.value,
208
+ }));
209
+ };
210
+
211
+ /**
212
+ * 解析 event-select 联动组件动作允许的 method 集合。
213
+ * 自定义 compActionConfig.options 时返回 null(跳过枚举校验)。
214
+ */
215
+ export const getCompActionAllowedValues = (
216
+ config: { src?: string; compActionConfig?: { options?: unknown } } = {},
217
+ model: { to?: Id } = {},
218
+ ): Set<string> | null => {
219
+ if (typeof config.compActionConfig?.options !== 'undefined') {
220
+ return null;
221
+ }
222
+
223
+ return collectEventNameOptionValues(getCompActionOptions(model.to), isEventNameCheckStrictly(config.src));
224
+ };
@@ -30,4 +30,6 @@ export * from './undo-redo';
30
30
  export * from './indexed-db';
31
31
  export * from './history';
32
32
  export * from './const';
33
+ export * from './type-match-rules';
34
+ export * from './event';
33
35
  export { default as loadMonaco } from './monaco-editor';
@@ -1,7 +1,34 @@
1
1
  let cached: Promise<typeof import('monaco-editor')> | undefined;
2
2
 
3
+ // 是否为 Monaco 内部抛出的取消错误(Canceled)
4
+ // 销毁编辑器时,WordHighlighter 内部挂起的 Delayer 会被取消,
5
+ // 取消时 reject 出的 Canceled 错误没有 catch,会变成 "Uncaught (in promise) Canceled",
6
+ // 该错误本身无害,这里做一次性、全局的兜底处理,仅吞掉这类取消错误。
7
+ const isMonacoCanceledError = (reason: any): boolean => {
8
+ if (!reason) return false;
9
+ if (reason instanceof Error) {
10
+ return reason.name === 'Canceled' || reason.message === 'Canceled';
11
+ }
12
+ return reason === 'Canceled';
13
+ };
14
+
15
+ let canceledRejectionHandlerInstalled = false;
16
+ const installCanceledRejectionHandler = () => {
17
+ if (canceledRejectionHandlerInstalled || typeof globalThis.addEventListener !== 'function') return;
18
+ canceledRejectionHandlerInstalled = true;
19
+
20
+ globalThis.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
21
+ if (isMonacoCanceledError(event.reason)) {
22
+ // 阻止其冒泡到控制台,避免无意义的报错噪音
23
+ event.preventDefault();
24
+ }
25
+ });
26
+ };
27
+
3
28
  export default () => {
4
29
  if (!cached) {
30
+ installCanceledRejectionHandler();
31
+
5
32
  cached = Promise.all([import('emmet-monaco-es'), import('monaco-editor')]).then(([emmet, monaco]) => {
6
33
  const { emmetHTML, emmetCSS } = emmet;
7
34
  emmetHTML(monaco);