@tmagic/editor 1.8.0-beta.4 → 1.8.0-beta.5
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/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/index.js +6 -3
- package/dist/es/initService.js +1 -1
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -55
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +333 -211
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
- package/dist/es/layouts/history-list/composables.js +73 -32
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/services/codeBlock.js +85 -37
- package/dist/es/services/dataSource.js +62 -26
- package/dist/es/services/editor.js +243 -100
- package/dist/es/services/history.js +270 -206
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +49 -6
- package/dist/es/utils/editor.js +35 -1
- package/dist/es/utils/history.js +223 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +49 -6
- package/dist/tmagic-editor.umd.cjs +1799 -905
- package/package.json +7 -7
- package/src/components/CompareForm.vue +3 -1
- package/src/components/ToolButton.vue +2 -2
- package/src/index.ts +1 -0
- package/src/initService.ts +1 -1
- package/src/layouts/Framework.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +34 -71
- package/src/layouts/history-list/BucketTab.vue +46 -54
- package/src/layouts/history-list/GroupRow.vue +146 -111
- package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
- package/src/layouts/history-list/HistoryListPanel.vue +296 -113
- package/src/layouts/history-list/InitialRow.vue +25 -9
- package/src/layouts/history-list/PageTab.vue +57 -51
- package/src/layouts/history-list/composables.ts +157 -36
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/sidebar/Sidebar.vue +6 -1
- package/src/services/codeBlock.ts +107 -37
- package/src/services/dataSource.ts +89 -40
- package/src/services/editor.ts +370 -134
- package/src/services/history.ts +305 -203
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +72 -5
- package/src/theme/page-bar.scss +0 -4
- package/src/type.ts +167 -63
- package/src/utils/editor.ts +41 -1
- package/src/utils/history.ts +298 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +783 -291
|
@@ -1,172 +1,19 @@
|
|
|
1
1
|
import BaseService from "./BaseService.js";
|
|
2
|
+
import { getEditorConfig } from "../utils/config.js";
|
|
2
3
|
import { UndoRedo } from "../utils/undo-redo.js";
|
|
4
|
+
import { createStackStep, deserializeStacks, getOrCreateStack, markStackSaved, mergePageSteps, mergeStackSteps, serializeStacks, undoFloor } from "../utils/history.js";
|
|
5
|
+
import { idbGet, idbSet } from "../utils/indexed-db.js";
|
|
6
|
+
import editor_default from "./editor.js";
|
|
7
|
+
import { guid } from "@tmagic/utils";
|
|
3
8
|
import { reactive } from "vue";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const groups = [];
|
|
13
|
-
let current = null;
|
|
14
|
-
const currentIndex = cursor - 1;
|
|
15
|
-
list.forEach((step, index) => {
|
|
16
|
-
const opType = History.detectOpType(step.oldContent, step.newContent);
|
|
17
|
-
const applied = index < cursor;
|
|
18
|
-
const isCurrent = index === currentIndex;
|
|
19
|
-
if (opType === "update" && current?.opType === "update") {
|
|
20
|
-
current.steps.push({
|
|
21
|
-
step,
|
|
22
|
-
index,
|
|
23
|
-
applied,
|
|
24
|
-
isCurrent
|
|
25
|
-
});
|
|
26
|
-
current.applied = applied;
|
|
27
|
-
if (isCurrent) current.isCurrent = true;
|
|
28
|
-
} else {
|
|
29
|
-
current = {
|
|
30
|
-
kind: "code-block",
|
|
31
|
-
id: codeBlockId,
|
|
32
|
-
opType,
|
|
33
|
-
steps: [{
|
|
34
|
-
step,
|
|
35
|
-
index,
|
|
36
|
-
applied,
|
|
37
|
-
isCurrent
|
|
38
|
-
}],
|
|
39
|
-
applied,
|
|
40
|
-
isCurrent
|
|
41
|
-
};
|
|
42
|
-
groups.push(current);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
return groups;
|
|
46
|
-
}
|
|
47
|
-
static mergeDataSourceSteps(dataSourceId, list, cursor) {
|
|
48
|
-
const groups = [];
|
|
49
|
-
let current = null;
|
|
50
|
-
const currentIndex = cursor - 1;
|
|
51
|
-
list.forEach((step, index) => {
|
|
52
|
-
const opType = History.detectOpType(step.oldSchema, step.newSchema);
|
|
53
|
-
const applied = index < cursor;
|
|
54
|
-
const isCurrent = index === currentIndex;
|
|
55
|
-
if (opType === "update" && current?.opType === "update") {
|
|
56
|
-
current.steps.push({
|
|
57
|
-
step,
|
|
58
|
-
index,
|
|
59
|
-
applied,
|
|
60
|
-
isCurrent
|
|
61
|
-
});
|
|
62
|
-
current.applied = applied;
|
|
63
|
-
if (isCurrent) current.isCurrent = true;
|
|
64
|
-
} else {
|
|
65
|
-
current = {
|
|
66
|
-
kind: "data-source",
|
|
67
|
-
id: dataSourceId,
|
|
68
|
-
opType,
|
|
69
|
-
steps: [{
|
|
70
|
-
step,
|
|
71
|
-
index,
|
|
72
|
-
applied,
|
|
73
|
-
isCurrent
|
|
74
|
-
}],
|
|
75
|
-
applied,
|
|
76
|
-
isCurrent
|
|
77
|
-
};
|
|
78
|
-
groups.push(current);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
return groups;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* 根据 old/new 是否为 null 推断 opType(与 push 时的约定一致)。
|
|
85
|
-
*/
|
|
86
|
-
static detectOpType(oldVal, newVal) {
|
|
87
|
-
if (oldVal === null && newVal !== null) return "add";
|
|
88
|
-
if (oldVal !== null && newVal === null) return "remove";
|
|
89
|
-
return "update";
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* 把页面栈拆成若干 group:
|
|
93
|
-
* - 单节点的 'update' 按 targetId 与相邻同 targetId 的 update 合并到一个 group;
|
|
94
|
-
* - 'add' / 'remove' 始终独立成组(语义上是结构变更,不应被收纳进单节点修改组);
|
|
95
|
-
* - 多节点 'update'(如批量改属性)也独立成组(无明确单一目标,避免误合并)。
|
|
96
|
-
*/
|
|
97
|
-
static mergePageSteps(pageId, list, cursor) {
|
|
98
|
-
const groups = [];
|
|
99
|
-
let current = null;
|
|
100
|
-
const currentIndex = cursor - 1;
|
|
101
|
-
list.forEach((step, index) => {
|
|
102
|
-
const applied = index < cursor;
|
|
103
|
-
const isCurrent = index === currentIndex;
|
|
104
|
-
const targetId = History.detectPageTargetId(step);
|
|
105
|
-
const targetName = History.detectPageTargetName(step);
|
|
106
|
-
const entry = {
|
|
107
|
-
step,
|
|
108
|
-
index,
|
|
109
|
-
applied,
|
|
110
|
-
isCurrent
|
|
111
|
-
};
|
|
112
|
-
const mergeable = step.opType === "update" && targetId !== void 0;
|
|
113
|
-
if (mergeable && current?.opType === "update" && current.targetId === targetId) {
|
|
114
|
-
current.steps.push(entry);
|
|
115
|
-
current.applied = applied;
|
|
116
|
-
if (isCurrent) current.isCurrent = true;
|
|
117
|
-
if (targetName) current.targetName = targetName;
|
|
118
|
-
} else {
|
|
119
|
-
current = {
|
|
120
|
-
kind: "page",
|
|
121
|
-
pageId,
|
|
122
|
-
opType: step.opType,
|
|
123
|
-
targetId: mergeable ? targetId : void 0,
|
|
124
|
-
targetName,
|
|
125
|
-
steps: [entry],
|
|
126
|
-
applied,
|
|
127
|
-
isCurrent
|
|
128
|
-
};
|
|
129
|
-
groups.push(current);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
return groups;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* 解析 StepValue 中的"目标节点 id"用于合并:
|
|
136
|
-
* - 单节点 update:取唯一一项 updatedItems 的节点 id;
|
|
137
|
-
* - 其它情形(多节点 update / add / remove):返回 undefined,表示不参与合并。
|
|
138
|
-
*/
|
|
139
|
-
static detectPageTargetId(step) {
|
|
140
|
-
if (step.opType !== "update") return void 0;
|
|
141
|
-
const items = step.updatedItems;
|
|
142
|
-
if (items?.length !== 1) return void 0;
|
|
143
|
-
return items[0].newNode?.id ?? items[0].oldNode?.id;
|
|
144
|
-
}
|
|
145
|
-
/** 解析 StepValue 中的目标节点可读名(用于 UI 展示)。 */
|
|
146
|
-
static detectPageTargetName(step) {
|
|
147
|
-
if (step.opType === "update") {
|
|
148
|
-
const items = step.updatedItems;
|
|
149
|
-
if (items?.length === 1) {
|
|
150
|
-
const node = items[0].newNode || items[0].oldNode;
|
|
151
|
-
return node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : void 0);
|
|
152
|
-
}
|
|
153
|
-
return items?.length ? `${items.length} 个节点` : void 0;
|
|
154
|
-
}
|
|
155
|
-
if (step.opType === "add") {
|
|
156
|
-
if (step.nodes?.length === 1) {
|
|
157
|
-
const n = step.nodes[0];
|
|
158
|
-
return n.name || n.type || `${n.id}`;
|
|
159
|
-
}
|
|
160
|
-
return step.nodes?.length ? `${step.nodes.length} 个节点` : void 0;
|
|
161
|
-
}
|
|
162
|
-
if (step.opType === "remove") {
|
|
163
|
-
if (step.removedItems?.length === 1) {
|
|
164
|
-
const n = step.removedItems[0].node;
|
|
165
|
-
return n.name || n.type || `${n.id}`;
|
|
166
|
-
}
|
|
167
|
-
return step.removedItems?.length ? `${step.removedItems.length} 个节点` : void 0;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
9
|
+
import serialize from "serialize-javascript";
|
|
10
|
+
//#region packages/editor/src/services/history.ts
|
|
11
|
+
/** 历史记录持久化快照的默认存储位置与结构版本。 */
|
|
12
|
+
var DEFAULT_DB_NAME = "tmagic-editor";
|
|
13
|
+
var DEFAULT_STORE_NAME = "history";
|
|
14
|
+
var DEFAULT_KEY = "default";
|
|
15
|
+
var PERSIST_VERSION = 1;
|
|
16
|
+
var History = class extends BaseService {
|
|
170
17
|
state = reactive({
|
|
171
18
|
pageSteps: {},
|
|
172
19
|
pageId: void 0,
|
|
@@ -206,6 +53,53 @@ var history_default = new class History extends BaseService {
|
|
|
206
53
|
this.state.dataSourceState = {};
|
|
207
54
|
}
|
|
208
55
|
/**
|
|
56
|
+
* 为指定页面 / 页面片种入一条「初始基线」记录(如加载 DSL 时的「初始 / 加载」基线)。
|
|
57
|
+
*
|
|
58
|
+
* 该记录是一条 `opType: 'initial'` 的 {@link StepValue},作为页面历史栈 **index 0 的固定底线**:
|
|
59
|
+
* - 它是一条真实入栈的 step(随栈一起持久化),但被钉为撤销/回滚的下限——cursor 永不低于它,
|
|
60
|
+
* 因此不会被 undo / goto / revert 触达(详见 {@link undo} / {@link setCanUndoRedo});
|
|
61
|
+
* - 历史面板把它过滤出分组列表(见 {@link getPageHistoryGroups}),改由底部「初始」行展示。
|
|
62
|
+
*
|
|
63
|
+
* 仅当目标页面栈为空时种入(保证 initial 一定位于 index 0);已存在 initial 底线时默认不重复种入,
|
|
64
|
+
* 传 `force=true` 且栈为空时按新基线种入。
|
|
65
|
+
*/
|
|
66
|
+
setPageMarker(pageId, options = {}) {
|
|
67
|
+
if (pageId === void 0 || pageId === null || `${pageId}` === "") return null;
|
|
68
|
+
const existing = this.getPageMarker(pageId);
|
|
69
|
+
if (existing) return existing;
|
|
70
|
+
const stack = getOrCreateStack(this.state.pageSteps, pageId);
|
|
71
|
+
if (stack.getLength() > 0) return null;
|
|
72
|
+
const marker = {
|
|
73
|
+
uuid: guid(),
|
|
74
|
+
opType: "initial",
|
|
75
|
+
diff: [],
|
|
76
|
+
data: {
|
|
77
|
+
name: options.name || "",
|
|
78
|
+
id: pageId
|
|
79
|
+
},
|
|
80
|
+
selectedBefore: [],
|
|
81
|
+
selectedAfter: [],
|
|
82
|
+
modifiedNodeIds: /* @__PURE__ */ new Map(),
|
|
83
|
+
historyDescription: options.description || "未修改的初始状态",
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
...options.source ? { source: options.source } : {}
|
|
86
|
+
};
|
|
87
|
+
stack.pushElement(marker);
|
|
88
|
+
if (`${pageId}` === `${this.state.pageId}`) this.setCanUndoRedo();
|
|
89
|
+
this.emit("page-marker-change", marker);
|
|
90
|
+
return marker;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 读取指定页面(缺省当前活动页)的初始基线 step(页面栈 index 0 且 `opType: 'initial'`);
|
|
94
|
+
* 不存在时返回 undefined。
|
|
95
|
+
*/
|
|
96
|
+
getPageMarker(pageId) {
|
|
97
|
+
const targetPageId = pageId ?? this.state.pageId;
|
|
98
|
+
if (!targetPageId) return void 0;
|
|
99
|
+
const first = this.state.pageSteps[targetPageId]?.getElementList()[0];
|
|
100
|
+
return first?.opType === "initial" ? first : void 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
209
103
|
* 把一条步骤推入指定页面的栈;不指定 pageId 时落到当前活动页。
|
|
210
104
|
*
|
|
211
105
|
* 跨页操作(例如 `moveToContainer` 把节点搬到其它页)必须显式传入 `pageId`,
|
|
@@ -214,11 +108,31 @@ var history_default = new class History extends BaseService {
|
|
|
214
108
|
push(state, pageId) {
|
|
215
109
|
const undoRedo = this.getUndoRedo(pageId);
|
|
216
110
|
if (!undoRedo) return null;
|
|
111
|
+
if (state.uuid === void 0) state.uuid = guid();
|
|
217
112
|
if (state.timestamp === void 0) state.timestamp = Date.now();
|
|
218
113
|
undoRedo.pushElement(state);
|
|
219
114
|
if (pageId === void 0 || `${pageId}` === `${this.state.pageId}`) this.emit("change", state);
|
|
220
115
|
return state;
|
|
221
116
|
}
|
|
117
|
+
/** 读取指定页面(缺省当前活动页)历史栈当前游标所在的 step(cursor - 1);无则返回 null。 */
|
|
118
|
+
getCurrentPageStep(pageId) {
|
|
119
|
+
const targetPageId = pageId ?? this.state.pageId;
|
|
120
|
+
if (!targetPageId) return null;
|
|
121
|
+
return this.state.pageSteps[targetPageId]?.getCurrentElement() ?? null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 用 `state` 替换指定页面栈当前游标所在的 step(并丢弃其后的重做尾部),游标不变。
|
|
125
|
+
* 用于「连续 set root 记录合并」等就地替换最新一条的场景;替换成功后按需刷新 / 通知。
|
|
126
|
+
*/
|
|
127
|
+
replaceCurrentPageStep(state, pageId) {
|
|
128
|
+
const undoRedo = this.getUndoRedo(pageId);
|
|
129
|
+
if (!undoRedo) return null;
|
|
130
|
+
if (state.uuid === void 0) state.uuid = guid();
|
|
131
|
+
if (state.timestamp === void 0) state.timestamp = Date.now();
|
|
132
|
+
if (!undoRedo.replaceCurrentElement(state)) return null;
|
|
133
|
+
this.emit("change", state);
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
222
136
|
/**
|
|
223
137
|
* 推入一条代码块变更记录(与页面/节点完全无关),按 `codeBlockId` 维度独立一份 UndoRedo 栈。
|
|
224
138
|
*
|
|
@@ -229,17 +143,15 @@ var history_default = new class History extends BaseService {
|
|
|
229
143
|
* - 不直接驱动 codeBlockService,调用方负责实际写回。
|
|
230
144
|
*/
|
|
231
145
|
pushCodeBlock(codeBlockId, payload) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
|
|
237
|
-
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : void 0,
|
|
146
|
+
const step = createStackStep(codeBlockId, {
|
|
147
|
+
oldValue: payload.oldContent,
|
|
148
|
+
newValue: payload.newContent,
|
|
149
|
+
changeRecords: payload.changeRecords,
|
|
238
150
|
historyDescription: payload.historyDescription,
|
|
239
|
-
source: payload.source
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
151
|
+
source: payload.source
|
|
152
|
+
});
|
|
153
|
+
if (!step) return null;
|
|
154
|
+
getOrCreateStack(this.state.codeBlockState, codeBlockId).pushElement(step);
|
|
243
155
|
this.emit("code-block-history-change", codeBlockId, step);
|
|
244
156
|
return step;
|
|
245
157
|
}
|
|
@@ -248,17 +160,15 @@ var history_default = new class History extends BaseService {
|
|
|
248
160
|
* 行为同 pushCodeBlock(新增 oldSchema=null;删除 newSchema=null)。
|
|
249
161
|
*/
|
|
250
162
|
pushDataSource(dataSourceId, payload) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
|
256
|
-
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : void 0,
|
|
163
|
+
const step = createStackStep(dataSourceId, {
|
|
164
|
+
oldValue: payload.oldSchema,
|
|
165
|
+
newValue: payload.newSchema,
|
|
166
|
+
changeRecords: payload.changeRecords,
|
|
257
167
|
historyDescription: payload.historyDescription,
|
|
258
|
-
source: payload.source
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.
|
|
168
|
+
source: payload.source
|
|
169
|
+
});
|
|
170
|
+
if (!step) return null;
|
|
171
|
+
getOrCreateStack(this.state.dataSourceState, dataSourceId).pushElement(step);
|
|
262
172
|
this.emit("data-source-history-change", dataSourceId, step);
|
|
263
173
|
return step;
|
|
264
174
|
}
|
|
@@ -313,6 +223,7 @@ var history_default = new class History extends BaseService {
|
|
|
313
223
|
undo() {
|
|
314
224
|
const undoRedo = this.getUndoRedo();
|
|
315
225
|
if (!undoRedo) return null;
|
|
226
|
+
if (undoRedo.getCursor() <= undoFloor(undoRedo)) return null;
|
|
316
227
|
const state = undoRedo.undo();
|
|
317
228
|
this.emit("change", state);
|
|
318
229
|
return state;
|
|
@@ -330,6 +241,127 @@ var history_default = new class History extends BaseService {
|
|
|
330
241
|
this.removeAllPlugins();
|
|
331
242
|
}
|
|
332
243
|
/**
|
|
244
|
+
* 清空指定页面(缺省当前活动页)的历史记录栈。
|
|
245
|
+
* 仅删除撤销/重做记录,不会改动当前 DSL;清空后该页将无法再撤销/重做之前的操作。
|
|
246
|
+
*/
|
|
247
|
+
clearPage(pageId) {
|
|
248
|
+
const targetPageId = pageId ?? this.state.pageId;
|
|
249
|
+
if (!targetPageId) return;
|
|
250
|
+
const marker = this.getPageMarker(targetPageId);
|
|
251
|
+
this.state.pageSteps[targetPageId] = new UndoRedo();
|
|
252
|
+
if (marker) this.setPageMarker(targetPageId, {
|
|
253
|
+
name: marker.data?.name,
|
|
254
|
+
description: marker.historyDescription,
|
|
255
|
+
source: marker.source
|
|
256
|
+
});
|
|
257
|
+
if (`${targetPageId}` === `${this.state.pageId}`) {
|
|
258
|
+
this.setCanUndoRedo();
|
|
259
|
+
this.emit("clear-page", null);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。
|
|
264
|
+
* 仅删除撤销/重做记录,不会改动数据源本身。
|
|
265
|
+
*/
|
|
266
|
+
clearDataSource(dataSourceId) {
|
|
267
|
+
if (dataSourceId !== void 0) delete this.state.dataSourceState[dataSourceId];
|
|
268
|
+
else this.state.dataSourceState = {};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。
|
|
272
|
+
* 仅删除撤销/重做记录,不会改动代码块本身。
|
|
273
|
+
*/
|
|
274
|
+
clearCodeBlock(codeBlockId) {
|
|
275
|
+
if (codeBlockId !== void 0) delete this.state.codeBlockState[codeBlockId];
|
|
276
|
+
else this.state.codeBlockState = {};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标为 `saved`。
|
|
280
|
+
* 适用于「整体落库」场景;若只保存了其中一类,请改用更细粒度的
|
|
281
|
+
* {@link markPageSaved} / {@link markCodeBlockSaved} / {@link markDataSourceSaved}。
|
|
282
|
+
*/
|
|
283
|
+
markSaved() {
|
|
284
|
+
Object.values(this.state.pageSteps).forEach(markStackSaved);
|
|
285
|
+
Object.values(this.state.codeBlockState).forEach(markStackSaved);
|
|
286
|
+
Object.values(this.state.dataSourceState).forEach(markStackSaved);
|
|
287
|
+
this.emit("mark-saved", { kind: "all" });
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 标记指定页面(缺省为当前活动页)的历史栈当前记录为已保存。
|
|
291
|
+
* 仅影响该页面自己的栈,不波及代码块 / 数据源 / 其它页面。
|
|
292
|
+
*/
|
|
293
|
+
markPageSaved(pageId) {
|
|
294
|
+
const targetPageId = pageId ?? this.state.pageId;
|
|
295
|
+
if (!targetPageId) return;
|
|
296
|
+
markStackSaved(this.state.pageSteps[targetPageId]);
|
|
297
|
+
this.emit("mark-saved", {
|
|
298
|
+
kind: "page",
|
|
299
|
+
id: targetPageId
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** 标记指定代码块的历史栈当前记录为已保存,仅影响该代码块自己的栈。 */
|
|
303
|
+
markCodeBlockSaved(codeBlockId) {
|
|
304
|
+
if (!codeBlockId) return;
|
|
305
|
+
markStackSaved(this.state.codeBlockState[codeBlockId]);
|
|
306
|
+
this.emit("mark-saved", {
|
|
307
|
+
kind: "code-block",
|
|
308
|
+
id: codeBlockId
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** 标记指定数据源的历史栈当前记录为已保存,仅影响该数据源自己的栈。 */
|
|
312
|
+
markDataSourceSaved(dataSourceId) {
|
|
313
|
+
if (!dataSourceId) return;
|
|
314
|
+
markStackSaved(this.state.dataSourceState[dataSourceId]);
|
|
315
|
+
this.emit("mark-saved", {
|
|
316
|
+
kind: "data-source",
|
|
317
|
+
id: dataSourceId
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源)序列化后写入本地 IndexedDB。
|
|
322
|
+
*
|
|
323
|
+
* - 每个 UndoRedo 栈连同其游标、容量一并保存,恢复后可继续 undo/redo;
|
|
324
|
+
* - `key` 用于区分不同活动页 / 项目(同一 store 下可保存多份快照),缺省为 `default`;
|
|
325
|
+
* - 返回写入成功的快照对象,便于调用方记录 savedAt 等信息;
|
|
326
|
+
* - 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
|
327
|
+
*/
|
|
328
|
+
async saveToIndexedDB(options = {}) {
|
|
329
|
+
const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY, appId } = options;
|
|
330
|
+
const snapshot = {
|
|
331
|
+
version: PERSIST_VERSION,
|
|
332
|
+
pageId: this.state.pageId,
|
|
333
|
+
pageSteps: serializeStacks(this.state.pageSteps),
|
|
334
|
+
codeBlockState: serializeStacks(this.state.codeBlockState),
|
|
335
|
+
dataSourceState: serializeStacks(this.state.dataSourceState),
|
|
336
|
+
savedAt: Date.now()
|
|
337
|
+
};
|
|
338
|
+
await idbSet(this.resolveDbName(dbName, appId), storeName, key, serialize(snapshot));
|
|
339
|
+
this.emit("save-to-indexed-db", snapshot);
|
|
340
|
+
return snapshot;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
|
|
344
|
+
*
|
|
345
|
+
* - 读取到的每个栈都会经 {@link UndoRedo.fromSerialized} 还原(含游标),随后可直接 undo/redo;
|
|
346
|
+
* - 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 pageId;
|
|
347
|
+
* - 找不到对应记录时返回 null,且不改动当前状态;
|
|
348
|
+
* - 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
|
349
|
+
*/
|
|
350
|
+
async restoreFromIndexedDB(options = {}) {
|
|
351
|
+
const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY, appId } = options;
|
|
352
|
+
const raw = await idbGet(this.resolveDbName(dbName, appId), storeName, key);
|
|
353
|
+
if (!raw) return null;
|
|
354
|
+
const snapshot = typeof raw === "string" ? getEditorConfig("parseDSL")(`(${raw})`) : raw;
|
|
355
|
+
if (!snapshot) return null;
|
|
356
|
+
this.state.pageSteps = deserializeStacks(snapshot.pageSteps);
|
|
357
|
+
this.state.codeBlockState = deserializeStacks(snapshot.codeBlockState);
|
|
358
|
+
this.state.dataSourceState = deserializeStacks(snapshot.dataSourceState);
|
|
359
|
+
this.state.pageId = snapshot.pageId;
|
|
360
|
+
this.setCanUndoRedo();
|
|
361
|
+
this.emit("restore-from-indexed-db", snapshot);
|
|
362
|
+
return snapshot;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
333
365
|
* 取出当前活动页的历史步骤平铺列表(包含已应用 + 已撤销)。
|
|
334
366
|
* 列表按时间正序,最早一步在最前面。
|
|
335
367
|
* 通常 UI 应使用 `getPageHistoryGroups` 取已合并分组的版本;本方法仅为兼容/调试保留。
|
|
@@ -360,8 +392,7 @@ var history_default = new class History extends BaseService {
|
|
|
360
392
|
if (!undoRedo) return [];
|
|
361
393
|
const list = undoRedo.getElementList();
|
|
362
394
|
if (!list.length) return [];
|
|
363
|
-
|
|
364
|
-
return History.mergePageSteps(targetPageId, list, cursor);
|
|
395
|
+
return mergePageSteps(targetPageId, list, undoRedo.getCursor()).filter((group) => group.opType !== "initial");
|
|
365
396
|
}
|
|
366
397
|
/**
|
|
367
398
|
* 取出全部代码块的历史栈,按 codeBlockId 分组。
|
|
@@ -377,7 +408,7 @@ var history_default = new class History extends BaseService {
|
|
|
377
408
|
const list = undoRedo.getElementList();
|
|
378
409
|
if (!list.length) return;
|
|
379
410
|
const cursor = undoRedo.getCursor();
|
|
380
|
-
groups.push(...
|
|
411
|
+
groups.push(...mergeStackSteps("code-block", id, list, cursor));
|
|
381
412
|
});
|
|
382
413
|
return groups;
|
|
383
414
|
}
|
|
@@ -427,6 +458,44 @@ var history_default = new class History extends BaseService {
|
|
|
427
458
|
}));
|
|
428
459
|
}
|
|
429
460
|
/**
|
|
461
|
+
* 按历史记录 uuid 在指定页面(默认当前活动页)的栈中查找其索引。
|
|
462
|
+
* 找不到时返回 -1。供「按 uuid 回滚」等需要把 uuid 映射回 index 的场景使用。
|
|
463
|
+
*/
|
|
464
|
+
getPageStepIndexByUuid(uuid, pageId) {
|
|
465
|
+
if (!uuid) return -1;
|
|
466
|
+
return this.getPageStepList(pageId).findIndex((entry) => entry.step.uuid === uuid);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 按历史记录 uuid 在全部代码块栈中查找其所属 codeBlockId 与索引。
|
|
470
|
+
* 找不到时返回 null。
|
|
471
|
+
*/
|
|
472
|
+
findCodeBlockStepLocationByUuid(uuid) {
|
|
473
|
+
if (!uuid) return null;
|
|
474
|
+
for (const id of Object.keys(this.state.codeBlockState)) {
|
|
475
|
+
const index = this.getCodeBlockStepList(id).findIndex((entry) => entry.step.uuid === uuid);
|
|
476
|
+
if (index >= 0) return {
|
|
477
|
+
id,
|
|
478
|
+
index
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* 按历史记录 uuid 在全部数据源栈中查找其所属 dataSourceId 与索引。
|
|
485
|
+
* 找不到时返回 null。
|
|
486
|
+
*/
|
|
487
|
+
findDataSourceStepLocationByUuid(uuid) {
|
|
488
|
+
if (!uuid) return null;
|
|
489
|
+
for (const id of Object.keys(this.state.dataSourceState)) {
|
|
490
|
+
const index = this.getDataSourceStepList(id).findIndex((entry) => entry.step.uuid === uuid);
|
|
491
|
+
if (index >= 0) return {
|
|
492
|
+
id,
|
|
493
|
+
index
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
430
499
|
* 取出全部数据源的历史栈,按 dataSourceId 分组。同上。
|
|
431
500
|
*/
|
|
432
501
|
getDataSourceHistoryGroups() {
|
|
@@ -436,7 +505,7 @@ var history_default = new class History extends BaseService {
|
|
|
436
505
|
const list = undoRedo.getElementList();
|
|
437
506
|
if (!list.length) return;
|
|
438
507
|
const cursor = undoRedo.getCursor();
|
|
439
|
-
groups.push(...
|
|
508
|
+
groups.push(...mergeStackSteps("data-source", id, list, cursor));
|
|
440
509
|
});
|
|
441
510
|
return groups;
|
|
442
511
|
}
|
|
@@ -452,25 +521,20 @@ var history_default = new class History extends BaseService {
|
|
|
452
521
|
if (!this.state.pageSteps[targetPageId]) this.state.pageSteps[targetPageId] = new UndoRedo();
|
|
453
522
|
return this.state.pageSteps[targetPageId];
|
|
454
523
|
}
|
|
455
|
-
setCanUndoRedo() {
|
|
456
|
-
const undoRedo = this.getUndoRedo();
|
|
457
|
-
this.state.canRedo = undoRedo?.canRedo() || false;
|
|
458
|
-
this.state.canUndo = undoRedo?.canUndo() || false;
|
|
459
|
-
}
|
|
460
524
|
/**
|
|
461
|
-
*
|
|
525
|
+
* 把基础 dbName 与当前 DSL(root app)的 id 拼成最终库名,实现不同应用历史隔离。
|
|
526
|
+
* 取不到 app id(如尚未加载 DSL)时退回基础 dbName。
|
|
462
527
|
*/
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
528
|
+
resolveDbName(dbName, appId) {
|
|
529
|
+
const resolvedAppId = appId ?? editor_default.get("root")?.id;
|
|
530
|
+
return resolvedAppId ? `${dbName}-${resolvedAppId}` : dbName;
|
|
466
531
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (!this.state.dataSourceState[dataSourceId]) this.state.dataSourceState[dataSourceId] = new UndoRedo();
|
|
472
|
-
return this.state.dataSourceState[dataSourceId];
|
|
532
|
+
setCanUndoRedo() {
|
|
533
|
+
const undoRedo = this.getUndoRedo();
|
|
534
|
+
this.state.canRedo = undoRedo?.canRedo() || false;
|
|
535
|
+
this.state.canUndo = undoRedo ? undoRedo.getCursor() > undoFloor(undoRedo) : false;
|
|
473
536
|
}
|
|
474
|
-
}
|
|
537
|
+
};
|
|
538
|
+
var history_default = new History();
|
|
475
539
|
//#endregion
|
|
476
540
|
export { history_default as default };
|
package/dist/es/services/ui.js
CHANGED
|
@@ -31,6 +31,7 @@ var state = shallowReactive({
|
|
|
31
31
|
showPageListButton: true,
|
|
32
32
|
hideSlideBar: false,
|
|
33
33
|
sideBarItems: [],
|
|
34
|
+
sideBarActiveTabName: "",
|
|
34
35
|
navMenuRect: {
|
|
35
36
|
left: 0,
|
|
36
37
|
top: 0,
|
|
@@ -63,7 +64,9 @@ var Ui = class extends BaseService {
|
|
|
63
64
|
}
|
|
64
65
|
if (name === "showGuides") mask?.showGuides(value);
|
|
65
66
|
if (name === "showRule") mask?.showRule(value);
|
|
67
|
+
const preValue = state[name];
|
|
66
68
|
state[name] = value;
|
|
69
|
+
if (preValue !== value) this.emit("state-change", name, value, preValue);
|
|
67
70
|
}
|
|
68
71
|
get(name) {
|
|
69
72
|
return state[name];
|
package/dist/es/style.css
CHANGED
|
@@ -617,6 +617,23 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
617
617
|
padding: 0;
|
|
618
618
|
list-style: none;
|
|
619
619
|
}
|
|
620
|
+
.m-editor-history-list-popover .m-editor-history-list-toolbar {
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
justify-content: flex-end;
|
|
624
|
+
padding: 0 4px 4px;
|
|
625
|
+
}
|
|
626
|
+
.m-editor-history-list-popover .m-editor-history-list-clear {
|
|
627
|
+
padding: 2px 8px;
|
|
628
|
+
border-radius: 4px;
|
|
629
|
+
font-size: 12px;
|
|
630
|
+
color: #f56c6c;
|
|
631
|
+
cursor: pointer;
|
|
632
|
+
user-select: none;
|
|
633
|
+
}
|
|
634
|
+
.m-editor-history-list-popover .m-editor-history-list-clear:hover {
|
|
635
|
+
background-color: rgba(245, 108, 108, 0.12);
|
|
636
|
+
}
|
|
620
637
|
.m-editor-history-list-popover .m-editor-history-list-item {
|
|
621
638
|
display: flex;
|
|
622
639
|
align-items: center;
|
|
@@ -750,12 +767,18 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
750
767
|
}
|
|
751
768
|
.m-editor-history-list-popover .m-editor-history-list-item-index {
|
|
752
769
|
flex: 0 0 auto;
|
|
770
|
+
min-width: 30px;
|
|
771
|
+
text-align: right;
|
|
753
772
|
color: #909399;
|
|
754
773
|
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
755
774
|
font-size: 11px;
|
|
756
775
|
font-weight: 400;
|
|
757
776
|
white-space: nowrap;
|
|
758
777
|
}
|
|
778
|
+
.m-editor-history-list-popover .m-editor-history-list-group.is-merged > .m-editor-history-list-group-head > .m-editor-history-list-item-index {
|
|
779
|
+
min-width: 0;
|
|
780
|
+
text-align: left;
|
|
781
|
+
}
|
|
759
782
|
.m-editor-history-list-popover .m-editor-history-list-item-time {
|
|
760
783
|
flex: 0 0 auto;
|
|
761
784
|
color: #a8abb2;
|
|
@@ -816,6 +839,18 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
816
839
|
white-space: nowrap;
|
|
817
840
|
font-weight: 400;
|
|
818
841
|
}
|
|
842
|
+
.m-editor-history-list-popover .m-editor-history-list-item-saved {
|
|
843
|
+
flex: 0 0 auto;
|
|
844
|
+
padding: 0 6px;
|
|
845
|
+
border-radius: 8px;
|
|
846
|
+
font-size: 10px;
|
|
847
|
+
line-height: 16px;
|
|
848
|
+
color: #fff;
|
|
849
|
+
background-color: #67c23a;
|
|
850
|
+
white-space: nowrap;
|
|
851
|
+
font-weight: 500;
|
|
852
|
+
letter-spacing: 0.2px;
|
|
853
|
+
}
|
|
819
854
|
.m-editor-history-list-popover .m-editor-history-list-item-merge {
|
|
820
855
|
flex: 0 0 auto;
|
|
821
856
|
padding: 0 8px;
|
|
@@ -827,6 +862,17 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
827
862
|
font-weight: 500;
|
|
828
863
|
letter-spacing: 0.2px;
|
|
829
864
|
}
|
|
865
|
+
.m-editor-history-list-popover .m-editor-history-list-item-actions {
|
|
866
|
+
display: none;
|
|
867
|
+
flex: 0 0 auto;
|
|
868
|
+
align-items: center;
|
|
869
|
+
gap: 6px;
|
|
870
|
+
}
|
|
871
|
+
.m-editor-history-list-popover .m-editor-history-list-group-head:hover > .m-editor-history-list-item-actions,
|
|
872
|
+
.m-editor-history-list-popover .m-editor-history-list-substeps > li:hover > .m-editor-history-list-item-actions,
|
|
873
|
+
.m-editor-history-list-popover .m-editor-history-list-initial:hover > .m-editor-history-list-item-actions {
|
|
874
|
+
display: flex;
|
|
875
|
+
}
|
|
830
876
|
.m-editor-history-list-popover .m-editor-history-list-item-diff {
|
|
831
877
|
flex: 0 0 auto;
|
|
832
878
|
padding: 0 6px;
|
|
@@ -847,13 +893,13 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
847
893
|
border-radius: 2px;
|
|
848
894
|
font-size: 10px;
|
|
849
895
|
line-height: 16px;
|
|
850
|
-
color: #
|
|
851
|
-
background-color: rgba(
|
|
896
|
+
color: #529b2e;
|
|
897
|
+
background-color: rgba(103, 194, 58, 0.12);
|
|
852
898
|
cursor: pointer;
|
|
853
899
|
user-select: none;
|
|
854
900
|
}
|
|
855
901
|
.m-editor-history-list-popover .m-editor-history-list-item-goto:hover {
|
|
856
|
-
background-color: rgba(
|
|
902
|
+
background-color: rgba(103, 194, 58, 0.24);
|
|
857
903
|
}
|
|
858
904
|
.m-editor-history-list-popover .m-editor-history-list-item-revert {
|
|
859
905
|
flex: 0 0 auto;
|
|
@@ -1396,9 +1442,6 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
1396
1442
|
transition: all 0.2s ease 0s;
|
|
1397
1443
|
padding: 5px 14px;
|
|
1398
1444
|
}
|
|
1399
|
-
.page-bar-popover .menu-item .tmagic-design-button {
|
|
1400
|
-
color: #313a40;
|
|
1401
|
-
}
|
|
1402
1445
|
.page-bar-popover .menu-item:hover {
|
|
1403
1446
|
background-color: #f3f5f9;
|
|
1404
1447
|
}
|