@tmagic/editor 1.8.0-beta.19 → 1.8.0-beta.20

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.
@@ -2,7 +2,7 @@ import { useServices } from "../../../hooks/use-services.js";
2
2
  import ContentMenu_default from "../../../components/ContentMenu.js";
3
3
  import FolderMinusIcon_default from "../../../icons/FolderMinusIcon.js";
4
4
  import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from "../../../utils/content-menu.js";
5
- import { isPage, isPageFragment } from "@tmagic/utils";
5
+ import { isPageOrFragment } from "@tmagic/utils";
6
6
  import { computed, createBlock, defineComponent, markRaw, openBlock, useTemplateRef } from "vue";
7
7
  import { Files, Plus } from "@element-plus/icons-vue";
8
8
  //#region packages/editor/src/layouts/sidebar/layer/LayerMenu.vue?vue&type=script&setup=true&lang.ts
@@ -55,7 +55,7 @@ var LayerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
55
55
  type: "button",
56
56
  text: "全部折叠",
57
57
  icon: FolderMinusIcon_default,
58
- display: () => isPage(node.value) || isPageFragment(node.value),
58
+ display: () => isPageOrFragment(node.value),
59
59
  handler: () => {
60
60
  emit("collapse-all");
61
61
  }
@@ -1,6 +1,6 @@
1
1
  import { UI_SELECT_MODE_EVENT_NAME } from "../../../utils/const.js";
2
2
  import { updateStatus } from "../../../utils/tree.js";
3
- import { getElById, isPage, isPageFragment } from "@tmagic/utils";
3
+ import { getElById, isPageOrFragment } from "@tmagic/utils";
4
4
  import { computed, nextTick } from "vue";
5
5
  import { throttle } from "lodash-es";
6
6
  //#region packages/editor/src/layouts/sidebar/layer/use-click.ts
@@ -16,7 +16,7 @@ var useClick = ({ editorService, stageOverlayService, uiService }, isCtrlKeyDown
16
16
  }
17
17
  };
18
18
  const multiSelect = async (data) => {
19
- if (isPage(data) || isPageFragment(data)) return;
19
+ if (isPageOrFragment(data)) return;
20
20
  const nodes = editorService.get("nodes") || [];
21
21
  const newNodes = [];
22
22
  let isCancel = false;
@@ -25,7 +25,7 @@ var useClick = ({ editorService, stageOverlayService, uiService }, isCtrlKeyDown
25
25
  isCancel = true;
26
26
  return;
27
27
  }
28
- if (isPage(node) || isPageFragment(node)) return;
28
+ if (isPageOrFragment(node)) return;
29
29
  newNodes.push(node.id);
30
30
  });
31
31
  if (!isCancel || newNodes.length === 0) newNodes.push(data.id);
@@ -1,5 +1,5 @@
1
1
  import { updateStatus } from "../../../utils/tree.js";
2
- import { getNodePath, isPage, isPageFragment, traverseNode } from "@tmagic/utils";
2
+ import { getNodePath, isPageOrFragment, traverseNode } from "@tmagic/utils";
3
3
  import { computed, onBeforeUnmount, ref, watch } from "vue";
4
4
  //#region packages/editor/src/layouts/sidebar/layer/use-node-status.ts
5
5
  var createPageNodeStatus = (page, initialLayerNodeStatus) => {
@@ -75,7 +75,7 @@ var useNodeStatus = ({ editorService }) => {
75
75
  }, { immediate: true });
76
76
  const addHandler = (newNodes) => {
77
77
  newNodes.forEach((node) => {
78
- if (isPage(node) || isPageFragment(node)) return;
78
+ if (isPageOrFragment(node)) return;
79
79
  traverseNode(node, (node) => {
80
80
  nodeStatusMap.value?.set(node.id, {
81
81
  visible: true,
@@ -3,7 +3,7 @@ import { useServices } from "../../../hooks/use-services.js";
3
3
  import ContentMenu_default from "../../../components/ContentMenu.js";
4
4
  import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from "../../../utils/content-menu.js";
5
5
  import CenterIcon_default from "../../../icons/CenterIcon.js";
6
- import { isPage, isPageFragment } from "@tmagic/utils";
6
+ import { isPageOrFragment } from "@tmagic/utils";
7
7
  import { NodeType } from "@tmagic/core";
8
8
  import { computed, createBlock, defineComponent, markRaw, openBlock, ref, useTemplateRef, watch } from "vue";
9
9
  import { Bottom, Top } from "@element-plus/icons-vue";
@@ -46,14 +46,14 @@ var ViewerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
46
46
  direction: "horizontal",
47
47
  display: () => {
48
48
  if (!node.value) return false;
49
- return !isPage(node.value) && !isPageFragment(node.value);
49
+ return !isPageOrFragment(node.value);
50
50
  }
51
51
  },
52
52
  {
53
53
  type: "button",
54
54
  text: "上移一层",
55
55
  icon: markRaw(Top),
56
- display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
56
+ display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
57
57
  handler: () => {
58
58
  editorService.moveLayer(1, { historySource: "stage-contextmenu" });
59
59
  }
@@ -62,7 +62,7 @@ var ViewerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
62
62
  type: "button",
63
63
  text: "下移一层",
64
64
  icon: markRaw(Bottom),
65
- display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
65
+ display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
66
66
  handler: () => {
67
67
  editorService.moveLayer(-1, { historySource: "stage-contextmenu" });
68
68
  }
@@ -71,7 +71,7 @@ var ViewerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
71
71
  type: "button",
72
72
  text: "置顶",
73
73
  icon: markRaw(Top),
74
- display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
74
+ display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
75
75
  handler: () => {
76
76
  editorService.moveLayer(LayerOffset.TOP, { historySource: "stage-contextmenu" });
77
77
  }
@@ -80,7 +80,7 @@ var ViewerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
80
80
  type: "button",
81
81
  text: "置底",
82
82
  icon: markRaw(Bottom),
83
- display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
83
+ display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
84
84
  handler: () => {
85
85
  editorService.moveLayer(LayerOffset.BOTTOM, { historySource: "stage-contextmenu" });
86
86
  }
@@ -89,7 +89,7 @@ var ViewerMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
89
89
  {
90
90
  type: "divider",
91
91
  direction: "horizontal",
92
- display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect
92
+ display: () => !isPageOrFragment(node.value) && !props.isMultiSelect
93
93
  },
94
94
  useDeleteMenu("stage-contextmenu"),
95
95
  {
@@ -1,11 +1,10 @@
1
1
  import BaseService from "./BaseService.js";
2
2
  import { IdleTask } from "../utils/dep/idle-task.js";
3
- import WorkerWrapper from "../utils/dep/worker.js";
3
+ import { CollectWorkerClient } from "../utils/dep/collect-worker-client.js";
4
4
  import { isPage } from "@tmagic/utils";
5
5
  import { DepTargetType, Watcher, traverseTarget } from "@tmagic/core";
6
6
  import { reactive, shallowReactive } from "vue";
7
7
  import { throttle } from "lodash-es";
8
- import serialize from "serialize-javascript";
9
8
  //#region packages/editor/src/services/dep.ts
10
9
  var Dep = class extends BaseService {
11
10
  state = shallowReactive({
@@ -13,6 +12,7 @@ var Dep = class extends BaseService {
13
12
  taskLength: 0
14
13
  });
15
14
  idleTask = new IdleTask();
15
+ collectWorker = new CollectWorkerClient();
16
16
  watcher = new Watcher({ initialTargets: reactive({}) });
17
17
  waitingWorker;
18
18
  resolveWaitingWorker;
@@ -65,6 +65,14 @@ var Dep = class extends BaseService {
65
65
  this.emit("collected", nodes, deep);
66
66
  this.emit("ds-collected", nodes, deep);
67
67
  }
68
+ /**
69
+ * 空闲收集依赖
70
+ *
71
+ * 遍历匹配是整个收集过程中最耗时的部分(要深度遍历节点树并对每个属性做 target 匹配),
72
+ * 节点/数据源多时在主线程执行会长时间卡死页面,因此优先交给 worker 执行,主线程只负责把结果写回 target。
73
+ * 自定义 target 的 isTarget 是无法跨线程传递的闭包函数(没有 descriptor),仍走主线程空闲队列;
74
+ * worker 不可用(如 SSR)或执行失败时也会回退到主线程空闲队列。
75
+ */
68
76
  async collectIdle(nodes, depExtendedData = {}, deep = false, type) {
69
77
  if (this.waitingWorker) await this.waitingWorker;
70
78
  const batch = {
@@ -74,11 +82,13 @@ var Dep = class extends BaseService {
74
82
  dsPending: 0,
75
83
  collectedEmitted: false,
76
84
  dsSettled: false,
85
+ aborted: false,
77
86
  resolve: () => {}
78
87
  };
79
- this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
80
- this.enqueueTask(node, target, depExtendedData, deep, batch);
81
- });
88
+ const workerTargets = [];
89
+ for (const target of this.watcher.getCollectableTargets(type)) if (target.descriptor && this.collectWorker.isSupported) workerTargets.push(target);
90
+ else this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
91
+ if (workerTargets.length && nodes.length) this.collectByCollectWorker(nodes, workerTargets, depExtendedData, deep, batch);
82
92
  if (batch.pending === 0) {
83
93
  this.emit("collected", nodes, deep);
84
94
  this.updateCollectingState();
@@ -97,16 +107,8 @@ var Dep = class extends BaseService {
97
107
  const { promise, resolve: waitingResolve } = Promise.withResolvers();
98
108
  this.waitingWorker = promise;
99
109
  this.resolveWaitingWorker = waitingResolve;
100
- return new Promise((resolve) => {
101
- const worker = new WorkerWrapper();
102
- worker.postMessage({ dsl: serialize(dsl) });
103
- worker.onmessage = (e) => {
104
- resolve(e.data);
105
- };
106
- worker.onerror = () => {
107
- resolve({});
108
- };
109
- }).then((depsData) => {
110
+ return this.collectWorker.collectDsl(dsl).then((deps) => {
111
+ const depsData = deps || {};
110
112
  if (generation !== this.workerGeneration) {
111
113
  waitingResolve();
112
114
  return depsData;
@@ -132,7 +134,8 @@ var Dep = class extends BaseService {
132
134
  }
133
135
  collectNode(node, target, depExtendedData = {}, deep = false) {
134
136
  if (isPage(node)) this.removePageDep(target, depExtendedData);
135
- else this.watcher.removeTargetDep(target, node);
137
+ else if (deep) this.watcher.removeTargetDep(target, node);
138
+ else target.removeDep(node.id);
136
139
  this.watcher.collectItem(node, target, depExtendedData, deep);
137
140
  }
138
141
  clear(nodes) {
@@ -150,6 +153,7 @@ var Dep = class extends BaseService {
150
153
  clearIdleTasks() {
151
154
  this.abortActiveBatches();
152
155
  this.idleTask.clearTasks();
156
+ this.collectWorker.abort();
153
157
  }
154
158
  on(eventName, listener) {
155
159
  return super.on(eventName, listener);
@@ -164,6 +168,7 @@ var Dep = class extends BaseService {
164
168
  this.resolveWaitingWorker = void 0;
165
169
  this.waitingWorker = void 0;
166
170
  this.idleTask.clearTasks();
171
+ this.collectWorker.abort();
167
172
  for (const type of Object.keys(this.watcher.getTargetsList())) this.removeTargets(type);
168
173
  this.set("collecting", false);
169
174
  }
@@ -173,6 +178,7 @@ var Dep = class extends BaseService {
173
178
  this.reset();
174
179
  this.removeAllPlugins();
175
180
  this.idleTask.removeAllListeners();
181
+ this.collectWorker.terminate();
176
182
  }
177
183
  emit(eventName, ...args) {
178
184
  return super.emit(eventName, ...args);
@@ -187,26 +193,73 @@ var Dep = class extends BaseService {
187
193
  if (dep.data?.pageId && dep.data.pageId === depExtendedData.pageId) delete target.deps[depKey];
188
194
  }
189
195
  }
196
+ /**
197
+ * 把遍历匹配交给 worker,结果回来后按 target 分片写回主线程
198
+ */
199
+ collectByCollectWorker(nodes, targets, depExtendedData, deep, batch) {
200
+ const hasDataSource = targets.some((target) => target.type === DepTargetType.DATA_SOURCE);
201
+ this.beginBatchTask(batch, hasDataSource);
202
+ this.collectWorker.collect({
203
+ nodes,
204
+ targets: targets.map((target) => target.descriptor),
205
+ depExtendedData,
206
+ deep
207
+ }).then((result) => {
208
+ if (batch.aborted) return;
209
+ for (const target of targets) if (result) this.enqueueApplyTask(target, result.deps[target.type]?.[target.id] || {}, result.nodeIds, batch, {
210
+ nodes,
211
+ depExtendedData,
212
+ deep
213
+ });
214
+ else this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
215
+ }).finally(() => {
216
+ this.onBatchTaskDone(batch, hasDataSource);
217
+ });
218
+ }
219
+ /**
220
+ * 用 worker 的收集结果替换 target 上这批节点的依赖
221
+ */
222
+ enqueueApplyTask(target, deps, nodeIds, batch, { nodes, depExtendedData, deep }) {
223
+ const isDataSource = target.type === DepTargetType.DATA_SOURCE;
224
+ this.beginBatchTask(batch, isDataSource);
225
+ this.idleTask.enqueueTask(() => {
226
+ try {
227
+ const current = this.watcher.getTarget(target.id, target.type);
228
+ if (current !== target) {
229
+ if (current) this.enqueueTasks(nodes, current, depExtendedData, deep, batch);
230
+ return;
231
+ }
232
+ if (nodes.some((node) => isPage(node))) this.removePageDep(target, depExtendedData);
233
+ for (const id of nodeIds) target.removeDep(id);
234
+ for (const [id, dep] of Object.entries(deps)) target.deps[id] = dep;
235
+ } finally {
236
+ this.onBatchTaskDone(batch, isDataSource);
237
+ }
238
+ }, void 0, isDataSource);
239
+ }
240
+ enqueueTasks(nodes, target, depExtendedData, deep, batch) {
241
+ for (const node of nodes) this.enqueueTask(node, target, depExtendedData, deep, batch);
242
+ }
190
243
  enqueueTask(node, target, depExtendedData, deep, batch) {
191
244
  const isDataSource = target.type === DepTargetType.DATA_SOURCE;
192
- batch.pending += 1;
193
- if (isDataSource) batch.dsPending += 1;
194
- this.idleTask.enqueueTask(({ node, deep, target }) => {
245
+ this.beginBatchTask(batch, isDataSource);
246
+ this.idleTask.enqueueTask(() => {
195
247
  try {
196
- this.collectNode(node, target, depExtendedData, deep);
248
+ this.collectNode(node, target, depExtendedData);
197
249
  } finally {
198
250
  this.onBatchTaskDone(batch, isDataSource);
199
251
  }
200
- }, {
201
- node,
202
- deep: false,
203
- target
204
- }, isDataSource);
252
+ }, void 0, isDataSource);
205
253
  if (deep && Array.isArray(node.items) && node.items.length) node.items.forEach((item) => {
206
254
  this.enqueueTask(item, target, depExtendedData, deep, batch);
207
255
  });
208
256
  }
257
+ beginBatchTask(batch, isDataSource) {
258
+ batch.pending += 1;
259
+ if (isDataSource) batch.dsPending += 1;
260
+ }
209
261
  onBatchTaskDone(batch, isDataSource) {
262
+ if (batch.aborted) return;
210
263
  if (isDataSource) {
211
264
  batch.dsPending -= 1;
212
265
  if (batch.dsPending === 0) this.settleBatchDs(batch);
@@ -238,7 +291,10 @@ var Dep = class extends BaseService {
238
291
  if (!this.activeBatches.size) return;
239
292
  const batches = [...this.activeBatches];
240
293
  this.activeBatches.clear();
241
- for (const batch of batches) batch.resolve(false);
294
+ for (const batch of batches) {
295
+ batch.aborted = true;
296
+ batch.resolve(false);
297
+ }
242
298
  this.updateCollectingState();
243
299
  }
244
300
  updateCollectingState() {
@@ -7,9 +7,9 @@ import storage_default, { Protocol } from "./storage.js";
7
7
  import { COPY_STORAGE_KEY, calcAlignCenterStyle, calcLayerTargetIndex, calcMoveStyle, classifyDragSources, collectRelatedNodes, describeStepForRevert, editorNodeMergeCustomizer, fixNodePosition, getInitPositionStyle, getNodeIndex, getPageFragmentList, getPageList, moveItemsInContainer, resolveSelectedNode, setChildrenLayout, setLayout, toggleFixedPosition } from "../utils/editor.js";
8
8
  import { beforePaste, getAddParent } from "../utils/operator.js";
9
9
  import { isFixed } from "@tmagic/stage";
10
- import { getNodeInfo, getNodePath, getValueByKeyPath, guid, isPage, isPageFragment, setValueByKeyPath, traverseNode } from "@tmagic/utils";
10
+ import { getNodeInfo, getNodePath, getValueByKeyPath, guid, isPage, isPageFragment, isPageOrFragment, setValueByKeyPath, traverseNode } from "@tmagic/utils";
11
11
  import { NodeType } from "@tmagic/core";
12
- import { reactive, toRaw } from "vue";
12
+ import { nextTick, reactive, toRaw } from "vue";
13
13
  import { cloneDeep as cloneDeep$1, isEmpty, isEqual, isObject, mergeWith, uniq } from "lodash-es";
14
14
  //#region packages/editor/src/services/editor.ts
15
15
  /**
@@ -159,7 +159,7 @@ var Editor = class extends BaseService {
159
159
  isOnDifferentPage(node) {
160
160
  const currentPageId = this.get("page")?.id;
161
161
  if (currentPageId === void 0 || currentPageId === null) return false;
162
- if (isPage(node) || isPageFragment(node)) return `${node.id}` !== `${currentPageId}`;
162
+ if (isPageOrFragment(node)) return `${node.id}` !== `${currentPageId}`;
163
163
  const nodePage = this.getNodeInfo(node.id, false).page;
164
164
  if (!nodePage) return false;
165
165
  return `${nodePage.id}` !== `${currentPageId}`;
@@ -252,8 +252,8 @@ var Editor = class extends BaseService {
252
252
  const curNode = this.get("node");
253
253
  const stage = this.get("stage");
254
254
  if (!curNode) throw new Error("当前选中节点为空");
255
- if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(isPage(node) || isPageFragment(node))) throw new Error("app下不能添加组件");
256
- if (parent.id !== curNode.id && !(isPage(node) || isPageFragment(node))) {
255
+ if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) throw new Error("app下不能添加组件");
256
+ if (parent.id !== curNode.id && !isPageOrFragment(node)) {
257
257
  const index = parent.items.indexOf(curNode);
258
258
  parent.items?.splice(index + 1, 0, node);
259
259
  } else parent.items?.push(node);
@@ -298,7 +298,7 @@ var Editor = class extends BaseService {
298
298
  } else addNodes.push(...addNode);
299
299
  const newNodes = await Promise.all(addNodes.map((node) => {
300
300
  const root = this.get("root");
301
- if ((isPage(node) || isPageFragment(node)) && root) return this.doAdd(node, root);
301
+ if (isPageOrFragment(node) && root) return this.doAdd(node, root);
302
302
  const parentNode = parent ?? getAddParent(node);
303
303
  if (!parentNode) throw new Error("未找到父元素");
304
304
  return this.doAdd(node, parentNode);
@@ -318,7 +318,7 @@ var Editor = class extends BaseService {
318
318
  else if (isPageFragment(newNodes[0])) this.state.pageFragmentLength += 1;
319
319
  else if (!skipSelect) stage?.select(newNodes[0].id);
320
320
  }
321
- if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
321
+ if (!isPageOrFragment(newNodes[0])) {
322
322
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
323
323
  if (!doNotPushHistory) {
324
324
  const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id;
@@ -351,7 +351,7 @@ var Editor = class extends BaseService {
351
351
  historySource,
352
352
  doNotPushHistory
353
353
  });
354
- const addedPages = newNodes.filter((node) => isPage(node) || isPageFragment(node));
354
+ const addedPages = newNodes.filter((node) => isPageOrFragment(node));
355
355
  if (addedPages.length) history_default.notifyPageStructureChange({
356
356
  add: addedPages,
357
357
  remove: []
@@ -365,13 +365,10 @@ var Editor = class extends BaseService {
365
365
  if (!parent || !curNode) throw new Error("找不要删除的节点");
366
366
  const index = getNodeIndex(curNode.id, parent);
367
367
  if (typeof index !== "number" || index === -1) throw new Error("找不要删除的节点");
368
- parent.items?.splice(index, 1);
369
368
  const stage = this.get("stage");
370
- stage?.remove({
371
- id: node.id,
372
- parentId: parent.id,
373
- root: cloneDeep$1(root)
374
- });
369
+ const currentPage = this.get("page");
370
+ const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
371
+ parent.items?.splice(index, 1);
375
372
  const selectedNodes = this.get("nodes");
376
373
  const removedSelectedIndex = selectedNodes.findIndex((n) => `${n.id}` === `${node.id}`);
377
374
  if (removedSelectedIndex !== -1) {
@@ -379,24 +376,29 @@ var Editor = class extends BaseService {
379
376
  nextSelected.splice(removedSelectedIndex, 1);
380
377
  this.set("nodes", nextSelected);
381
378
  }
382
- if (isPage(node) || isPageFragment(node)) {
383
- const currentPage = this.get("page");
384
- if (currentPage && `${currentPage.id}` === `${node.id}`) this.set("page", null);
385
- }
386
- const selectDefault = async (pages) => {
387
- if (pages[0]) {
388
- await this.select(pages[0]);
389
- stage?.select(pages[0].id);
390
- } else this.selectRoot();
379
+ const removeData = {
380
+ id: node.id,
381
+ parentId: parent.id,
382
+ root: cloneDeep$1(root)
391
383
  };
392
384
  const rootItems = root.items || [];
393
- if (isPage(node)) {
394
- this.state.pageLength -= 1;
395
- if (!doNotSelect && !doNotSwitchPage) await selectDefault(rootItems);
396
- } else if (isPageFragment(node)) {
397
- this.state.pageFragmentLength -= 1;
398
- if (!doNotSelect && !doNotSwitchPage) await selectDefault(rootItems);
385
+ const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
386
+ const shouldSwitchPage = shouldReselect && rootItems.length > 0;
387
+ if (isPageOrFragment(node)) {
388
+ if (isPage(node)) this.state.pageLength -= 1;
389
+ else this.state.pageFragmentLength -= 1;
390
+ if (shouldSwitchPage) {
391
+ await this.select(rootItems[0]);
392
+ stage?.select(rootItems[0].id);
393
+ await nextTick();
394
+ stage?.remove(removeData);
395
+ } else {
396
+ stage?.remove(removeData);
397
+ if (shouldReselect) this.selectRoot();
398
+ else if (isDeletingCurrentPage) this.set("page", null);
399
+ }
399
400
  } else {
401
+ stage?.remove(removeData);
400
402
  if (!doNotSelect) {
401
403
  await this.select(parent);
402
404
  stage?.select(parent.id);
@@ -425,7 +427,7 @@ var Editor = class extends BaseService {
425
427
  }));
426
428
  const removedItems = [];
427
429
  let pageForOp = null;
428
- if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) for (const n of nodes) {
430
+ if (!isPageOrFragment(nodes[0])) for (const n of nodes) {
429
431
  const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
430
432
  if (parent && curNode) {
431
433
  if (!pageForOp && page) pageForOp = {
@@ -459,7 +461,7 @@ var Editor = class extends BaseService {
459
461
  historySource,
460
462
  doNotPushHistory
461
463
  });
462
- const removedPages = nodes.filter((node) => isPage(node) || isPageFragment(node));
464
+ const removedPages = nodes.filter((node) => isPageOrFragment(node));
463
465
  if (removedPages.length) history_default.notifyPageStructureChange({
464
466
  add: [],
465
467
  remove: removedPages
@@ -497,7 +499,7 @@ var Editor = class extends BaseService {
497
499
  selectedNodes.splice(targetIndex, 1, newConfig);
498
500
  this.set("nodes", [...selectedNodes]);
499
501
  }
500
- if (isPage(newConfig) || isPageFragment(newConfig)) {
502
+ if (isPageOrFragment(newConfig)) {
501
503
  const currentPage = this.get("page");
502
504
  if (currentPage && `${currentPage.id}` === `${newConfig.id}`) this.set("page", newConfig);
503
505
  }
@@ -751,7 +753,7 @@ var Editor = class extends BaseService {
751
753
  */
752
754
  async moveToContainer(config, targetId, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
753
755
  const isBatch = Array.isArray(config);
754
- const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
756
+ const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
755
757
  if (configs.length === 0) throw new Error("没有可移动的节点");
756
758
  this.captureSelectionBeforeOp();
757
759
  const target = this.getNodeById(targetId, false);
@@ -1225,7 +1227,7 @@ var Editor = class extends BaseService {
1225
1227
  getPageOfNode(id) {
1226
1228
  const { node, page } = this.getNodeInfo(id, false);
1227
1229
  if (page) return page;
1228
- if (node && (isPage(node) || isPageFragment(node))) return node;
1230
+ if (node && isPageOrFragment(node)) return node;
1229
1231
  return null;
1230
1232
  }
1231
1233
  captureSelectionBeforeOp() {
@@ -247,7 +247,7 @@ var History = class extends BaseService {
247
247
  * 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
248
248
  *
249
249
  * 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
250
- * 对 isPage / isPageFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
250
+ * 对 isPageOrFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
251
251
  * 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
252
252
  *
253
253
  * 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
@@ -2,7 +2,7 @@ import { KeyBindingCommand } from "../type.js";
2
2
  import BaseService from "./BaseService.js";
3
3
  import editor_default from "./editor.js";
4
4
  import ui_default from "./ui.js";
5
- import { isPage, isPageFragment } from "@tmagic/utils";
5
+ import { isPageOrFragment } from "@tmagic/utils";
6
6
  import KeyController from "keycon";
7
7
  //#region packages/editor/src/services/keybinding.ts
8
8
  var Keybinding = class extends BaseService {
@@ -12,7 +12,7 @@ var Keybinding = class extends BaseService {
12
12
  commands = {
13
13
  [KeyBindingCommand.DELETE_NODE]: () => {
14
14
  const nodes = editor_default.get("nodes");
15
- if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
15
+ if (!nodes || isPageOrFragment(nodes[0])) return;
16
16
  editor_default.remove(nodes, { historySource: "shortcut" });
17
17
  },
18
18
  [KeyBindingCommand.COPY_NODE]: () => {
@@ -21,7 +21,7 @@ var Keybinding = class extends BaseService {
21
21
  },
22
22
  [KeyBindingCommand.CUT_NODE]: () => {
23
23
  const nodes = editor_default.get("nodes");
24
- if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
24
+ if (!nodes || isPageOrFragment(nodes[0])) return;
25
25
  editor_default.copy(nodes);
26
26
  editor_default.remove(nodes, { historySource: "shortcut" });
27
27
  },
@@ -1,5 +1,5 @@
1
1
  import { COPY_STORAGE_KEY } from "./editor.js";
2
- import { calcValueByFontsize, isPage, isPageFragment } from "@tmagic/utils";
2
+ import { calcValueByFontsize, isPage, isPageOrFragment } from "@tmagic/utils";
3
3
  import { NodeType, cloneDeep } from "@tmagic/core";
4
4
  import { computed, markRaw } from "vue";
5
5
  import { CopyDocument, Delete, DocumentCopy } from "@element-plus/icons-vue";
@@ -15,7 +15,7 @@ var useDeleteMenu = (historySource) => ({
15
15
  icon: Delete,
16
16
  display: ({ editorService }) => {
17
17
  const node = editorService.get("node");
18
- return node?.type !== NodeType.ROOT && !isPage(node) && !isPageFragment(node);
18
+ return node?.type !== NodeType.ROOT && !isPageOrFragment(node);
19
19
  },
20
20
  handler: ({ editorService }) => {
21
21
  const nodes = editorService.get("nodes");
@@ -0,0 +1,114 @@
1
+ import { error } from "../logger.js";
2
+ import WorkerWrapper from "./worker.js";
3
+ import serialize from "serialize-javascript";
4
+ //#region packages/editor/src/utils/dep/collect-worker-client.ts
5
+ /**
6
+ * 依赖收集 worker 的主线程客户端
7
+ *
8
+ * 依赖收集需要深度遍历整棵节点树并对每个属性做匹配,节点/数据源多时会长时间占用主线程导致页面卡死,
9
+ * 这里把遍历匹配放到 worker 中执行,主线程只负责把结果写回 target。
10
+ *
11
+ * 全量收集与增量收集共用一个常驻 worker:增量收集调用频繁(每次节点更新都会触发),
12
+ * 按次创建 worker 的启动开销无法接受,也容易漏掉销毁导致线程泄漏。
13
+ */
14
+ var CollectWorkerClient = class {
15
+ worker = null;
16
+ seed = 0;
17
+ pending = /* @__PURE__ */ new Map();
18
+ get isSupported() {
19
+ return typeof Worker !== "undefined";
20
+ }
21
+ /**
22
+ * 在 worker 中全量收集整个 DSL 的依赖,target 由 worker 根据 DSL 重建
23
+ * @returns 收集结果,返回 null 表示 worker 不可用或执行失败
24
+ */
25
+ collectDsl(dsl) {
26
+ return this.request((id) => ({
27
+ id,
28
+ dsl: serialize(dsl)
29
+ })).then((response) => response?.deps || null);
30
+ }
31
+ /**
32
+ * 在 worker 中增量收集指定节点的依赖,target 以可序列化描述的形式传入
33
+ * @returns 收集结果,返回 null 表示 worker 不可用或执行失败,调用方需要回退到主线程收集
34
+ */
35
+ collect(payload) {
36
+ return this.request((id) => ({
37
+ id,
38
+ payload: serialize(payload)
39
+ })).then((response) => response ? {
40
+ deps: response.deps || {},
41
+ nodeIds: response.nodeIds || []
42
+ } : null);
43
+ }
44
+ terminate() {
45
+ this.worker?.terminate();
46
+ this.worker = null;
47
+ this.settleAllPending();
48
+ }
49
+ /**
50
+ * 丢弃在途请求并重建 worker
51
+ * clearIdleTasks / reset 中断收集时调用,避免已 abort 的长任务继续占着常驻 worker 拖慢后续收集
52
+ */
53
+ abort() {
54
+ this.terminate();
55
+ }
56
+ /**
57
+ * worker 按收到的顺序逐个处理请求,因此请求间不会互相打断,用 id 把结果分发回各自的调用方
58
+ */
59
+ request(createRequest) {
60
+ const worker = this.getWorker();
61
+ if (!worker) return Promise.resolve(null);
62
+ this.seed += 1;
63
+ const id = this.seed;
64
+ return new Promise((resolve) => {
65
+ this.pending.set(id, resolve);
66
+ try {
67
+ worker.postMessage(createRequest(id));
68
+ } catch (e) {
69
+ error("magic editor: 依赖收集 worker 通信失败", e);
70
+ this.pending.delete(id);
71
+ resolve(null);
72
+ }
73
+ });
74
+ }
75
+ getWorker() {
76
+ if (!this.isSupported) return null;
77
+ if (this.worker) return this.worker;
78
+ try {
79
+ const worker = new WorkerWrapper();
80
+ worker.onmessage = (e) => {
81
+ this.handleResponse(e.data);
82
+ };
83
+ worker.onerror = () => {
84
+ this.handleFatalError();
85
+ };
86
+ worker.onmessageerror = () => {
87
+ this.handleFatalError();
88
+ };
89
+ this.worker = worker;
90
+ } catch (e) {
91
+ error("magic editor: 依赖收集 worker 创建失败", e);
92
+ return null;
93
+ }
94
+ return this.worker;
95
+ }
96
+ handleResponse(response) {
97
+ const resolve = this.pending.get(response?.id);
98
+ if (!resolve) return;
99
+ this.pending.delete(response.id);
100
+ resolve(response.failed ? null : response);
101
+ }
102
+ handleFatalError() {
103
+ this.worker?.terminate();
104
+ this.worker = null;
105
+ this.settleAllPending();
106
+ }
107
+ settleAllPending() {
108
+ const resolvers = [...this.pending.values()];
109
+ this.pending.clear();
110
+ for (const resolve of resolvers) resolve(null);
111
+ }
112
+ };
113
+ //#endregion
114
+ export { CollectWorkerClient };