@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.
@@ -0,0 +1,13 @@
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
+ const mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalHistory.dev.mjs') : import('./LexicalHistory.prod.mjs'));
10
+ export const HistoryExtension = mod.HistoryExtension;
11
+ export const SharedHistoryExtension = mod.SharedHistoryExtension;
12
+ export const createEmptyHistoryState = mod.createEmptyHistoryState;
13
+ export const registerHistory = mod.registerHistory;
@@ -0,0 +1,9 @@
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
+ "use strict";var e=require("@lexical/extension"),t=require("@lexical/utils"),n=require("lexical");function r(e,t,r,o,i){if(null===e||0===r.size&&0===o.size&&!i)return 0;const s=t._selection,a=e._selection;if(i)return 1;if(!(n.$isRangeSelection(s)&&n.$isRangeSelection(a)&&a.isCollapsed()&&s.isCollapsed()))return 0;const c=function(e,t,r){const o=e._nodeMap,i=[];for(const e of t){const t=o.get(e);void 0!==t&&i.push(t)}for(const[e,t]of r){if(!t)continue;const r=o.get(e);void 0===r||n.$isRootNode(r)||i.push(r)}return i}(t,r,o);if(0===c.length)return 0;if(c.length>1){const r=t._nodeMap,o=r.get(s.anchor.key),i=r.get(a.anchor.key);return o&&i&&!e._nodeMap.has(o.__key)&&n.$isTextNode(o)&&1===o.__text.length&&1===s.anchor.offset?2:0}const d=c[0],u=e._nodeMap.get(d.__key);if(!n.$isTextNode(u)||!n.$isTextNode(d)||u.__mode!==d.__mode)return 0;const l=u.__text,_=d.__text;if(l===_)return 0;const f=s.anchor,p=a.anchor;if(f.key!==p.key||"text"!==f.type)return 0;const h=f.offset,O=p.offset,S=_.length-l.length;return 1===S&&O===h-1?2:-1===S&&O===h+1?3:-1===S&&O===h?4:0}function o(e,t){let o=Date.now(),i=0;return(s,a,c,d,u,l)=>{const _=Date.now();if(l.has(n.HISTORIC_TAG))return i=0,o=_,2;const f=r(s,a,d,u,e.isComposing()),p=(()=>{const r=null===c||c.editor===e,p=l.has(n.HISTORY_PUSH_TAG);if(!p&&r&&l.has(n.HISTORY_MERGE_TAG))return 0;if(null===s)return 1;const h=a._selection;if(!(d.size>0||u.size>0))return null!==h?0:2;const O="number"==typeof t?t:t.peek();if(!1===p&&0!==f&&f===i&&_<o+O&&r)return 0;if(1===d.size){if(function(e,t,r){const o=t._nodeMap.get(e),i=r._nodeMap.get(e),s=t._selection,a=r._selection;return!(n.$isRangeSelection(s)&&n.$isRangeSelection(a)&&"element"===s.anchor.type&&"element"===s.focus.type&&"text"===a.anchor.type&&"text"===a.focus.type||!n.$isTextNode(o)||!n.$isTextNode(i)||o.__parent!==i.__parent)&&JSON.stringify(t.read(()=>o.exportJSON()))===JSON.stringify(r.read(()=>i.exportJSON()))}(Array.from(d)[0],s,a))return 0}return 1})();return o=_,i=f,p}}function i(e){e.undoStack=[],e.redoStack=[],e.current=null}function s(e,r,s){const a=o(e,s),c=t.mergeRegister(e.registerCommand(n.UNDO_COMMAND,()=>(function(e,t){const r=t.redoStack,o=t.undoStack;if(0!==o.length){const i=t.current,s=o.pop();null!==i&&(r.push(i),e.dispatchCommand(n.CAN_REDO_COMMAND,!0)),0===o.length&&e.dispatchCommand(n.CAN_UNDO_COMMAND,!1),t.current=s||null,s&&s.editor.setEditorState(s.editorState,{tag:n.HISTORIC_TAG})}}(e,r),!0),n.COMMAND_PRIORITY_EDITOR),e.registerCommand(n.REDO_COMMAND,()=>(function(e,t){const r=t.redoStack,o=t.undoStack;if(0!==r.length){const i=t.current;null!==i&&(o.push(i),e.dispatchCommand(n.CAN_UNDO_COMMAND,!0));const s=r.pop();0===r.length&&e.dispatchCommand(n.CAN_REDO_COMMAND,!1),t.current=s||null,s&&s.editor.setEditorState(s.editorState,{tag:n.HISTORIC_TAG})}}(e,r),!0),n.COMMAND_PRIORITY_EDITOR),e.registerCommand(n.CLEAR_EDITOR_COMMAND,()=>(i(r),!1),n.COMMAND_PRIORITY_EDITOR),e.registerCommand(n.CLEAR_HISTORY_COMMAND,()=>(i(r),e.dispatchCommand(n.CAN_REDO_COMMAND,!1),e.dispatchCommand(n.CAN_UNDO_COMMAND,!1),!0),n.COMMAND_PRIORITY_EDITOR),e.registerUpdateListener(({editorState:t,prevEditorState:o,dirtyLeaves:i,dirtyElements:s,tags:c})=>{const d=r.current,u=r.redoStack,l=r.undoStack,_=null===d?null:d.editorState;if(null!==d&&t===_)return;const f=a(o,t,d,i,s,c);if(1===f)0!==u.length&&(r.redoStack=[],e.dispatchCommand(n.CAN_REDO_COMMAND,!1)),null!==d&&(l.push({...d}),e.dispatchCommand(n.CAN_UNDO_COMMAND,!0));else if(2===f)return;r.current={editor:e,editorState:t}}));return c}function a(){return{current:null,redoStack:[],undoStack:[]}}const c=n.defineExtension({build:(t,{delay:n,createInitialHistoryState:r,disabled:o})=>e.namedSignals({delay:n,disabled:o,historyState:r(t)}),config:n.safeCast({createInitialHistoryState:a,delay:300,disabled:"undefined"==typeof window}),name:"@lexical/history/History",register:(t,n,r)=>{const o=r.getOutput();return e.effect(()=>o.disabled.value?void 0:s(t,o.historyState.value,o.delay))}});const d=n.defineExtension({dependencies:[n.configExtension(c,{createInitialHistoryState:()=>{throw new Error("SharedHistory did not inherit parent history")},disabled:!0})],name:"@lexical/history/SharedHistory",register(t,n,r){const{output:o}=r.getDependency(c),i=function(t){return t?e.getPeerDependencyFromEditor(t,c.name):null}(t._parentEditor);if(!i)return()=>{};const s=i.output;return e.effect(()=>e.batch(()=>{o.delay.value=s.delay.value,o.historyState.value=s.historyState.value,o.disabled.value=s.disabled.value}))}});exports.HistoryExtension=c,exports.SharedHistoryExtension=d,exports.createEmptyHistoryState=a,exports.registerHistory=s;
@@ -0,0 +1,9 @@
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{namedSignals as t,effect as e,batch as n,getPeerDependencyFromEditor as r}from"@lexical/extension";import{mergeRegister as o}from"@lexical/utils";import{defineExtension as i,safeCast as a,configExtension as s,UNDO_COMMAND as c,COMMAND_PRIORITY_EDITOR as d,REDO_COMMAND as u,CLEAR_EDITOR_COMMAND as l,CLEAR_HISTORY_COMMAND as f,CAN_REDO_COMMAND as p,CAN_UNDO_COMMAND as h,HISTORIC_TAG as m,HISTORY_PUSH_TAG as y,HISTORY_MERGE_TAG as g,$isRangeSelection as S,$isTextNode as _,$isRootNode as k}from"lexical";function x(t,e,n,r,o){if(null===t||0===n.size&&0===r.size&&!o)return 0;const i=e._selection,a=t._selection;if(o)return 1;if(!(S(i)&&S(a)&&a.isCollapsed()&&i.isCollapsed()))return 0;const s=function(t,e,n){const r=t._nodeMap,o=[];for(const t of e){const e=r.get(t);void 0!==e&&o.push(e)}for(const[t,e]of n){if(!e)continue;const n=r.get(t);void 0===n||k(n)||o.push(n)}return o}(e,n,r);if(0===s.length)return 0;if(s.length>1){const n=e._nodeMap,r=n.get(i.anchor.key),o=n.get(a.anchor.key);return r&&o&&!t._nodeMap.has(r.__key)&&_(r)&&1===r.__text.length&&1===i.anchor.offset?2:0}const c=s[0],d=t._nodeMap.get(c.__key);if(!_(d)||!_(c)||d.__mode!==c.__mode)return 0;const u=d.__text,l=c.__text;if(u===l)return 0;const f=i.anchor,p=a.anchor;if(f.key!==p.key||"text"!==f.type)return 0;const h=f.offset,m=p.offset,y=l.length-u.length;return 1===y&&m===h-1?2:-1===y&&m===h+1?3:-1===y&&m===h?4:0}function C(t,e){let n=Date.now(),r=0;return(o,i,a,s,c,d)=>{const u=Date.now();if(d.has(m))return r=0,n=u,2;const l=x(o,i,s,c,t.isComposing()),f=(()=>{const f=null===a||a.editor===t,p=d.has(y);if(!p&&f&&d.has(g))return 0;if(null===o)return 1;const h=i._selection;if(!(s.size>0||c.size>0))return null!==h?0:2;const m="number"==typeof e?e:e.peek();if(!1===p&&0!==l&&l===r&&u<n+m&&f)return 0;if(1===s.size){if(function(t,e,n){const r=e._nodeMap.get(t),o=n._nodeMap.get(t),i=e._selection,a=n._selection;return!(S(i)&&S(a)&&"element"===i.anchor.type&&"element"===i.focus.type&&"text"===a.anchor.type&&"text"===a.focus.type||!_(r)||!_(o)||r.__parent!==o.__parent)&&JSON.stringify(e.read(()=>r.exportJSON()))===JSON.stringify(n.read(()=>o.exportJSON()))}(Array.from(s)[0],o,i))return 0}return 1})();return n=u,r=l,f}}function v(t){t.undoStack=[],t.redoStack=[],t.current=null}function b(t,e,n){const r=C(t,n),i=o(t.registerCommand(c,()=>(function(t,e){const n=e.redoStack,r=e.undoStack;if(0!==r.length){const o=e.current,i=r.pop();null!==o&&(n.push(o),t.dispatchCommand(p,!0)),0===r.length&&t.dispatchCommand(h,!1),e.current=i||null,i&&i.editor.setEditorState(i.editorState,{tag:m})}}(t,e),!0),d),t.registerCommand(u,()=>(function(t,e){const n=e.redoStack,r=e.undoStack;if(0!==n.length){const o=e.current;null!==o&&(r.push(o),t.dispatchCommand(h,!0));const i=n.pop();0===n.length&&t.dispatchCommand(p,!1),e.current=i||null,i&&i.editor.setEditorState(i.editorState,{tag:m})}}(t,e),!0),d),t.registerCommand(l,()=>(v(e),!1),d),t.registerCommand(f,()=>(v(e),t.dispatchCommand(p,!1),t.dispatchCommand(h,!1),!0),d),t.registerUpdateListener(({editorState:n,prevEditorState:o,dirtyLeaves:i,dirtyElements:a,tags:s})=>{const c=e.current,d=e.redoStack,u=e.undoStack,l=null===c?null:c.editorState;if(null!==c&&n===l)return;const f=r(o,n,c,i,a,s);if(1===f)0!==d.length&&(e.redoStack=[],t.dispatchCommand(p,!1)),null!==c&&(u.push({...c}),t.dispatchCommand(h,!0));else if(2===f)return;e.current={editor:t,editorState:n}}));return i}function w(){return{current:null,redoStack:[],undoStack:[]}}const E=i({build:(e,{delay:n,createInitialHistoryState:r,disabled:o})=>t({delay:n,disabled:o,historyState:r(e)}),config:a({createInitialHistoryState:w,delay:300,disabled:"undefined"==typeof window}),name:"@lexical/history/History",register:(t,n,r)=>{const o=r.getOutput();return e(()=>o.disabled.value?void 0:b(t,o.historyState.value,o.delay))}});const H=i({dependencies:[s(E,{createInitialHistoryState:()=>{throw new Error("SharedHistory did not inherit parent history")},disabled:!0})],name:"@lexical/history/SharedHistory",register(t,o,i){const{output:a}=i.getDependency(E),s=function(t){return t?r(t,E.name):null}(t._parentEditor);if(!s)return()=>{};const c=s.output;return e(()=>n(()=>{a.delay.value=c.delay.value,a.historyState.value=c.historyState.value,a.disabled.value=c.disabled.value}))}});export{E as HistoryExtension,H as SharedHistoryExtension,w as createEmptyHistoryState,b as registerHistory};
package/index.d.ts ADDED
@@ -0,0 +1,63 @@
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
+ import type { EditorState, LexicalEditor } from 'lexical';
9
+ import { ReadonlySignal } from '@lexical/extension';
10
+ export type HistoryStateEntry = {
11
+ editor: LexicalEditor;
12
+ editorState: EditorState;
13
+ };
14
+ export type HistoryState = {
15
+ current: null | HistoryStateEntry;
16
+ redoStack: Array<HistoryStateEntry>;
17
+ undoStack: Array<HistoryStateEntry>;
18
+ };
19
+ /**
20
+ * Registers necessary listeners to manage undo/redo history stack and related editor commands.
21
+ * It returns `unregister` callback that cleans up all listeners and should be called on editor unmount.
22
+ * @param editor - The lexical editor.
23
+ * @param historyState - The history state, containing the current state and the undo/redo stack.
24
+ * @param delay - The time (in milliseconds) the editor should delay generating a new history stack,
25
+ * instead of merging the current changes with the current stack.
26
+ * @returns The listeners cleanup callback function.
27
+ */
28
+ export declare function registerHistory(editor: LexicalEditor, historyState: HistoryState, delay: number | ReadonlySignal<number>): () => void;
29
+ /**
30
+ * Creates an empty history state.
31
+ * @returns - The empty history state, as an object.
32
+ */
33
+ export declare function createEmptyHistoryState(): HistoryState;
34
+ export interface HistoryConfig {
35
+ /**
36
+ * The time (in milliseconds) the editor should delay generating a new history stack,
37
+ * instead of merging the current changes with the current stack. The default is 300ms.
38
+ */
39
+ delay: number;
40
+ /**
41
+ * The initial history state, the default is {@link createEmptyHistoryState}.
42
+ */
43
+ createInitialHistoryState: (editor: LexicalEditor) => HistoryState;
44
+ /**
45
+ * Whether history is disabled or not
46
+ */
47
+ disabled: boolean;
48
+ }
49
+ /**
50
+ * Registers necessary listeners to manage undo/redo history stack and related
51
+ * editor commands, via the \@lexical/history module.
52
+ */
53
+ export declare const HistoryExtension: import("lexical").LexicalExtension<HistoryConfig, "@lexical/history/History", import("@lexical/extension").NamedSignalsOutput<{
54
+ delay: number;
55
+ disabled: boolean;
56
+ historyState: HistoryState;
57
+ }>, unknown>;
58
+ /**
59
+ * Registers necessary listeners to manage undo/redo history stack and related
60
+ * editor commands, via the \@lexical/history module, only if the parent editor
61
+ * has a history plugin implementation.
62
+ */
63
+ export declare const SharedHistoryExtension: import("lexical").LexicalExtension<import("lexical").ExtensionConfigBase, "@lexical/history/SharedHistory", unknown, unknown>;
package/package.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "history"
9
9
  ],
10
10
  "license": "MIT",
11
- "version": "0.38.0",
11
+ "version": "0.38.1",
12
12
  "main": "LexicalHistory.js",
13
13
  "types": "index.d.ts",
14
14
  "dependencies": {
15
- "@lexical/extension": "0.38.0",
16
- "@lexical/utils": "0.38.0",
17
- "lexical": "0.38.0"
15
+ "@lexical/extension": "0.38.1",
16
+ "@lexical/utils": "0.38.1",
17
+ "lexical": "0.38.1"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -1,452 +0,0 @@
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 type {JSX} from 'react';
10
-
11
- import {createEmptyHistoryState, registerHistory} from '@lexical/history';
12
- import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
13
- import {ContentEditable} from '@lexical/react/LexicalContentEditable';
14
- import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
15
- import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
16
- import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
17
- import {$createQuoteNode} from '@lexical/rich-text';
18
- import {$setBlocksType} from '@lexical/selection';
19
- import {$restoreEditorState} from '@lexical/utils';
20
- import {
21
- $applyNodeReplacement,
22
- $createNodeSelection,
23
- $createParagraphNode,
24
- $createRangeSelection,
25
- $createTextNode,
26
- $getRoot,
27
- $isNodeSelection,
28
- $setSelection,
29
- CAN_REDO_COMMAND,
30
- CAN_UNDO_COMMAND,
31
- CLEAR_HISTORY_COMMAND,
32
- COMMAND_PRIORITY_CRITICAL,
33
- HISTORY_MERGE_TAG,
34
- type KlassConstructor,
35
- LexicalEditor,
36
- LexicalNode,
37
- type NodeKey,
38
- REDO_COMMAND,
39
- SerializedElementNode,
40
- type SerializedTextNode,
41
- type Spread,
42
- TextNode,
43
- UNDO_COMMAND,
44
- } from 'lexical';
45
- import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils';
46
- import {createRoot, Root} from 'react-dom/client';
47
- import * as ReactTestUtils from 'shared/react-test-utils';
48
- import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
49
-
50
- type SerializedCustomTextNode = Spread<
51
- {type: string; classes: string[]},
52
- SerializedTextNode
53
- >;
54
-
55
- class CustomTextNode extends TextNode {
56
- /** @internal */
57
- declare ['constructor']: KlassConstructor<typeof CustomTextNode>;
58
-
59
- __classes: Set<string>;
60
- constructor(text: string, classes: Iterable<string>, key?: NodeKey) {
61
- super(text, key);
62
- this.__classes = new Set(classes);
63
- }
64
- static getType(): 'custom-text' {
65
- return 'custom-text';
66
- }
67
- static clone(node: CustomTextNode): CustomTextNode {
68
- return new CustomTextNode(node.__text, node.__classes, node.__key);
69
- }
70
- addClass(className: string): this {
71
- const self = this.getWritable();
72
- self.__classes.add(className);
73
- return self;
74
- }
75
- removeClass(className: string): this {
76
- const self = this.getWritable();
77
- self.__classes.delete(className);
78
- return self;
79
- }
80
- setClasses(classes: Iterable<string>): this {
81
- const self = this.getWritable();
82
- self.__classes = new Set(classes);
83
- return self;
84
- }
85
- getClasses(): ReadonlySet<string> {
86
- return this.getLatest().__classes;
87
- }
88
- static importJSON({text, classes}: SerializedCustomTextNode): CustomTextNode {
89
- return $createCustomTextNode(text, classes);
90
- }
91
- exportJSON(): SerializedCustomTextNode {
92
- return {
93
- ...super.exportJSON(),
94
- classes: Array.from(this.getClasses()),
95
- };
96
- }
97
- }
98
- function $createCustomTextNode(
99
- text: string,
100
- classes: string[] = [],
101
- ): CustomTextNode {
102
- return $applyNodeReplacement(new CustomTextNode(text, classes));
103
- }
104
- function $isCustomTextNode(
105
- node: LexicalNode | null | undefined,
106
- ): node is CustomTextNode {
107
- return node instanceof CustomTextNode;
108
- }
109
-
110
- describe('LexicalHistory tests', () => {
111
- let container: HTMLDivElement | null = null;
112
- let reactRoot: Root;
113
-
114
- beforeEach(() => {
115
- container = document.createElement('div');
116
- reactRoot = createRoot(container);
117
- document.body.appendChild(container);
118
- });
119
-
120
- afterEach(() => {
121
- if (container !== null) {
122
- document.body.removeChild(container);
123
- }
124
- container = null;
125
-
126
- vi.restoreAllMocks();
127
- });
128
-
129
- // Shared instance across tests
130
- let editor: LexicalEditor;
131
-
132
- function TestPlugin() {
133
- // Plugin used just to get our hands on the Editor object
134
- [editor] = useLexicalComposerContext();
135
- return null;
136
- }
137
- function Test(): JSX.Element {
138
- return (
139
- <TestComposer>
140
- <RichTextPlugin
141
- contentEditable={<ContentEditable />}
142
- placeholder={
143
- <div className="editor-placeholder">Enter some text...</div>
144
- }
145
- ErrorBoundary={LexicalErrorBoundary}
146
- />
147
- <TestPlugin />
148
- <HistoryPlugin />
149
- </TestComposer>
150
- );
151
- }
152
-
153
- test('LexicalHistory after clearing', async () => {
154
- let canRedo = true;
155
- let canUndo = true;
156
-
157
- ReactTestUtils.act(() => {
158
- reactRoot.render(<Test key="smth" />);
159
- });
160
-
161
- editor.registerCommand<boolean>(
162
- CAN_REDO_COMMAND,
163
- (payload) => {
164
- canRedo = payload;
165
- return false;
166
- },
167
- COMMAND_PRIORITY_CRITICAL,
168
- );
169
-
170
- editor.registerCommand<boolean>(
171
- CAN_UNDO_COMMAND,
172
- (payload) => {
173
- canUndo = payload;
174
- return false;
175
- },
176
- COMMAND_PRIORITY_CRITICAL,
177
- );
178
-
179
- await Promise.resolve().then();
180
-
181
- await ReactTestUtils.act(async () => {
182
- editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
183
- });
184
-
185
- expect(canRedo).toBe(false);
186
- expect(canUndo).toBe(false);
187
- });
188
-
189
- test('LexicalHistory.Redo after Quote Node', async () => {
190
- ReactTestUtils.act(() => {
191
- reactRoot.render(<Test key="smth" />);
192
- });
193
-
194
- // Wait for update to complete
195
- await Promise.resolve().then();
196
-
197
- await ReactTestUtils.act(async () => {
198
- await editor.update(() => {
199
- const root = $getRoot();
200
- const paragraph1 = $createParagraphNodeWithText('AAA');
201
- const paragraph2 = $createParagraphNodeWithText('BBB');
202
-
203
- // The editor has one child that is an empty
204
- // paragraph Node.
205
- root.getChildAtIndex(0)?.replace(paragraph1);
206
- root.append(paragraph2);
207
- });
208
- });
209
-
210
- const initialJSONState = editor.getEditorState().toJSON();
211
-
212
- await ReactTestUtils.act(async () => {
213
- await editor.update(() => {
214
- const root = $getRoot();
215
- const selection = $createRangeSelection();
216
-
217
- const firstTextNode = root.getAllTextNodes()[0];
218
- selection.anchor.set(firstTextNode.getKey(), 0, 'text');
219
- selection.focus.set(firstTextNode.getKey(), 3, 'text');
220
-
221
- $setSelection(selection);
222
- $setBlocksType(selection, () => $createQuoteNode());
223
- });
224
- });
225
-
226
- const afterQuoteInsertionJSONState = editor.getEditorState().toJSON();
227
- expect(afterQuoteInsertionJSONState.root.children.length).toBe(2);
228
- expect(afterQuoteInsertionJSONState.root.children[0].type).toBe('quote');
229
-
230
- expect(
231
- (afterQuoteInsertionJSONState.root.children as SerializedElementNode[])[0]
232
- .children.length,
233
- ).toBe(1);
234
- expect(
235
- (afterQuoteInsertionJSONState.root.children as SerializedElementNode[])[0]
236
- .children[0].type,
237
- ).toBe('text');
238
- expect(
239
- (
240
- (
241
- afterQuoteInsertionJSONState.root.children as SerializedElementNode[]
242
- )[0].children[0] as SerializedTextNode
243
- ).text,
244
- ).toBe('AAA');
245
-
246
- await ReactTestUtils.act(async () => {
247
- await editor.update(() => {
248
- editor.dispatchCommand(UNDO_COMMAND, undefined);
249
- });
250
- });
251
-
252
- expect(JSON.stringify(initialJSONState)).toBe(
253
- JSON.stringify(editor.getEditorState().toJSON()),
254
- );
255
- });
256
-
257
- test('LexicalHistory in sequence: change, undo, redo, undo, change', async () => {
258
- let canRedo = false;
259
- let canUndo = false;
260
-
261
- ReactTestUtils.act(() => {
262
- reactRoot.render(<Test key="smth" />);
263
- });
264
-
265
- editor.registerCommand<boolean>(
266
- CAN_REDO_COMMAND,
267
- (payload) => {
268
- canRedo = payload;
269
- return false;
270
- },
271
- COMMAND_PRIORITY_CRITICAL,
272
- );
273
-
274
- editor.registerCommand<boolean>(
275
- CAN_UNDO_COMMAND,
276
- (payload) => {
277
- canUndo = payload;
278
- return false;
279
- },
280
- COMMAND_PRIORITY_CRITICAL,
281
- );
282
-
283
- // focus (needs the focus to initialize)
284
- await ReactTestUtils.act(async () => {
285
- editor.focus();
286
- });
287
-
288
- expect(canRedo).toBe(false);
289
- expect(canUndo).toBe(false);
290
-
291
- // change
292
- await ReactTestUtils.act(async () => {
293
- await editor.update(() => {
294
- const root = $getRoot();
295
- const paragraph = $createParagraphNodeWithText('foo');
296
- root.append(paragraph);
297
- });
298
- });
299
- expect(canRedo).toBe(false);
300
- expect(canUndo).toBe(true);
301
-
302
- // undo
303
- await ReactTestUtils.act(async () => {
304
- await editor.update(() => {
305
- editor.dispatchCommand(UNDO_COMMAND, undefined);
306
- });
307
- });
308
- expect(canRedo).toBe(true);
309
- expect(canUndo).toBe(false);
310
-
311
- // redo
312
- await ReactTestUtils.act(async () => {
313
- await editor.update(() => {
314
- editor.dispatchCommand(REDO_COMMAND, undefined);
315
- });
316
- });
317
- expect(canRedo).toBe(false);
318
- expect(canUndo).toBe(true);
319
-
320
- // undo
321
- await ReactTestUtils.act(async () => {
322
- await editor.update(() => {
323
- editor.dispatchCommand(UNDO_COMMAND, undefined);
324
- });
325
- });
326
- expect(canRedo).toBe(true);
327
- expect(canUndo).toBe(false);
328
-
329
- // change
330
- await ReactTestUtils.act(async () => {
331
- await editor.update(() => {
332
- const root = $getRoot();
333
- const paragraph = $createParagraphNodeWithText('foo');
334
- root.append(paragraph);
335
- });
336
- });
337
-
338
- expect(canRedo).toBe(false);
339
- expect(canUndo).toBe(true);
340
- });
341
-
342
- test('undoStack selection points to the same editor', async () => {
343
- const editor_ = createTestEditor({namespace: 'parent'});
344
- const sharedHistory = createEmptyHistoryState();
345
- registerHistory(editor_, sharedHistory, 1000);
346
- await editor_.update(() => {
347
- const root = $getRoot();
348
- const paragraph = $createParagraphNode();
349
- root.append(paragraph);
350
- });
351
- await editor_.update(() => {
352
- const root = $getRoot();
353
- const paragraph = $createParagraphNode();
354
- root.append(paragraph);
355
- const nodeSelection = $createNodeSelection();
356
- nodeSelection.add(paragraph.getKey());
357
- $setSelection(nodeSelection);
358
- });
359
- const nestedEditor = createTestEditor({namespace: 'nested'});
360
- await nestedEditor.update(
361
- () => {
362
- const root = $getRoot();
363
- const paragraph = $createParagraphNode();
364
- root.append(paragraph);
365
- paragraph.selectEnd();
366
- },
367
- {
368
- tag: HISTORY_MERGE_TAG,
369
- },
370
- );
371
- nestedEditor._parentEditor = editor_;
372
- registerHistory(nestedEditor, sharedHistory, 1000);
373
-
374
- await nestedEditor.update(() => {
375
- const root = $getRoot();
376
- const paragraph = $createParagraphNode();
377
- root.append(paragraph);
378
- paragraph.selectEnd();
379
- });
380
-
381
- expect(sharedHistory.undoStack.length).toBe(2);
382
- await editor_.dispatchCommand(UNDO_COMMAND, undefined);
383
- expect($isNodeSelection(editor_.getEditorState()._selection)).toBe(true);
384
- });
385
-
386
- test('Changes to TextNode leaf are detected properly #6409', async () => {
387
- editor = createTestEditor({
388
- nodes: [CustomTextNode],
389
- });
390
- const sharedHistory = createEmptyHistoryState();
391
- registerHistory(editor, sharedHistory, 0);
392
- editor.update(
393
- () => {
394
- $getRoot()
395
- .clear()
396
- .append(
397
- $createParagraphNode().append(
398
- $createCustomTextNode('Initial text'),
399
- ),
400
- );
401
- },
402
- {discrete: true},
403
- );
404
- expect(sharedHistory.undoStack).toHaveLength(0);
405
-
406
- editor.update(
407
- () => {
408
- // Mark dirty with no changes
409
- for (const node of $getRoot().getAllTextNodes()) {
410
- node.getWritable();
411
- }
412
- // Restore the editor state and ensure the history did not change
413
- $restoreEditorState(editor, editor.getEditorState());
414
- },
415
- {discrete: true},
416
- );
417
- expect(sharedHistory.undoStack).toHaveLength(0);
418
- editor.update(
419
- () => {
420
- // Mark dirty with text change
421
- for (const node of $getRoot().getAllTextNodes()) {
422
- if ($isCustomTextNode(node)) {
423
- node.setTextContent(node.getTextContent() + '!');
424
- }
425
- }
426
- },
427
- {discrete: true},
428
- );
429
- expect(sharedHistory.undoStack).toHaveLength(1);
430
-
431
- editor.update(
432
- () => {
433
- // Mark dirty with only a change to the class
434
- for (const node of $getRoot().getAllTextNodes()) {
435
- if ($isCustomTextNode(node)) {
436
- node.addClass('updated');
437
- }
438
- }
439
- },
440
- {discrete: true},
441
- );
442
- expect(sharedHistory.undoStack).toHaveLength(2);
443
- });
444
- });
445
-
446
- const $createParagraphNodeWithText = (text: string) => {
447
- const paragraph = $createParagraphNode();
448
- const textNode = $createTextNode(text);
449
-
450
- paragraph.append(textNode);
451
- return paragraph;
452
- };