@lexical/history 0.38.0 → 0.38.1
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 +21 -0
- package/LexicalHistory.dev.js +363 -0
- package/{src/index.ts → LexicalHistory.dev.mjs} +92 -336
- package/LexicalHistory.js +3 -3
- package/LexicalHistory.mjs +15 -0
- package/LexicalHistory.node.mjs +13 -0
- package/LexicalHistory.prod.js +9 -0
- package/LexicalHistory.prod.mjs +9 -0
- package/index.d.ts +63 -0
- package/package.json +4 -4
- package/src/__tests__/unit/LexicalHistory.test.tsx +0 -452
- /package/{flow/LexicalHistory.js.flow → LexicalHistory.js.flow} +0 -0
|
@@ -6,126 +6,59 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import { namedSignals, effect, batch, getPeerDependencyFromEditor } from '@lexical/extension';
|
|
10
|
+
import { mergeRegister } from '@lexical/utils';
|
|
11
|
+
import { defineExtension, safeCast, configExtension, UNDO_COMMAND, COMMAND_PRIORITY_EDITOR, REDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, HISTORIC_TAG, HISTORY_PUSH_TAG, HISTORY_MERGE_TAG, $isRangeSelection, $isTextNode, $isRootNode } from 'lexical';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {mergeRegister} from '@lexical/utils';
|
|
19
|
-
import {
|
|
20
|
-
$isRangeSelection,
|
|
21
|
-
$isRootNode,
|
|
22
|
-
$isTextNode,
|
|
23
|
-
CAN_REDO_COMMAND,
|
|
24
|
-
CAN_UNDO_COMMAND,
|
|
25
|
-
CLEAR_EDITOR_COMMAND,
|
|
26
|
-
CLEAR_HISTORY_COMMAND,
|
|
27
|
-
COMMAND_PRIORITY_EDITOR,
|
|
28
|
-
configExtension,
|
|
29
|
-
defineExtension,
|
|
30
|
-
HISTORIC_TAG,
|
|
31
|
-
HISTORY_MERGE_TAG,
|
|
32
|
-
HISTORY_PUSH_TAG,
|
|
33
|
-
REDO_COMMAND,
|
|
34
|
-
safeCast,
|
|
35
|
-
UNDO_COMMAND,
|
|
36
|
-
} from 'lexical';
|
|
13
|
+
/**
|
|
14
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
15
|
+
*
|
|
16
|
+
* This source code is licensed under the MIT license found in the
|
|
17
|
+
* LICENSE file in the root directory of this source tree.
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
37
20
|
|
|
38
|
-
type MergeAction = 0 | 1 | 2;
|
|
39
21
|
const HISTORY_MERGE = 0;
|
|
40
22
|
const HISTORY_PUSH = 1;
|
|
41
23
|
const DISCARD_HISTORY_CANDIDATE = 2;
|
|
42
|
-
|
|
43
|
-
type ChangeType = 0 | 1 | 2 | 3 | 4;
|
|
44
24
|
const OTHER = 0;
|
|
45
25
|
const COMPOSING_CHARACTER = 1;
|
|
46
26
|
const INSERT_CHARACTER_AFTER_SELECTION = 2;
|
|
47
27
|
const DELETE_CHARACTER_BEFORE_SELECTION = 3;
|
|
48
28
|
const DELETE_CHARACTER_AFTER_SELECTION = 4;
|
|
49
|
-
|
|
50
|
-
export type HistoryStateEntry = {
|
|
51
|
-
editor: LexicalEditor;
|
|
52
|
-
editorState: EditorState;
|
|
53
|
-
};
|
|
54
|
-
export type HistoryState = {
|
|
55
|
-
current: null | HistoryStateEntry;
|
|
56
|
-
redoStack: Array<HistoryStateEntry>;
|
|
57
|
-
undoStack: Array<HistoryStateEntry>;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type IntentionallyMarkedAsDirtyElement = boolean;
|
|
61
|
-
|
|
62
|
-
function getDirtyNodes(
|
|
63
|
-
editorState: EditorState,
|
|
64
|
-
dirtyLeaves: Set<NodeKey>,
|
|
65
|
-
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
|
66
|
-
): Array<LexicalNode> {
|
|
29
|
+
function getDirtyNodes(editorState, dirtyLeaves, dirtyElements) {
|
|
67
30
|
const nodeMap = editorState._nodeMap;
|
|
68
31
|
const nodes = [];
|
|
69
|
-
|
|
70
32
|
for (const dirtyLeafKey of dirtyLeaves) {
|
|
71
33
|
const dirtyLeaf = nodeMap.get(dirtyLeafKey);
|
|
72
|
-
|
|
73
34
|
if (dirtyLeaf !== undefined) {
|
|
74
35
|
nodes.push(dirtyLeaf);
|
|
75
36
|
}
|
|
76
37
|
}
|
|
77
|
-
|
|
78
38
|
for (const [dirtyElementKey, intentionallyMarkedAsDirty] of dirtyElements) {
|
|
79
39
|
if (!intentionallyMarkedAsDirty) {
|
|
80
40
|
continue;
|
|
81
41
|
}
|
|
82
|
-
|
|
83
42
|
const dirtyElement = nodeMap.get(dirtyElementKey);
|
|
84
|
-
|
|
85
43
|
if (dirtyElement !== undefined && !$isRootNode(dirtyElement)) {
|
|
86
44
|
nodes.push(dirtyElement);
|
|
87
45
|
}
|
|
88
46
|
}
|
|
89
|
-
|
|
90
47
|
return nodes;
|
|
91
48
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
prevEditorState: null | EditorState,
|
|
95
|
-
nextEditorState: EditorState,
|
|
96
|
-
dirtyLeavesSet: Set<NodeKey>,
|
|
97
|
-
dirtyElementsSet: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
|
98
|
-
isComposing: boolean,
|
|
99
|
-
): ChangeType {
|
|
100
|
-
if (
|
|
101
|
-
prevEditorState === null ||
|
|
102
|
-
(dirtyLeavesSet.size === 0 && dirtyElementsSet.size === 0 && !isComposing)
|
|
103
|
-
) {
|
|
49
|
+
function getChangeType(prevEditorState, nextEditorState, dirtyLeavesSet, dirtyElementsSet, isComposing) {
|
|
50
|
+
if (prevEditorState === null || dirtyLeavesSet.size === 0 && dirtyElementsSet.size === 0 && !isComposing) {
|
|
104
51
|
return OTHER;
|
|
105
52
|
}
|
|
106
|
-
|
|
107
53
|
const nextSelection = nextEditorState._selection;
|
|
108
54
|
const prevSelection = prevEditorState._selection;
|
|
109
|
-
|
|
110
55
|
if (isComposing) {
|
|
111
56
|
return COMPOSING_CHARACTER;
|
|
112
57
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
!$isRangeSelection(nextSelection) ||
|
|
116
|
-
!$isRangeSelection(prevSelection) ||
|
|
117
|
-
!prevSelection.isCollapsed() ||
|
|
118
|
-
!nextSelection.isCollapsed()
|
|
119
|
-
) {
|
|
58
|
+
if (!$isRangeSelection(nextSelection) || !$isRangeSelection(prevSelection) || !prevSelection.isCollapsed() || !nextSelection.isCollapsed()) {
|
|
120
59
|
return OTHER;
|
|
121
60
|
}
|
|
122
|
-
|
|
123
|
-
const dirtyNodes = getDirtyNodes(
|
|
124
|
-
nextEditorState,
|
|
125
|
-
dirtyLeavesSet,
|
|
126
|
-
dirtyElementsSet,
|
|
127
|
-
);
|
|
128
|
-
|
|
61
|
+
const dirtyNodes = getDirtyNodes(nextEditorState, dirtyLeavesSet, dirtyElementsSet);
|
|
129
62
|
if (dirtyNodes.length === 0) {
|
|
130
63
|
return OTHER;
|
|
131
64
|
}
|
|
@@ -136,123 +69,58 @@ function getChangeType(
|
|
|
136
69
|
const nextNodeMap = nextEditorState._nodeMap;
|
|
137
70
|
const nextAnchorNode = nextNodeMap.get(nextSelection.anchor.key);
|
|
138
71
|
const prevAnchorNode = nextNodeMap.get(prevSelection.anchor.key);
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
nextAnchorNode &&
|
|
142
|
-
prevAnchorNode &&
|
|
143
|
-
!prevEditorState._nodeMap.has(nextAnchorNode.__key) &&
|
|
144
|
-
$isTextNode(nextAnchorNode) &&
|
|
145
|
-
nextAnchorNode.__text.length === 1 &&
|
|
146
|
-
nextSelection.anchor.offset === 1
|
|
147
|
-
) {
|
|
72
|
+
if (nextAnchorNode && prevAnchorNode && !prevEditorState._nodeMap.has(nextAnchorNode.__key) && $isTextNode(nextAnchorNode) && nextAnchorNode.__text.length === 1 && nextSelection.anchor.offset === 1) {
|
|
148
73
|
return INSERT_CHARACTER_AFTER_SELECTION;
|
|
149
74
|
}
|
|
150
|
-
|
|
151
75
|
return OTHER;
|
|
152
76
|
}
|
|
153
|
-
|
|
154
77
|
const nextDirtyNode = dirtyNodes[0];
|
|
155
|
-
|
|
156
78
|
const prevDirtyNode = prevEditorState._nodeMap.get(nextDirtyNode.__key);
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
!$isTextNode(prevDirtyNode) ||
|
|
160
|
-
!$isTextNode(nextDirtyNode) ||
|
|
161
|
-
prevDirtyNode.__mode !== nextDirtyNode.__mode
|
|
162
|
-
) {
|
|
79
|
+
if (!$isTextNode(prevDirtyNode) || !$isTextNode(nextDirtyNode) || prevDirtyNode.__mode !== nextDirtyNode.__mode) {
|
|
163
80
|
return OTHER;
|
|
164
81
|
}
|
|
165
|
-
|
|
166
82
|
const prevText = prevDirtyNode.__text;
|
|
167
83
|
const nextText = nextDirtyNode.__text;
|
|
168
|
-
|
|
169
84
|
if (prevText === nextText) {
|
|
170
85
|
return OTHER;
|
|
171
86
|
}
|
|
172
|
-
|
|
173
87
|
const nextAnchor = nextSelection.anchor;
|
|
174
88
|
const prevAnchor = prevSelection.anchor;
|
|
175
|
-
|
|
176
89
|
if (nextAnchor.key !== prevAnchor.key || nextAnchor.type !== 'text') {
|
|
177
90
|
return OTHER;
|
|
178
91
|
}
|
|
179
|
-
|
|
180
92
|
const nextAnchorOffset = nextAnchor.offset;
|
|
181
93
|
const prevAnchorOffset = prevAnchor.offset;
|
|
182
94
|
const textDiff = nextText.length - prevText.length;
|
|
183
|
-
|
|
184
95
|
if (textDiff === 1 && prevAnchorOffset === nextAnchorOffset - 1) {
|
|
185
96
|
return INSERT_CHARACTER_AFTER_SELECTION;
|
|
186
97
|
}
|
|
187
|
-
|
|
188
98
|
if (textDiff === -1 && prevAnchorOffset === nextAnchorOffset + 1) {
|
|
189
99
|
return DELETE_CHARACTER_BEFORE_SELECTION;
|
|
190
100
|
}
|
|
191
|
-
|
|
192
101
|
if (textDiff === -1 && prevAnchorOffset === nextAnchorOffset) {
|
|
193
102
|
return DELETE_CHARACTER_AFTER_SELECTION;
|
|
194
103
|
}
|
|
195
|
-
|
|
196
104
|
return OTHER;
|
|
197
105
|
}
|
|
198
|
-
|
|
199
|
-
function isTextNodeUnchanged(
|
|
200
|
-
key: NodeKey,
|
|
201
|
-
prevEditorState: EditorState,
|
|
202
|
-
nextEditorState: EditorState,
|
|
203
|
-
): boolean {
|
|
106
|
+
function isTextNodeUnchanged(key, prevEditorState, nextEditorState) {
|
|
204
107
|
const prevNode = prevEditorState._nodeMap.get(key);
|
|
205
108
|
const nextNode = nextEditorState._nodeMap.get(key);
|
|
206
|
-
|
|
207
109
|
const prevSelection = prevEditorState._selection;
|
|
208
110
|
const nextSelection = nextEditorState._selection;
|
|
209
|
-
const isDeletingLine =
|
|
210
|
-
|
|
211
|
-
$isRangeSelection(nextSelection) &&
|
|
212
|
-
prevSelection.anchor.type === 'element' &&
|
|
213
|
-
prevSelection.focus.type === 'element' &&
|
|
214
|
-
nextSelection.anchor.type === 'text' &&
|
|
215
|
-
nextSelection.focus.type === 'text';
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
!isDeletingLine &&
|
|
219
|
-
$isTextNode(prevNode) &&
|
|
220
|
-
$isTextNode(nextNode) &&
|
|
221
|
-
prevNode.__parent === nextNode.__parent
|
|
222
|
-
) {
|
|
111
|
+
const isDeletingLine = $isRangeSelection(prevSelection) && $isRangeSelection(nextSelection) && prevSelection.anchor.type === 'element' && prevSelection.focus.type === 'element' && nextSelection.anchor.type === 'text' && nextSelection.focus.type === 'text';
|
|
112
|
+
if (!isDeletingLine && $isTextNode(prevNode) && $isTextNode(nextNode) && prevNode.__parent === nextNode.__parent) {
|
|
223
113
|
// This has the assumption that object key order won't change if the
|
|
224
114
|
// content did not change, which should normally be safe given
|
|
225
115
|
// the manner in which nodes and exportJSON are typically implemented.
|
|
226
|
-
return (
|
|
227
|
-
JSON.stringify(prevEditorState.read(() => prevNode.exportJSON())) ===
|
|
228
|
-
JSON.stringify(nextEditorState.read(() => nextNode.exportJSON()))
|
|
229
|
-
);
|
|
116
|
+
return JSON.stringify(prevEditorState.read(() => prevNode.exportJSON())) === JSON.stringify(nextEditorState.read(() => nextNode.exportJSON()));
|
|
230
117
|
}
|
|
231
118
|
return false;
|
|
232
119
|
}
|
|
233
|
-
|
|
234
|
-
function createMergeActionGetter(
|
|
235
|
-
editor: LexicalEditor,
|
|
236
|
-
delayOrStore: number | ReadonlySignal<number>,
|
|
237
|
-
): (
|
|
238
|
-
prevEditorState: null | EditorState,
|
|
239
|
-
nextEditorState: EditorState,
|
|
240
|
-
currentHistoryEntry: null | HistoryStateEntry,
|
|
241
|
-
dirtyLeaves: Set<NodeKey>,
|
|
242
|
-
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
|
243
|
-
tags: Set<string>,
|
|
244
|
-
) => MergeAction {
|
|
120
|
+
function createMergeActionGetter(editor, delayOrStore) {
|
|
245
121
|
let prevChangeTime = Date.now();
|
|
246
122
|
let prevChangeType = OTHER;
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
prevEditorState,
|
|
250
|
-
nextEditorState,
|
|
251
|
-
currentHistoryEntry,
|
|
252
|
-
dirtyLeaves,
|
|
253
|
-
dirtyElements,
|
|
254
|
-
tags,
|
|
255
|
-
) => {
|
|
123
|
+
return (prevEditorState, nextEditorState, currentHistoryEntry, dirtyLeaves, dirtyElements, tags) => {
|
|
256
124
|
const changeTime = Date.now();
|
|
257
125
|
|
|
258
126
|
// If applying changes from history stack there's no need
|
|
@@ -262,50 +130,27 @@ function createMergeActionGetter(
|
|
|
262
130
|
prevChangeTime = changeTime;
|
|
263
131
|
return DISCARD_HISTORY_CANDIDATE;
|
|
264
132
|
}
|
|
265
|
-
|
|
266
|
-
const changeType = getChangeType(
|
|
267
|
-
prevEditorState,
|
|
268
|
-
nextEditorState,
|
|
269
|
-
dirtyLeaves,
|
|
270
|
-
dirtyElements,
|
|
271
|
-
editor.isComposing(),
|
|
272
|
-
);
|
|
273
|
-
|
|
133
|
+
const changeType = getChangeType(prevEditorState, nextEditorState, dirtyLeaves, dirtyElements, editor.isComposing());
|
|
274
134
|
const mergeAction = (() => {
|
|
275
|
-
const isSameEditor =
|
|
276
|
-
currentHistoryEntry === null || currentHistoryEntry.editor === editor;
|
|
135
|
+
const isSameEditor = currentHistoryEntry === null || currentHistoryEntry.editor === editor;
|
|
277
136
|
const shouldPushHistory = tags.has(HISTORY_PUSH_TAG);
|
|
278
|
-
const shouldMergeHistory =
|
|
279
|
-
!shouldPushHistory && isSameEditor && tags.has(HISTORY_MERGE_TAG);
|
|
280
|
-
|
|
137
|
+
const shouldMergeHistory = !shouldPushHistory && isSameEditor && tags.has(HISTORY_MERGE_TAG);
|
|
281
138
|
if (shouldMergeHistory) {
|
|
282
139
|
return HISTORY_MERGE;
|
|
283
140
|
}
|
|
284
|
-
|
|
285
141
|
if (prevEditorState === null) {
|
|
286
142
|
return HISTORY_PUSH;
|
|
287
143
|
}
|
|
288
|
-
|
|
289
144
|
const selection = nextEditorState._selection;
|
|
290
145
|
const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;
|
|
291
|
-
|
|
292
146
|
if (!hasDirtyNodes) {
|
|
293
147
|
if (selection !== null) {
|
|
294
148
|
return HISTORY_MERGE;
|
|
295
149
|
}
|
|
296
|
-
|
|
297
150
|
return DISCARD_HISTORY_CANDIDATE;
|
|
298
151
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
typeof delayOrStore === 'number' ? delayOrStore : delayOrStore.peek();
|
|
302
|
-
if (
|
|
303
|
-
shouldPushHistory === false &&
|
|
304
|
-
changeType !== OTHER &&
|
|
305
|
-
changeType === prevChangeType &&
|
|
306
|
-
changeTime < prevChangeTime + delay &&
|
|
307
|
-
isSameEditor
|
|
308
|
-
) {
|
|
152
|
+
const delay = typeof delayOrStore === 'number' ? delayOrStore : delayOrStore.peek();
|
|
153
|
+
if (shouldPushHistory === false && changeType !== OTHER && changeType === prevChangeType && changeTime < prevChangeTime + delay && isSameEditor) {
|
|
309
154
|
return HISTORY_MERGE;
|
|
310
155
|
}
|
|
311
156
|
|
|
@@ -313,80 +158,61 @@ function createMergeActionGetter(
|
|
|
313
158
|
// due to some node transform reverting the change.
|
|
314
159
|
if (dirtyLeaves.size === 1) {
|
|
315
160
|
const dirtyLeafKey = Array.from(dirtyLeaves)[0];
|
|
316
|
-
if (
|
|
317
|
-
isTextNodeUnchanged(dirtyLeafKey, prevEditorState, nextEditorState)
|
|
318
|
-
) {
|
|
161
|
+
if (isTextNodeUnchanged(dirtyLeafKey, prevEditorState, nextEditorState)) {
|
|
319
162
|
return HISTORY_MERGE;
|
|
320
163
|
}
|
|
321
164
|
}
|
|
322
|
-
|
|
323
165
|
return HISTORY_PUSH;
|
|
324
166
|
})();
|
|
325
|
-
|
|
326
167
|
prevChangeTime = changeTime;
|
|
327
168
|
prevChangeType = changeType;
|
|
328
|
-
|
|
329
169
|
return mergeAction;
|
|
330
170
|
};
|
|
331
171
|
}
|
|
332
|
-
|
|
333
|
-
function redo(editor: LexicalEditor, historyState: HistoryState): void {
|
|
172
|
+
function redo(editor, historyState) {
|
|
334
173
|
const redoStack = historyState.redoStack;
|
|
335
174
|
const undoStack = historyState.undoStack;
|
|
336
|
-
|
|
337
175
|
if (redoStack.length !== 0) {
|
|
338
176
|
const current = historyState.current;
|
|
339
|
-
|
|
340
177
|
if (current !== null) {
|
|
341
178
|
undoStack.push(current);
|
|
342
179
|
editor.dispatchCommand(CAN_UNDO_COMMAND, true);
|
|
343
180
|
}
|
|
344
|
-
|
|
345
181
|
const historyStateEntry = redoStack.pop();
|
|
346
|
-
|
|
347
182
|
if (redoStack.length === 0) {
|
|
348
183
|
editor.dispatchCommand(CAN_REDO_COMMAND, false);
|
|
349
184
|
}
|
|
350
|
-
|
|
351
185
|
historyState.current = historyStateEntry || null;
|
|
352
|
-
|
|
353
186
|
if (historyStateEntry) {
|
|
354
187
|
historyStateEntry.editor.setEditorState(historyStateEntry.editorState, {
|
|
355
|
-
tag: HISTORIC_TAG
|
|
188
|
+
tag: HISTORIC_TAG
|
|
356
189
|
});
|
|
357
190
|
}
|
|
358
191
|
}
|
|
359
192
|
}
|
|
360
|
-
|
|
361
|
-
function undo(editor: LexicalEditor, historyState: HistoryState): void {
|
|
193
|
+
function undo(editor, historyState) {
|
|
362
194
|
const redoStack = historyState.redoStack;
|
|
363
195
|
const undoStack = historyState.undoStack;
|
|
364
196
|
const undoStackLength = undoStack.length;
|
|
365
|
-
|
|
366
197
|
if (undoStackLength !== 0) {
|
|
367
198
|
const current = historyState.current;
|
|
368
199
|
const historyStateEntry = undoStack.pop();
|
|
369
|
-
|
|
370
200
|
if (current !== null) {
|
|
371
201
|
redoStack.push(current);
|
|
372
202
|
editor.dispatchCommand(CAN_REDO_COMMAND, true);
|
|
373
203
|
}
|
|
374
|
-
|
|
375
204
|
if (undoStack.length === 0) {
|
|
376
205
|
editor.dispatchCommand(CAN_UNDO_COMMAND, false);
|
|
377
206
|
}
|
|
378
|
-
|
|
379
207
|
historyState.current = historyStateEntry || null;
|
|
380
|
-
|
|
381
208
|
if (historyStateEntry) {
|
|
382
209
|
historyStateEntry.editor.setEditorState(historyStateEntry.editorState, {
|
|
383
|
-
tag: HISTORIC_TAG
|
|
210
|
+
tag: HISTORIC_TAG
|
|
384
211
|
});
|
|
385
212
|
}
|
|
386
213
|
}
|
|
387
214
|
}
|
|
388
|
-
|
|
389
|
-
function clearHistory(historyState: HistoryState) {
|
|
215
|
+
function clearHistory(historyState) {
|
|
390
216
|
historyState.undoStack = [];
|
|
391
217
|
historyState.redoStack = [];
|
|
392
218
|
historyState.current = null;
|
|
@@ -401,53 +227,31 @@ function clearHistory(historyState: HistoryState) {
|
|
|
401
227
|
* instead of merging the current changes with the current stack.
|
|
402
228
|
* @returns The listeners cleanup callback function.
|
|
403
229
|
*/
|
|
404
|
-
|
|
405
|
-
editor: LexicalEditor,
|
|
406
|
-
historyState: HistoryState,
|
|
407
|
-
delay: number | ReadonlySignal<number>,
|
|
408
|
-
): () => void {
|
|
230
|
+
function registerHistory(editor, historyState, delay) {
|
|
409
231
|
const getMergeAction = createMergeActionGetter(editor, delay);
|
|
410
|
-
|
|
411
232
|
const applyChange = ({
|
|
412
233
|
editorState,
|
|
413
234
|
prevEditorState,
|
|
414
235
|
dirtyLeaves,
|
|
415
236
|
dirtyElements,
|
|
416
|
-
tags
|
|
417
|
-
}
|
|
418
|
-
editorState: EditorState;
|
|
419
|
-
prevEditorState: EditorState;
|
|
420
|
-
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
|
|
421
|
-
dirtyLeaves: Set<NodeKey>;
|
|
422
|
-
tags: Set<string>;
|
|
423
|
-
}): void => {
|
|
237
|
+
tags
|
|
238
|
+
}) => {
|
|
424
239
|
const current = historyState.current;
|
|
425
240
|
const redoStack = historyState.redoStack;
|
|
426
241
|
const undoStack = historyState.undoStack;
|
|
427
242
|
const currentEditorState = current === null ? null : current.editorState;
|
|
428
|
-
|
|
429
243
|
if (current !== null && editorState === currentEditorState) {
|
|
430
244
|
return;
|
|
431
245
|
}
|
|
432
|
-
|
|
433
|
-
const mergeAction = getMergeAction(
|
|
434
|
-
prevEditorState,
|
|
435
|
-
editorState,
|
|
436
|
-
current,
|
|
437
|
-
dirtyLeaves,
|
|
438
|
-
dirtyElements,
|
|
439
|
-
tags,
|
|
440
|
-
);
|
|
441
|
-
|
|
246
|
+
const mergeAction = getMergeAction(prevEditorState, editorState, current, dirtyLeaves, dirtyElements, tags);
|
|
442
247
|
if (mergeAction === HISTORY_PUSH) {
|
|
443
248
|
if (redoStack.length !== 0) {
|
|
444
249
|
historyState.redoStack = [];
|
|
445
250
|
editor.dispatchCommand(CAN_REDO_COMMAND, false);
|
|
446
251
|
}
|
|
447
|
-
|
|
448
252
|
if (current !== null) {
|
|
449
253
|
undoStack.push({
|
|
450
|
-
...current
|
|
254
|
+
...current
|
|
451
255
|
});
|
|
452
256
|
editor.dispatchCommand(CAN_UNDO_COMMAND, true);
|
|
453
257
|
}
|
|
@@ -458,48 +262,24 @@ export function registerHistory(
|
|
|
458
262
|
// Else we merge
|
|
459
263
|
historyState.current = {
|
|
460
264
|
editor,
|
|
461
|
-
editorState
|
|
265
|
+
editorState
|
|
462
266
|
};
|
|
463
267
|
};
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
},
|
|
480
|
-
COMMAND_PRIORITY_EDITOR,
|
|
481
|
-
),
|
|
482
|
-
editor.registerCommand(
|
|
483
|
-
CLEAR_EDITOR_COMMAND,
|
|
484
|
-
() => {
|
|
485
|
-
clearHistory(historyState);
|
|
486
|
-
return false;
|
|
487
|
-
},
|
|
488
|
-
COMMAND_PRIORITY_EDITOR,
|
|
489
|
-
),
|
|
490
|
-
editor.registerCommand(
|
|
491
|
-
CLEAR_HISTORY_COMMAND,
|
|
492
|
-
() => {
|
|
493
|
-
clearHistory(historyState);
|
|
494
|
-
editor.dispatchCommand(CAN_REDO_COMMAND, false);
|
|
495
|
-
editor.dispatchCommand(CAN_UNDO_COMMAND, false);
|
|
496
|
-
return true;
|
|
497
|
-
},
|
|
498
|
-
COMMAND_PRIORITY_EDITOR,
|
|
499
|
-
),
|
|
500
|
-
editor.registerUpdateListener(applyChange),
|
|
501
|
-
);
|
|
502
|
-
|
|
268
|
+
const unregister = mergeRegister(editor.registerCommand(UNDO_COMMAND, () => {
|
|
269
|
+
undo(editor, historyState);
|
|
270
|
+
return true;
|
|
271
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(REDO_COMMAND, () => {
|
|
272
|
+
redo(editor, historyState);
|
|
273
|
+
return true;
|
|
274
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(CLEAR_EDITOR_COMMAND, () => {
|
|
275
|
+
clearHistory(historyState);
|
|
276
|
+
return false;
|
|
277
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(CLEAR_HISTORY_COMMAND, () => {
|
|
278
|
+
clearHistory(historyState);
|
|
279
|
+
editor.dispatchCommand(CAN_REDO_COMMAND, false);
|
|
280
|
+
editor.dispatchCommand(CAN_UNDO_COMMAND, false);
|
|
281
|
+
return true;
|
|
282
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerUpdateListener(applyChange));
|
|
503
283
|
return unregister;
|
|
504
284
|
}
|
|
505
285
|
|
|
@@ -507,65 +287,41 @@ export function registerHistory(
|
|
|
507
287
|
* Creates an empty history state.
|
|
508
288
|
* @returns - The empty history state, as an object.
|
|
509
289
|
*/
|
|
510
|
-
|
|
290
|
+
function createEmptyHistoryState() {
|
|
511
291
|
return {
|
|
512
292
|
current: null,
|
|
513
293
|
redoStack: [],
|
|
514
|
-
undoStack: []
|
|
294
|
+
undoStack: []
|
|
515
295
|
};
|
|
516
296
|
}
|
|
517
|
-
|
|
518
|
-
export interface HistoryConfig {
|
|
519
|
-
/**
|
|
520
|
-
* The time (in milliseconds) the editor should delay generating a new history stack,
|
|
521
|
-
* instead of merging the current changes with the current stack. The default is 300ms.
|
|
522
|
-
*/
|
|
523
|
-
delay: number;
|
|
524
|
-
/**
|
|
525
|
-
* The initial history state, the default is {@link createEmptyHistoryState}.
|
|
526
|
-
*/
|
|
527
|
-
createInitialHistoryState: (editor: LexicalEditor) => HistoryState;
|
|
528
|
-
/**
|
|
529
|
-
* Whether history is disabled or not
|
|
530
|
-
*/
|
|
531
|
-
disabled: boolean;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
297
|
/**
|
|
535
298
|
* Registers necessary listeners to manage undo/redo history stack and related
|
|
536
299
|
* editor commands, via the \@lexical/history module.
|
|
537
300
|
*/
|
|
538
301
|
|
|
539
|
-
|
|
540
|
-
build: (editor, {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
302
|
+
const HistoryExtension = defineExtension({
|
|
303
|
+
build: (editor, {
|
|
304
|
+
delay,
|
|
305
|
+
createInitialHistoryState,
|
|
306
|
+
disabled
|
|
307
|
+
}) => namedSignals({
|
|
308
|
+
delay,
|
|
309
|
+
disabled,
|
|
310
|
+
historyState: createInitialHistoryState(editor)
|
|
311
|
+
}),
|
|
312
|
+
config: safeCast({
|
|
547
313
|
createInitialHistoryState: createEmptyHistoryState,
|
|
548
314
|
delay: 300,
|
|
549
|
-
disabled: typeof window === 'undefined'
|
|
315
|
+
disabled: typeof window === 'undefined'
|
|
550
316
|
}),
|
|
551
317
|
name: '@lexical/history/History',
|
|
552
318
|
register: (editor, config, state) => {
|
|
553
319
|
const stores = state.getOutput();
|
|
554
|
-
return effect(() =>
|
|
555
|
-
|
|
556
|
-
? undefined
|
|
557
|
-
: registerHistory(editor, stores.historyState.value, stores.delay),
|
|
558
|
-
);
|
|
559
|
-
},
|
|
320
|
+
return effect(() => stores.disabled.value ? undefined : registerHistory(editor, stores.historyState.value, stores.delay));
|
|
321
|
+
}
|
|
560
322
|
});
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return editor
|
|
564
|
-
? getPeerDependencyFromEditor<typeof HistoryExtension>(
|
|
565
|
-
editor,
|
|
566
|
-
HistoryExtension.name,
|
|
567
|
-
)
|
|
568
|
-
: null;
|
|
323
|
+
function getHistoryPeer(editor) {
|
|
324
|
+
return editor ? getPeerDependencyFromEditor(editor, HistoryExtension.name) : null;
|
|
569
325
|
}
|
|
570
326
|
|
|
571
327
|
/**
|
|
@@ -573,30 +329,30 @@ function getHistoryPeer(editor: LexicalEditor | null | undefined) {
|
|
|
573
329
|
* editor commands, via the \@lexical/history module, only if the parent editor
|
|
574
330
|
* has a history plugin implementation.
|
|
575
331
|
*/
|
|
576
|
-
|
|
577
|
-
dependencies: [
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}),
|
|
584
|
-
],
|
|
332
|
+
const SharedHistoryExtension = defineExtension({
|
|
333
|
+
dependencies: [configExtension(HistoryExtension, {
|
|
334
|
+
createInitialHistoryState: () => {
|
|
335
|
+
throw new Error('SharedHistory did not inherit parent history');
|
|
336
|
+
},
|
|
337
|
+
disabled: true
|
|
338
|
+
})],
|
|
585
339
|
name: '@lexical/history/SharedHistory',
|
|
586
340
|
register(editor, _config, state) {
|
|
587
|
-
const {
|
|
341
|
+
const {
|
|
342
|
+
output
|
|
343
|
+
} = state.getDependency(HistoryExtension);
|
|
588
344
|
const parentPeer = getHistoryPeer(editor._parentEditor);
|
|
589
345
|
if (!parentPeer) {
|
|
590
346
|
return () => {};
|
|
591
347
|
}
|
|
592
348
|
const parentOutput = parentPeer.output;
|
|
593
|
-
return effect(() =>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
);
|
|
601
|
-
},
|
|
349
|
+
return effect(() => batch(() => {
|
|
350
|
+
output.delay.value = parentOutput.delay.value;
|
|
351
|
+
output.historyState.value = parentOutput.historyState.value;
|
|
352
|
+
// Note that toggling the parent history will force this to be changed
|
|
353
|
+
output.disabled.value = parentOutput.disabled.value;
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
602
356
|
});
|
|
357
|
+
|
|
358
|
+
export { HistoryExtension, SharedHistoryExtension, createEmptyHistoryState, registerHistory };
|
package/LexicalHistory.js
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
'use strict'
|
|
10
|
-
|
|
11
|
-
module.exports =
|
|
9
|
+
'use strict'
|
|
10
|
+
const LexicalHistory = process.env.NODE_ENV !== 'production' ? require('./LexicalHistory.dev.js') : require('./LexicalHistory.prod.js');
|
|
11
|
+
module.exports = LexicalHistory;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as modDev from './LexicalHistory.dev.mjs';
|
|
10
|
+
import * as modProd from './LexicalHistory.prod.mjs';
|
|
11
|
+
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
|
|
12
|
+
export const HistoryExtension = mod.HistoryExtension;
|
|
13
|
+
export const SharedHistoryExtension = mod.SharedHistoryExtension;
|
|
14
|
+
export const createEmptyHistoryState = mod.createEmptyHistoryState;
|
|
15
|
+
export const registerHistory = mod.registerHistory;
|