@embedpdf/plugin-history 2.15.1 → 3.0.0-next.16
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/LICENSE +192 -21
- package/README.md +65 -0
- package/dist/contract-B7QL_Q3D.js +11 -0
- package/dist/contract-B7QL_Q3D.js.map +1 -0
- package/dist/contract-DHipVkAd.cjs +21 -0
- package/dist/contract-DHipVkAd.cjs.map +1 -0
- package/dist/contract-J_XQyhyq.d.cts +66 -0
- package/dist/contract-J_XQyhyq.d.ts +66 -0
- package/dist/contract.cjs +4 -0
- package/dist/contract.d.cts +2 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +2 -0
- package/dist/index.cjs +356 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +348 -377
- package/dist/index.js.map +1 -1
- package/package.json +25 -57
- package/dist/lib/actions.d.ts +0 -34
- package/dist/lib/history-plugin.d.ts +0 -33
- package/dist/lib/index.d.ts +0 -8
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/reducer.d.ts +0 -5
- package/dist/lib/types.d.ts +0 -151
- package/dist/preact/adapter.d.ts +0 -1
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -10
- package/dist/preact/index.js.map +0 -1
- package/dist/react/adapter.d.ts +0 -1
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -10
- package/dist/react/index.js.map +0 -1
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/use-history.d.ts +0 -11
- package/dist/shared/index.d.ts +0 -2
- package/dist/shared-preact/hooks/index.d.ts +0 -1
- package/dist/shared-preact/hooks/use-history.d.ts +0 -11
- package/dist/shared-preact/index.d.ts +0 -2
- package/dist/shared-react/hooks/index.d.ts +0 -1
- package/dist/shared-react/hooks/use-history.d.ts +0 -11
- package/dist/shared-react/index.d.ts +0 -2
- package/dist/svelte/hooks/index.d.ts +0 -1
- package/dist/svelte/hooks/use-history.d.ts +0 -11
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -2
- package/dist/svelte/index.js +0 -10
- package/dist/svelte/index.js.map +0 -1
- package/dist/vue/hooks/index.d.ts +0 -1
- package/dist/vue/hooks/use-history.d.ts +0 -3
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/index.js +0 -10
- package/dist/vue/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,381 +1,352 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
defaultConfig: {}
|
|
11
|
-
};
|
|
12
|
-
const INIT_HISTORY_STATE = "HISTORY/INIT_STATE";
|
|
13
|
-
const CLEANUP_HISTORY_STATE = "HISTORY/CLEANUP_STATE";
|
|
14
|
-
const SET_HISTORY_DOCUMENT_STATE = "HISTORY/SET_DOCUMENT_STATE";
|
|
15
|
-
const SET_ACTIVE_HISTORY_DOCUMENT = "HISTORY/SET_ACTIVE_DOCUMENT";
|
|
16
|
-
const initHistoryState = (documentId) => ({
|
|
17
|
-
type: INIT_HISTORY_STATE,
|
|
18
|
-
payload: { documentId }
|
|
19
|
-
});
|
|
20
|
-
const cleanupHistoryState = (documentId) => ({
|
|
21
|
-
type: CLEANUP_HISTORY_STATE,
|
|
22
|
-
payload: { documentId }
|
|
1
|
+
import { n as HistoryToken, t as HISTORY_DEFAULTS } from "./contract-B7QL_Q3D.js";
|
|
2
|
+
import { definePlugin, defineState } from "@embedpdf/core";
|
|
3
|
+
import { EngineError, EngineErrorCode } from "@embedpdf/engine-core/runtime";
|
|
4
|
+
//#region src/model.ts
|
|
5
|
+
const emptyHistory = () => ({
|
|
6
|
+
undo: [],
|
|
7
|
+
redo: [],
|
|
8
|
+
merging: null,
|
|
9
|
+
nextSeq: 0
|
|
23
10
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.documentHistories = /* @__PURE__ */ new Map();
|
|
32
|
-
this.historyChange$ = createEmitter();
|
|
33
|
-
}
|
|
34
|
-
async initialize(_) {
|
|
35
|
-
this.logger.info("HistoryPlugin", "Initialize", "History plugin initialized");
|
|
36
|
-
}
|
|
37
|
-
// ─────────────────────────────────────────────────────────
|
|
38
|
-
// Document Lifecycle (from BasePlugin)
|
|
39
|
-
// ─────────────────────────────────────────────────────────
|
|
40
|
-
onDocumentLoadingStarted(documentId) {
|
|
41
|
-
this.dispatch(initHistoryState(documentId));
|
|
42
|
-
this.documentHistories.set(documentId, {
|
|
43
|
-
topicHistories: /* @__PURE__ */ new Map(),
|
|
44
|
-
globalTimeline: [],
|
|
45
|
-
globalIndex: -1
|
|
46
|
-
});
|
|
47
|
-
this.logger.debug(
|
|
48
|
-
"HistoryPlugin",
|
|
49
|
-
"DocumentOpened",
|
|
50
|
-
`Initialized history state for document: ${documentId}`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
onDocumentClosed(documentId) {
|
|
54
|
-
this.dispatch(cleanupHistoryState(documentId));
|
|
55
|
-
this.documentHistories.delete(documentId);
|
|
56
|
-
this.logger.debug(
|
|
57
|
-
"HistoryPlugin",
|
|
58
|
-
"DocumentClosed",
|
|
59
|
-
`Cleaned up history state for document: ${documentId}`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
// ─────────────────────────────────────────────────────────
|
|
63
|
-
// Helper Methods
|
|
64
|
-
// ─────────────────────────────────────────────────────────
|
|
65
|
-
getDocumentHistoryData(documentId) {
|
|
66
|
-
const id = documentId ?? this.getActiveDocumentId();
|
|
67
|
-
const data = this.documentHistories.get(id);
|
|
68
|
-
if (!data) {
|
|
69
|
-
throw new Error(`History data not found for document: ${id}`);
|
|
70
|
-
}
|
|
71
|
-
return data;
|
|
72
|
-
}
|
|
73
|
-
getDocumentHistoryState(documentId) {
|
|
74
|
-
const data = this.documentHistories.get(documentId);
|
|
75
|
-
if (!data) {
|
|
76
|
-
throw new Error(`History data not found for document: ${documentId}`);
|
|
77
|
-
}
|
|
78
|
-
const topics = {};
|
|
79
|
-
Array.from(data.topicHistories.entries()).forEach(([topic, history]) => {
|
|
80
|
-
topics[topic] = {
|
|
81
|
-
canUndo: history.currentIndex > -1,
|
|
82
|
-
canRedo: history.currentIndex < history.commands.length - 1
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
return {
|
|
86
|
-
global: {
|
|
87
|
-
canUndo: data.globalIndex > -1,
|
|
88
|
-
canRedo: data.globalIndex < data.globalTimeline.length - 1
|
|
89
|
-
},
|
|
90
|
-
topics
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
emitHistoryChange(documentId, topic) {
|
|
94
|
-
const state = this.getDocumentHistoryState(documentId);
|
|
95
|
-
this.dispatch(setHistoryDocumentState(documentId, state));
|
|
96
|
-
this.historyChange$.emit({
|
|
97
|
-
documentId,
|
|
98
|
-
topic,
|
|
99
|
-
state
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// ─────────────────────────────────────────────────────────
|
|
103
|
-
// History Operations (per document)
|
|
104
|
-
// ─────────────────────────────────────────────────────────
|
|
105
|
-
register(command, topic, documentId) {
|
|
106
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
107
|
-
if (!data.topicHistories.has(topic)) {
|
|
108
|
-
data.topicHistories.set(topic, { commands: [], currentIndex: -1 });
|
|
109
|
-
}
|
|
110
|
-
const topicHistory = data.topicHistories.get(topic);
|
|
111
|
-
topicHistory.commands.splice(topicHistory.currentIndex + 1);
|
|
112
|
-
topicHistory.commands.push(command);
|
|
113
|
-
topicHistory.currentIndex++;
|
|
114
|
-
const historyEntry = { command, topic };
|
|
115
|
-
data.globalTimeline.splice(data.globalIndex + 1);
|
|
116
|
-
data.globalTimeline.push(historyEntry);
|
|
117
|
-
data.globalIndex++;
|
|
118
|
-
command.execute();
|
|
119
|
-
this.emitHistoryChange(documentId, topic);
|
|
120
|
-
}
|
|
121
|
-
undo(topic, documentId) {
|
|
122
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
123
|
-
let affectedTopic;
|
|
124
|
-
if (topic) {
|
|
125
|
-
const topicHistory = data.topicHistories.get(topic);
|
|
126
|
-
if (topicHistory && topicHistory.currentIndex > -1) {
|
|
127
|
-
topicHistory.commands[topicHistory.currentIndex].undo();
|
|
128
|
-
topicHistory.currentIndex--;
|
|
129
|
-
affectedTopic = topic;
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
if (data.globalIndex > -1) {
|
|
133
|
-
const entry = data.globalTimeline[data.globalIndex];
|
|
134
|
-
entry.command.undo();
|
|
135
|
-
data.topicHistories.get(entry.topic).currentIndex--;
|
|
136
|
-
data.globalIndex--;
|
|
137
|
-
affectedTopic = entry.topic;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (affectedTopic) {
|
|
141
|
-
this.emitHistoryChange(documentId, affectedTopic);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
redo(topic, documentId) {
|
|
145
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
146
|
-
let affectedTopic;
|
|
147
|
-
if (topic) {
|
|
148
|
-
const topicHistory = data.topicHistories.get(topic);
|
|
149
|
-
if (topicHistory && topicHistory.currentIndex < topicHistory.commands.length - 1) {
|
|
150
|
-
topicHistory.currentIndex++;
|
|
151
|
-
topicHistory.commands[topicHistory.currentIndex].execute();
|
|
152
|
-
affectedTopic = topic;
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
if (data.globalIndex < data.globalTimeline.length - 1) {
|
|
156
|
-
data.globalIndex++;
|
|
157
|
-
const entry = data.globalTimeline[data.globalIndex];
|
|
158
|
-
entry.command.execute();
|
|
159
|
-
data.topicHistories.get(entry.topic).currentIndex++;
|
|
160
|
-
affectedTopic = entry.topic;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (affectedTopic) {
|
|
164
|
-
this.emitHistoryChange(documentId, affectedTopic);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
canUndo(topic, documentId) {
|
|
168
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
169
|
-
if (topic) {
|
|
170
|
-
const history = data.topicHistories.get(topic);
|
|
171
|
-
return !!history && history.currentIndex > -1;
|
|
172
|
-
}
|
|
173
|
-
return data.globalIndex > -1;
|
|
174
|
-
}
|
|
175
|
-
canRedo(topic, documentId) {
|
|
176
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
177
|
-
if (topic) {
|
|
178
|
-
const history = data.topicHistories.get(topic);
|
|
179
|
-
return !!history && history.currentIndex < history.commands.length - 1;
|
|
180
|
-
}
|
|
181
|
-
return data.globalIndex < data.globalTimeline.length - 1;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Purges history entries that match the given predicate based on command metadata.
|
|
185
|
-
* Used to remove commands that are no longer valid (e.g., after a permanent redaction commit).
|
|
186
|
-
*
|
|
187
|
-
* @param predicate A function that returns true for commands that should be purged
|
|
188
|
-
* @param topic If provided, only purges entries for that specific topic
|
|
189
|
-
* @param documentId The document to purge history for
|
|
190
|
-
* @returns The number of entries that were purged
|
|
191
|
-
*/
|
|
192
|
-
purgeByMetadata(predicate, topic, documentId) {
|
|
193
|
-
const data = this.getDocumentHistoryData(documentId);
|
|
194
|
-
let purgedCount = 0;
|
|
195
|
-
const shouldPurge = (command) => {
|
|
196
|
-
return predicate(command.metadata);
|
|
197
|
-
};
|
|
198
|
-
const topicsToProcess = topic ? [topic] : Array.from(data.topicHistories.keys());
|
|
199
|
-
for (const topicName of topicsToProcess) {
|
|
200
|
-
const topicHistory = data.topicHistories.get(topicName);
|
|
201
|
-
if (!topicHistory) continue;
|
|
202
|
-
const newCommands = [];
|
|
203
|
-
let indexAdjustment = 0;
|
|
204
|
-
for (let i = 0; i < topicHistory.commands.length; i++) {
|
|
205
|
-
const command = topicHistory.commands[i];
|
|
206
|
-
if (shouldPurge(command)) {
|
|
207
|
-
if (i <= topicHistory.currentIndex) {
|
|
208
|
-
indexAdjustment++;
|
|
209
|
-
}
|
|
210
|
-
purgedCount++;
|
|
211
|
-
} else {
|
|
212
|
-
newCommands.push(command);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
topicHistory.commands = newCommands;
|
|
216
|
-
topicHistory.currentIndex = Math.max(-1, topicHistory.currentIndex - indexAdjustment);
|
|
217
|
-
}
|
|
218
|
-
const newTimeline = [];
|
|
219
|
-
let globalIndexAdjustment = 0;
|
|
220
|
-
for (let i = 0; i < data.globalTimeline.length; i++) {
|
|
221
|
-
const entry = data.globalTimeline[i];
|
|
222
|
-
const matchesTopic = !topic || entry.topic === topic;
|
|
223
|
-
if (matchesTopic && shouldPurge(entry.command)) {
|
|
224
|
-
if (i <= data.globalIndex) {
|
|
225
|
-
globalIndexAdjustment++;
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
newTimeline.push(entry);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
data.globalTimeline = newTimeline;
|
|
232
|
-
data.globalIndex = Math.max(-1, data.globalIndex - globalIndexAdjustment);
|
|
233
|
-
if (purgedCount > 0) {
|
|
234
|
-
this.emitHistoryChange(documentId, topic);
|
|
235
|
-
this.logger.debug(
|
|
236
|
-
"HistoryPlugin",
|
|
237
|
-
"PurgeByMetadata",
|
|
238
|
-
`Purged ${purgedCount} history entries for document: ${documentId}${topic ? `, topic: ${topic}` : ""}`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
return purgedCount;
|
|
242
|
-
}
|
|
243
|
-
// ─────────────────────────────────────────────────────────
|
|
244
|
-
// Document Scoping
|
|
245
|
-
// ─────────────────────────────────────────────────────────
|
|
246
|
-
createHistoryScope(documentId) {
|
|
247
|
-
return {
|
|
248
|
-
register: (command, topic) => this.register(command, topic, documentId),
|
|
249
|
-
undo: (topic) => this.undo(topic, documentId),
|
|
250
|
-
redo: (topic) => this.redo(topic, documentId),
|
|
251
|
-
canUndo: (topic) => this.canUndo(topic, documentId),
|
|
252
|
-
canRedo: (topic) => this.canRedo(topic, documentId),
|
|
253
|
-
getHistoryState: () => this.getDocumentHistoryState(documentId),
|
|
254
|
-
onHistoryChange: (listener) => this.historyChange$.on((event) => {
|
|
255
|
-
if (event.documentId === documentId) {
|
|
256
|
-
listener(event.topic);
|
|
257
|
-
}
|
|
258
|
-
}),
|
|
259
|
-
purgeByMetadata: (predicate, topic) => this.purgeByMetadata(predicate, topic, documentId)
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
// ─────────────────────────────────────────────────────────
|
|
263
|
-
// Capability
|
|
264
|
-
// ─────────────────────────────────────────────────────────
|
|
265
|
-
buildCapability() {
|
|
266
|
-
return {
|
|
267
|
-
// Active document operations
|
|
268
|
-
register: (command, topic) => {
|
|
269
|
-
const documentId = this.getActiveDocumentId();
|
|
270
|
-
this.register(command, topic, documentId);
|
|
271
|
-
},
|
|
272
|
-
undo: (topic) => {
|
|
273
|
-
const documentId = this.getActiveDocumentId();
|
|
274
|
-
this.undo(topic, documentId);
|
|
275
|
-
},
|
|
276
|
-
redo: (topic) => {
|
|
277
|
-
const documentId = this.getActiveDocumentId();
|
|
278
|
-
this.redo(topic, documentId);
|
|
279
|
-
},
|
|
280
|
-
canUndo: (topic) => {
|
|
281
|
-
const documentId = this.getActiveDocumentId();
|
|
282
|
-
return this.canUndo(topic, documentId);
|
|
283
|
-
},
|
|
284
|
-
canRedo: (topic) => {
|
|
285
|
-
const documentId = this.getActiveDocumentId();
|
|
286
|
-
return this.canRedo(topic, documentId);
|
|
287
|
-
},
|
|
288
|
-
getHistoryState: () => {
|
|
289
|
-
const documentId = this.getActiveDocumentId();
|
|
290
|
-
return this.getDocumentHistoryState(documentId);
|
|
291
|
-
},
|
|
292
|
-
// Document-scoped operations
|
|
293
|
-
forDocument: (documentId) => this.createHistoryScope(documentId),
|
|
294
|
-
// Events
|
|
295
|
-
onHistoryChange: this.historyChange$.on,
|
|
296
|
-
// Purge operations
|
|
297
|
-
purgeByMetadata: (predicate, topic) => {
|
|
298
|
-
const documentId = this.getActiveDocumentId();
|
|
299
|
-
return this.purgeByMetadata(predicate, topic, documentId);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
// ─────────────────────────────────────────────────────────
|
|
304
|
-
// Lifecycle
|
|
305
|
-
// ─────────────────────────────────────────────────────────
|
|
306
|
-
async destroy() {
|
|
307
|
-
this.historyChange$.clear();
|
|
308
|
-
this.documentHistories.clear();
|
|
309
|
-
super.destroy();
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
_HistoryPlugin.id = "history";
|
|
313
|
-
let HistoryPlugin = _HistoryPlugin;
|
|
314
|
-
const initialDocumentState = {
|
|
315
|
-
global: {
|
|
316
|
-
canUndo: false,
|
|
317
|
-
canRedo: false
|
|
318
|
-
},
|
|
319
|
-
topics: {}
|
|
11
|
+
/** The stack a step takes its entry from, and the one it puts it on. */
|
|
12
|
+
const stacksOf = (direction) => direction === "undo" ? {
|
|
13
|
+
from: "undo",
|
|
14
|
+
to: "redo"
|
|
15
|
+
} : {
|
|
16
|
+
from: "redo",
|
|
17
|
+
to: "undo"
|
|
320
18
|
};
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
19
|
+
/** The entry the next step of `direction` takes, or `null`. */
|
|
20
|
+
const nextOf = (history, direction) => history[direction].at(-1) ?? null;
|
|
21
|
+
/**
|
|
22
|
+
* A new change: it joins the newest entry when typing goes on under the same key, else it is a
|
|
23
|
+
* new entry on top, and the oldest go past `limit`. Anything that could be redone is forgotten.
|
|
24
|
+
*/
|
|
25
|
+
function recorded(history, change, limit) {
|
|
26
|
+
const part = {
|
|
27
|
+
seq: history.nextSeq,
|
|
28
|
+
id: change.opId,
|
|
29
|
+
opId: change.opId,
|
|
30
|
+
forward: change.shows,
|
|
31
|
+
backward: change.undo
|
|
32
|
+
};
|
|
33
|
+
const top = history.undo.at(-1);
|
|
34
|
+
if (top && change.merge !== null && history.merging === change.merge) return {
|
|
35
|
+
undo: [...history.undo.slice(0, -1), {
|
|
36
|
+
...top,
|
|
37
|
+
parts: [...top.parts, part]
|
|
38
|
+
}],
|
|
39
|
+
redo: [],
|
|
40
|
+
merging: change.merge,
|
|
41
|
+
nextSeq: history.nextSeq + 1
|
|
42
|
+
};
|
|
43
|
+
const entry = {
|
|
44
|
+
seq: part.seq,
|
|
45
|
+
label: change.label,
|
|
46
|
+
merge: change.merge,
|
|
47
|
+
parts: [part]
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
undo: [...history.undo, entry].slice(-Math.max(1, limit)),
|
|
51
|
+
redo: [],
|
|
52
|
+
merging: change.merge,
|
|
53
|
+
nextSeq: history.nextSeq + 1
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A step was staged: the entry `seq` leaves the top of its stack for the top of the other, each
|
|
58
|
+
* part now named by the change that staged it (`opIds`, by part id).
|
|
59
|
+
*/
|
|
60
|
+
function stepped(history, direction, seq, opIds) {
|
|
61
|
+
const { from, to } = stacksOf(direction);
|
|
62
|
+
const entry = history[from].find((candidate) => candidate.seq === seq);
|
|
63
|
+
if (!entry) return history;
|
|
64
|
+
const moved = {
|
|
65
|
+
...entry,
|
|
66
|
+
parts: entry.parts.map((part) => ({
|
|
67
|
+
...part,
|
|
68
|
+
opId: opIds.get(part.id) ?? part.opId
|
|
69
|
+
}))
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
...history,
|
|
73
|
+
[from]: history[from].filter((candidate) => candidate !== entry),
|
|
74
|
+
[to]: [...history[to], moved],
|
|
75
|
+
merging: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Every entry without the parts `drop` picks, and without the entries left with none. */
|
|
79
|
+
const withoutParts = (entries, drop) => {
|
|
80
|
+
let changed = false;
|
|
81
|
+
const kept = [];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const parts = entry.parts.filter((part) => !drop(part));
|
|
84
|
+
if (parts.length === entry.parts.length) {
|
|
85
|
+
kept.push(entry);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
changed = true;
|
|
89
|
+
if (parts.length) kept.push({
|
|
90
|
+
...entry,
|
|
91
|
+
parts
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return changed ? kept : entries;
|
|
324
95
|
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The change `id` never happened (the engine refused it), or did nothing that can be undone:
|
|
98
|
+
* its part goes, from whichever stack holds it.
|
|
99
|
+
*/
|
|
100
|
+
function forgotten(history, id) {
|
|
101
|
+
const undo = withoutParts(history.undo, (part) => part.id === id);
|
|
102
|
+
const redo = withoutParts(history.redo, (part) => part.id === id);
|
|
103
|
+
return undo === history.undo && redo === history.redo ? history : {
|
|
104
|
+
...history,
|
|
105
|
+
undo,
|
|
106
|
+
redo
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The engine refused one change of a step, so that part didn't move: it goes back where it
|
|
111
|
+
* was, as `part` was before the step, into its entry there (or a new one with the entry's
|
|
112
|
+
* number, in its place), wherever a later step had put it since. Its other parts stay where
|
|
113
|
+
* the step put them.
|
|
114
|
+
*/
|
|
115
|
+
function stepRefused(history, direction, entry, part) {
|
|
116
|
+
const { from } = stacksOf(direction);
|
|
117
|
+
const { undo, redo } = forgotten(history, part.id);
|
|
118
|
+
const left = {
|
|
119
|
+
undo,
|
|
120
|
+
redo
|
|
121
|
+
};
|
|
122
|
+
const back = [...left[from]];
|
|
123
|
+
const at = back.findIndex((candidate) => candidate.seq === entry.seq);
|
|
124
|
+
if (at >= 0) {
|
|
125
|
+
const parts = [...back[at].parts, part].sort((left, right) => left.seq - right.seq);
|
|
126
|
+
back[at] = {
|
|
127
|
+
...back[at],
|
|
128
|
+
parts
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
const before = (candidate) => from === "undo" ? candidate.seq < entry.seq : candidate.seq > entry.seq;
|
|
132
|
+
const index = back.filter(before).length;
|
|
133
|
+
back.splice(index, 0, {
|
|
134
|
+
...entry,
|
|
135
|
+
parts: [part]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...history,
|
|
140
|
+
...left,
|
|
141
|
+
[from]: back,
|
|
142
|
+
merging: null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The engine can no longer undo the entry `seq` (a final change or a new version came after
|
|
147
|
+
* it, or it is too old): it goes, and so does everything recorded before it.
|
|
148
|
+
*/
|
|
149
|
+
function unavailable(history, seq) {
|
|
150
|
+
const newer = (entry) => entry.seq > seq;
|
|
151
|
+
return {
|
|
152
|
+
...history,
|
|
153
|
+
undo: history.undo.filter(newer),
|
|
154
|
+
redo: history.redo.filter(newer),
|
|
155
|
+
merging: null
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Nothing can be undone or redone. */
|
|
159
|
+
const cleared = (history) => history.undo.length === 0 && history.redo.length === 0 && history.merging === null ? history : {
|
|
160
|
+
...history,
|
|
161
|
+
undo: [],
|
|
162
|
+
redo: [],
|
|
163
|
+
merging: null
|
|
368
164
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/controller.ts
|
|
167
|
+
/** The document events after which nothing before them can be undone. */
|
|
168
|
+
const ENDS_UNDO = /* @__PURE__ */ new Set([
|
|
169
|
+
"redaction.applied",
|
|
170
|
+
"pages.flattened",
|
|
171
|
+
"signatures.completed",
|
|
172
|
+
"forms.repaired",
|
|
173
|
+
"document.versioned"
|
|
174
|
+
]);
|
|
175
|
+
/** Whether the engine refused an undo because it can't be undone any more. */
|
|
176
|
+
const isUnavailable = (error) => EngineError.is(error.cause, EngineErrorCode.UndoUnavailable);
|
|
177
|
+
/** How many of an undo's parts were left alone, in whole or in part. */
|
|
178
|
+
const skippedIn = (result) => result.items.filter((item) => item.type === "skipped" || "skipped" in item && (item.skipped?.length ?? 0) > 0).length;
|
|
179
|
+
function createHistoryController(ctx) {
|
|
180
|
+
const settings = ctx.settings();
|
|
181
|
+
const undone = ctx.events.source();
|
|
182
|
+
const undoFailed = ctx.events.source();
|
|
183
|
+
/**
|
|
184
|
+
* The changes the engine will never apply, while a step is on its way: a refused change, and
|
|
185
|
+
* a step that undid one. A step that names one is refused too, and changes nothing more.
|
|
186
|
+
*/
|
|
187
|
+
const dead = /* @__PURE__ */ new Set();
|
|
188
|
+
/** The parts forgotten while a step is on its way: a refusal of their step puts nothing back. */
|
|
189
|
+
const gone = /* @__PURE__ */ new Set();
|
|
190
|
+
let inFlight = 0;
|
|
191
|
+
const forget = (ids) => {
|
|
192
|
+
if (inFlight === 0) return;
|
|
193
|
+
for (const id of ids) gone.add(id);
|
|
194
|
+
};
|
|
195
|
+
const partsOf = (entries) => entries.flatMap((entry) => entry.parts.map((part) => part.id));
|
|
196
|
+
const clear = () => {
|
|
197
|
+
const history = ctx.state.get();
|
|
198
|
+
forget(partsOf([...history.undo, ...history.redo]));
|
|
199
|
+
ctx.state.update(cleared);
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* One step's change for one part, and what its answer does. `moot`: it named a change the
|
|
203
|
+
* engine never applied, or a part the history forgot, so its refusal says nothing new.
|
|
204
|
+
*/
|
|
205
|
+
const answerOf = (direction, entry, part, change) => change.result.then((result) => {
|
|
206
|
+
if (!result.meta.undoable) {
|
|
207
|
+
forget([part.id]);
|
|
208
|
+
ctx.state.update(forgotten, part.id);
|
|
209
|
+
}
|
|
210
|
+
return { skipped: skippedIn(result) };
|
|
211
|
+
}, (error) => {
|
|
212
|
+
dead.add(change.opId);
|
|
213
|
+
if (dead.has(part.opId) || gone.has(part.id)) return { moot: true };
|
|
214
|
+
if (isUnavailable(error)) {
|
|
215
|
+
const history = ctx.state.get();
|
|
216
|
+
const older = [...history.undo, ...history.redo].filter((other) => other.seq <= entry.seq);
|
|
217
|
+
forget(partsOf(older));
|
|
218
|
+
ctx.state.update(unavailable, entry.seq);
|
|
219
|
+
} else ctx.state.update(stepRefused, direction, entry, part);
|
|
220
|
+
return { error };
|
|
221
|
+
});
|
|
222
|
+
/**
|
|
223
|
+
* Take one step: the undo of each part of the next entry, as one change each (undo goes
|
|
224
|
+
* newest first, redo oldest first), staged now, so the views show it at once.
|
|
225
|
+
*/
|
|
226
|
+
const step = (direction) => {
|
|
227
|
+
ctx.changes.sendHolds();
|
|
228
|
+
const entry = nextOf(ctx.state.get(), direction);
|
|
229
|
+
if (!entry) return;
|
|
230
|
+
const parts = direction === "undo" ? [...entry.parts].reverse() : entry.parts;
|
|
231
|
+
const staged = [];
|
|
232
|
+
for (const part of parts) try {
|
|
233
|
+
const change = ctx.changes.stage({
|
|
234
|
+
label: entry.label,
|
|
235
|
+
undoOf: part.opId,
|
|
236
|
+
shows: direction === "undo" ? part.backward : part.forward
|
|
237
|
+
});
|
|
238
|
+
staged.push({
|
|
239
|
+
part,
|
|
240
|
+
change
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (staged.length === 0) throw error;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
ctx.state.update(stepped, direction, entry.seq, new Map(staged.map(({ part, change }) => [part.id, change.opId])));
|
|
247
|
+
inFlight += 1;
|
|
248
|
+
Promise.all(staged.map(({ part, change }) => answerOf(direction, entry, part, change))).then((answers) => {
|
|
249
|
+
inFlight -= 1;
|
|
250
|
+
if (inFlight === 0) {
|
|
251
|
+
dead.clear();
|
|
252
|
+
gone.clear();
|
|
253
|
+
}
|
|
254
|
+
const redo = direction === "redo";
|
|
255
|
+
if (answers.every((answer) => "moot" in answer)) return;
|
|
256
|
+
const failure = answers.find((answer) => "error" in answer);
|
|
257
|
+
if (failure && "error" in failure) {
|
|
258
|
+
undoFailed.emit({
|
|
259
|
+
label: entry.label,
|
|
260
|
+
redo,
|
|
261
|
+
reason: isUnavailable(failure.error) ? "unavailable" : "refused",
|
|
262
|
+
error: failure.error
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const skipped = answers.reduce((sum, answer) => sum + ("skipped" in answer ? answer.skipped : 0), 0);
|
|
267
|
+
undone.emit({
|
|
268
|
+
label: entry.label,
|
|
269
|
+
redo,
|
|
270
|
+
skipped
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
/** A change still being typed: what undo would undo before anything in the history. */
|
|
275
|
+
const openHold = () => ctx.changes.pending().find((change) => change.held && change.history) ?? null;
|
|
276
|
+
return {
|
|
277
|
+
api: {
|
|
278
|
+
...settings.api,
|
|
279
|
+
undo: () => step("undo"),
|
|
280
|
+
redo: () => step("redo"),
|
|
281
|
+
canUndo: () => openHold() !== null || ctx.state.get().undo.length > 0,
|
|
282
|
+
canRedo: () => openHold() === null && ctx.state.get().redo.length > 0,
|
|
283
|
+
getUndoLabel: () => openHold()?.label ?? nextOf(ctx.state.get(), "undo")?.label ?? null,
|
|
284
|
+
getRedoLabel: () => openHold() === null ? nextOf(ctx.state.get(), "redo")?.label ?? null : null,
|
|
285
|
+
clear,
|
|
286
|
+
onUndone: undone.on,
|
|
287
|
+
onUndoFailed: undoFailed.on
|
|
288
|
+
},
|
|
289
|
+
connect() {
|
|
290
|
+
ctx.listen(ctx.changes.onStaged, (change) => {
|
|
291
|
+
if (change.undoOf !== null || !change.history) return;
|
|
292
|
+
ctx.state.update(recorded, change, settings.get().limit);
|
|
293
|
+
});
|
|
294
|
+
ctx.listen(ctx.changes.onSettled, (settled) => {
|
|
295
|
+
const { change } = settled;
|
|
296
|
+
if (change.undoOf !== null || !change.history) return;
|
|
297
|
+
if (settled.status === "refused" || !settled.result.meta.undoable) {
|
|
298
|
+
if (settled.status === "refused" && inFlight > 0) dead.add(change.opId);
|
|
299
|
+
forget([change.opId]);
|
|
300
|
+
ctx.state.update(forgotten, change.opId);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
ctx.listen(ctx.doc.events, (event) => {
|
|
304
|
+
if (ENDS_UNDO.has(event.type)) clear();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/history.plugin.ts
|
|
311
|
+
/**
|
|
312
|
+
* Undo and redo, one history per open document, of what this session changes through the
|
|
313
|
+
* viewer's plugins: every change they stage on the document's change queue is a step. No
|
|
314
|
+
* dependencies and no engine state of its own. `config` is the settings the app registers,
|
|
315
|
+
* over {@link HISTORY_DEFAULTS}.
|
|
316
|
+
*/
|
|
317
|
+
const historyPlugin = (config) => definePlugin({
|
|
318
|
+
id: "history",
|
|
319
|
+
token: HistoryToken,
|
|
320
|
+
scope: "document",
|
|
321
|
+
state: emptyHistory,
|
|
322
|
+
settings: {
|
|
323
|
+
defaults: HISTORY_DEFAULTS,
|
|
324
|
+
registered: config
|
|
325
|
+
},
|
|
326
|
+
create: createHistoryController
|
|
327
|
+
});
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/state.ts
|
|
330
|
+
/**
|
|
331
|
+
* The history page's State table as code: what `useHistoryState()` returns,
|
|
332
|
+
* and the same fields in every other framework. What the undo and redo
|
|
333
|
+
* buttons read.
|
|
334
|
+
*/
|
|
335
|
+
const historyState = defineState(HistoryToken, {
|
|
336
|
+
read: (history) => ({
|
|
337
|
+
canUndo: history.canUndo(),
|
|
338
|
+
canRedo: history.canRedo(),
|
|
339
|
+
undoLabel: history.getUndoLabel(),
|
|
340
|
+
redoLabel: history.getRedoLabel()
|
|
341
|
+
}),
|
|
342
|
+
empty: {
|
|
343
|
+
canUndo: false,
|
|
344
|
+
canRedo: false,
|
|
345
|
+
undoLabel: null,
|
|
346
|
+
redoLabel: null
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
//#endregion
|
|
350
|
+
export { HISTORY_DEFAULTS, HistoryToken, historyPlugin, historyState };
|
|
351
|
+
|
|
352
|
+
//# sourceMappingURL=index.js.map
|