mirror of
https://github.com/facebook/lexical.git
synced 2025-05-20 16:48:04 +08:00
Attached/detached nodes listener (#1292)
This commit is contained in:
@ -135,6 +135,10 @@ export type RegisteredNode = {
|
||||
};
|
||||
export type Transform<T> = (node: T) => void;
|
||||
|
||||
export type MutationListeners = Map<MutationListener, Class<LexicalNode>>;
|
||||
export type MutatedNodes = Map<Class<LexicalNode>, Map<NodeKey, NodeMutation>>;
|
||||
export type NodeMutation = 'attached' | 'detached';
|
||||
|
||||
export type ErrorListener = (error: Error) => void;
|
||||
export type UpdateListener = ({
|
||||
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
||||
@ -150,6 +154,7 @@ export type RootListener = (
|
||||
prevRootElement: null | HTMLElement,
|
||||
) => void;
|
||||
export type TextContentListener = (text: string) => void;
|
||||
export type MutationListener = (nodes: Map<NodeKey, NodeMutation>) => void;
|
||||
export type CommandListener = (
|
||||
type: string,
|
||||
payload: CommandPayload,
|
||||
@ -176,6 +181,7 @@ type Listeners = {
|
||||
command: Array<Set<CommandListener>>,
|
||||
decorator: Set<DecoratorListener>,
|
||||
error: Set<ErrorListener>,
|
||||
mutation: MutationListeners,
|
||||
root: Set<RootListener>,
|
||||
textcontent: Set<TextContentListener>,
|
||||
update: Set<UpdateListener>,
|
||||
@ -187,6 +193,7 @@ export type ListenerType =
|
||||
| 'root'
|
||||
| 'decorator'
|
||||
| 'textcontent'
|
||||
| 'mutation'
|
||||
| 'command';
|
||||
|
||||
export type TransformerType = 'text' | 'decorator' | 'element' | 'root';
|
||||
@ -338,6 +345,7 @@ class BaseLexicalEditor {
|
||||
command: [new Set(), new Set(), new Set(), new Set(), new Set()],
|
||||
decorator: new Set(),
|
||||
error: new Set(),
|
||||
mutation: new Map(),
|
||||
root: new Set(),
|
||||
textcontent: new Set(),
|
||||
update: new Set(),
|
||||
@ -366,31 +374,60 @@ class BaseLexicalEditor {
|
||||
}
|
||||
addListener(
|
||||
type: ListenerType,
|
||||
listener:
|
||||
arg1:
|
||||
| ErrorListener
|
||||
| UpdateListener
|
||||
| DecoratorListener
|
||||
| RootListener
|
||||
| TextContentListener
|
||||
| CommandListener,
|
||||
priority: CommandListenerPriority,
|
||||
| CommandListener
|
||||
| Class<LexicalNode>,
|
||||
arg2: MutationListener | CommandListenerPriority,
|
||||
): () => void {
|
||||
const listenerSetOrMap = this._listeners[type];
|
||||
if (type === 'command') {
|
||||
// $FlowFixMe: TODO refine
|
||||
const listener: CommandListener = arg1;
|
||||
// $FlowFixMe: TODO refine
|
||||
const priority = (arg2: CommandListenerPriority);
|
||||
if (priority === undefined) {
|
||||
invariant(false, 'Listener for type "command" requires a "priority".');
|
||||
}
|
||||
|
||||
// $FlowFixMe: unsure how to csae this
|
||||
// $FlowFixMe: unsure how to cast this
|
||||
const commands: Array<Set<CommandListener>> = listenerSetOrMap;
|
||||
const commandSet = commands[priority];
|
||||
// $FlowFixMe: cast
|
||||
commandSet.add(listener);
|
||||
return () => {
|
||||
// $FlowFixMe: cast
|
||||
commandSet.delete(listener);
|
||||
};
|
||||
} else if (type === 'mutation') {
|
||||
// $FlowFixMe: refine
|
||||
const klass = (arg1: Class<LexicalNode>);
|
||||
// $FlowFixMe: refine
|
||||
const mutationListener = (arg2: MutationListener);
|
||||
const registeredNode = this._nodes.get(klass.getType());
|
||||
if (registeredNode === undefined) {
|
||||
invariant(
|
||||
false,
|
||||
'Node %s has not been registered. Ensure node has been passed to createEditor.',
|
||||
klass.name,
|
||||
);
|
||||
}
|
||||
const mutations = this._listeners.mutation;
|
||||
mutations.set(mutationListener, klass);
|
||||
return () => {
|
||||
mutations.delete(mutationListener);
|
||||
};
|
||||
} else {
|
||||
const listener:
|
||||
| ErrorListener
|
||||
| UpdateListener
|
||||
| DecoratorListener
|
||||
| RootListener
|
||||
| TextContentListener
|
||||
// $FlowFixMe: TODO refine
|
||||
| CommandListener = arg1;
|
||||
// $FlowFixMe: TODO refine this from the above types
|
||||
listenerSetOrMap.add(listener);
|
||||
|
||||
@ -601,6 +638,11 @@ declare export class LexicalEditor {
|
||||
addListener(type: 'root', listener: RootListener): () => void;
|
||||
addListener(type: 'decorator', listener: DecoratorListener): () => void;
|
||||
addListener(type: 'textcontent', listener: TextContentListener): () => void;
|
||||
addListener(
|
||||
type: 'mutation',
|
||||
klass: Class<LexicalNode>,
|
||||
listener: MutationListener,
|
||||
): () => void;
|
||||
addListener(
|
||||
type: 'command',
|
||||
listener: CommandListener,
|
||||
|
@ -31,10 +31,10 @@ import {
|
||||
getActiveEditorState,
|
||||
} from './LexicalUpdates';
|
||||
import {
|
||||
$generateKey,
|
||||
$getCompositionKey,
|
||||
$getNodeByKey,
|
||||
$setCompositionKey,
|
||||
$setNodeKey,
|
||||
internalMarkNodeAsDirty,
|
||||
internalMarkSiblingsAsDirty,
|
||||
} from './LexicalUtils';
|
||||
@ -131,8 +131,8 @@ export class LexicalNode {
|
||||
|
||||
constructor(key?: NodeKey): void {
|
||||
this.__type = this.constructor.getType();
|
||||
this.__key = key || $generateKey(this);
|
||||
this.__parent = null;
|
||||
$setNodeKey(this, key);
|
||||
|
||||
// Ensure custom nodes implement required methods.
|
||||
if (__DEV__) {
|
||||
|
@ -11,6 +11,8 @@ import type {
|
||||
EditorConfig,
|
||||
IntentionallyMarkedAsDirtyElement,
|
||||
LexicalEditor,
|
||||
MutatedNodes,
|
||||
RegisteredNodes,
|
||||
} from './LexicalEditor';
|
||||
import type {NodeKey, NodeMap} from './LexicalNode';
|
||||
import type {RangeSelection} from './LexicalSelection';
|
||||
@ -40,6 +42,7 @@ import {
|
||||
getDOMTextNode,
|
||||
getTextDirection,
|
||||
isSelectionWithinEditor,
|
||||
setMutatedNode,
|
||||
} from './LexicalUtils';
|
||||
|
||||
let subTreeTextContent = '';
|
||||
@ -47,6 +50,7 @@ let subTreeDirectionedTextContent = '';
|
||||
let editorTextContent = '';
|
||||
let activeEditorConfig: EditorConfig<{...}>;
|
||||
let activeEditor: LexicalEditor;
|
||||
let activeEditorNodes: RegisteredNodes;
|
||||
let treatAllNodesAsDirty: boolean = false;
|
||||
let activeEditorStateReadOnly: boolean = false;
|
||||
let activeTextDirection = null;
|
||||
@ -55,6 +59,7 @@ let activeDirtyLeaves: Set<NodeKey>;
|
||||
let activePrevNodeMap: NodeMap;
|
||||
let activeNextNodeMap: NodeMap;
|
||||
let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
|
||||
let mutatedNodes: MutatedNodes;
|
||||
|
||||
function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
|
||||
const node = activePrevNodeMap.get(key);
|
||||
@ -72,6 +77,9 @@ function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
|
||||
const children = node.__children;
|
||||
destroyChildren(children, 0, children.length - 1, null);
|
||||
}
|
||||
if (node !== undefined) {
|
||||
setMutatedNode(mutatedNodes, activeEditorNodes, node, 'detached');
|
||||
}
|
||||
}
|
||||
|
||||
function destroyChildren(
|
||||
@ -199,6 +207,7 @@ function createNode(
|
||||
// Freeze the node in DEV to prevent accidental mutations
|
||||
Object.freeze(node);
|
||||
}
|
||||
setMutatedNode(mutatedNodes, activeEditorNodes, node, 'attached');
|
||||
return dom;
|
||||
}
|
||||
|
||||
@ -612,7 +621,7 @@ function reconcileRoot(
|
||||
dirtyType: 0 | 1 | 2,
|
||||
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
||||
dirtyLeaves: Set<NodeKey>,
|
||||
): void {
|
||||
): MutatedNodes {
|
||||
subTreeTextContent = '';
|
||||
editorTextContent = '';
|
||||
subTreeDirectionedTextContent = '';
|
||||
@ -622,12 +631,15 @@ function reconcileRoot(
|
||||
activeTextDirection = null;
|
||||
activeEditor = editor;
|
||||
activeEditorConfig = editor._config;
|
||||
activeEditorNodes = editor._nodes;
|
||||
activeDirtyElements = dirtyElements;
|
||||
activeDirtyLeaves = dirtyLeaves;
|
||||
activePrevNodeMap = prevEditorState._nodeMap;
|
||||
activeNextNodeMap = nextEditorState._nodeMap;
|
||||
activeEditorStateReadOnly = nextEditorState._readOnly;
|
||||
activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
|
||||
const currentMutatedNodes = new Map();
|
||||
mutatedNodes = currentMutatedNodes;
|
||||
reconcileNode('root', null);
|
||||
|
||||
// We don't want a bunch of void checks throughout the scope
|
||||
@ -637,6 +649,8 @@ function reconcileRoot(
|
||||
// $FlowFixMe
|
||||
activeEditor = undefined;
|
||||
// $FlowFixMe
|
||||
activeEditorNodes = undefined;
|
||||
// $FlowFixMe
|
||||
activeDirtyElements = undefined;
|
||||
// $FlowFixMe
|
||||
activeDirtyLeaves = undefined;
|
||||
@ -648,6 +662,10 @@ function reconcileRoot(
|
||||
activeEditorConfig = undefined;
|
||||
// $FlowFixMe
|
||||
activePrevKeyToDOMMap = undefined;
|
||||
// $FlowFixMe
|
||||
mutatedNodes = undefined;
|
||||
|
||||
return currentMutatedNodes;
|
||||
}
|
||||
|
||||
export function updateEditorState(
|
||||
@ -658,8 +676,9 @@ export function updateEditorState(
|
||||
pendingSelection: RangeSelection | null,
|
||||
needsUpdate: boolean,
|
||||
editor: LexicalEditor,
|
||||
): void {
|
||||
): null | MutatedNodes {
|
||||
const observer = editor._observer;
|
||||
let reconcileMutatedNodes = null;
|
||||
|
||||
if (needsUpdate && observer !== null) {
|
||||
const dirtyType = editor._dirtyType;
|
||||
@ -668,7 +687,7 @@ export function updateEditorState(
|
||||
|
||||
observer.disconnect();
|
||||
try {
|
||||
reconcileRoot(
|
||||
reconcileMutatedNodes = reconcileRoot(
|
||||
currentEditorState,
|
||||
pendingEditorState,
|
||||
editor,
|
||||
@ -698,6 +717,8 @@ export function updateEditorState(
|
||||
domSelection,
|
||||
);
|
||||
}
|
||||
|
||||
return reconcileMutatedNodes;
|
||||
}
|
||||
|
||||
function scrollIntoViewIfNeeded(node: Node, rootElement: ?HTMLElement): void {
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
CommandPayload,
|
||||
EditorUpdateOptions,
|
||||
LexicalEditor,
|
||||
MutatedNodes,
|
||||
Transform,
|
||||
} from './LexicalEditor';
|
||||
import type {ParsedEditorState} from './LexicalEditorState';
|
||||
@ -346,7 +347,7 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
|
||||
editor._updating = true;
|
||||
|
||||
try {
|
||||
updateEditorState(
|
||||
const mutatedNodes = updateEditorState(
|
||||
rootElement,
|
||||
currentEditorState,
|
||||
pendingEditorState,
|
||||
@ -355,6 +356,14 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
|
||||
needsUpdate,
|
||||
editor,
|
||||
);
|
||||
if (mutatedNodes !== null) {
|
||||
triggerMutationListeners(
|
||||
editor,
|
||||
currentEditorState,
|
||||
pendingEditorState,
|
||||
mutatedNodes,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Report errors
|
||||
triggerListeners('error', editor, false, error);
|
||||
@ -423,6 +432,22 @@ function triggerTextContentListeners(
|
||||
}
|
||||
}
|
||||
|
||||
function triggerMutationListeners(
|
||||
editor: LexicalEditor,
|
||||
currentEditorState: EditorState,
|
||||
pendingEditorState: EditorState,
|
||||
mutatedNodes: MutatedNodes,
|
||||
): void {
|
||||
const listeners = editor._listeners.mutation;
|
||||
listeners.forEach((klass, listener) => {
|
||||
const mutatedNodesByType = mutatedNodes.get(klass);
|
||||
if (mutatedNodesByType === undefined) {
|
||||
return;
|
||||
}
|
||||
listener(mutatedNodesByType);
|
||||
});
|
||||
}
|
||||
|
||||
export function triggerListeners(
|
||||
type: 'update' | 'error' | 'root' | 'decorator' | 'textcontent',
|
||||
|
||||
|
@ -10,7 +10,10 @@
|
||||
import type {
|
||||
IntentionallyMarkedAsDirtyElement,
|
||||
LexicalEditor,
|
||||
MutatedNodes,
|
||||
NodeMutation,
|
||||
RegisteredNode,
|
||||
RegisteredNodes,
|
||||
} from './LexicalEditor';
|
||||
import type {EditorState} from './LexicalEditorState';
|
||||
import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
|
||||
@ -157,7 +160,11 @@ export function $isLeafNode(node: ?LexicalNode): boolean %checks {
|
||||
return $isTextNode(node) || $isLineBreakNode(node) || $isDecoratorNode(node);
|
||||
}
|
||||
|
||||
export function $generateKey(node: LexicalNode): NodeKey {
|
||||
export function $setNodeKey(node: LexicalNode, existingKey: ?NodeKey): void {
|
||||
if (existingKey != null) {
|
||||
node.__key = existingKey;
|
||||
return;
|
||||
}
|
||||
errorOnReadOnly();
|
||||
errorOnInfiniteTransforms();
|
||||
const editor = getActiveEditor();
|
||||
@ -172,7 +179,7 @@ export function $generateKey(node: LexicalNode): NodeKey {
|
||||
}
|
||||
editor._cloneNotNeeded.add(key);
|
||||
editor._dirtyType = HAS_DIRTY_NODES;
|
||||
return key;
|
||||
node.__key = key;
|
||||
}
|
||||
|
||||
function internalMarkParentElementsAsDirty(
|
||||
@ -779,3 +786,22 @@ export function getCachedClassNameArray<Theme: {...}>(
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
|
||||
export function setMutatedNode(
|
||||
mutatedNodes: MutatedNodes,
|
||||
registeredNodes: RegisteredNodes,
|
||||
node: LexicalNode,
|
||||
mutation: NodeMutation,
|
||||
) {
|
||||
const registeredNode = registeredNodes.get(node.__type);
|
||||
if (registeredNode === undefined) {
|
||||
invariant(false, 'Type %s not in registeredNodes', node.__type);
|
||||
}
|
||||
const klass = registeredNode.klass;
|
||||
let mutatedNodesByType = mutatedNodes.get(klass);
|
||||
if (mutatedNodesByType === undefined) {
|
||||
mutatedNodesByType = new Map();
|
||||
mutatedNodes.set(klass, mutatedNodesByType);
|
||||
}
|
||||
mutatedNodesByType.set(node.__key, mutation);
|
||||
}
|
||||
|
@ -1337,4 +1337,167 @@ describe('LexicalEditor tests', () => {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenCalledWith('foobar');
|
||||
});
|
||||
|
||||
it('mutation listener', async () => {
|
||||
init();
|
||||
const paragraphMutations = jest.fn();
|
||||
const textNodeMutations = jest.fn();
|
||||
editor.addListener('mutation', ParagraphNode, paragraphMutations);
|
||||
editor.addListener('mutation', TextNode, textNodeMutations);
|
||||
|
||||
const paragraphKeys = [];
|
||||
const textNodeKeys = [];
|
||||
// No await intentional (batch with next)
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode('foo');
|
||||
root.append(paragraph);
|
||||
paragraph.append(textNode);
|
||||
paragraphKeys.push(paragraph.getKey());
|
||||
textNodeKeys.push(textNode.getKey());
|
||||
});
|
||||
await editor.update(() => {
|
||||
const textNode = $getNodeByKey(textNodeKeys[0]);
|
||||
const textNode2 = $createTextNode('bar').toggleFormat('bold');
|
||||
const textNode3 = $createTextNode('xyz').toggleFormat('italic');
|
||||
textNode.insertAfter(textNode2);
|
||||
textNode2.insertAfter(textNode3);
|
||||
textNodeKeys.push(textNode2.getKey());
|
||||
textNodeKeys.push(textNode3.getKey());
|
||||
});
|
||||
await editor.update(() => {
|
||||
$getRoot().clear();
|
||||
});
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
paragraphKeys.push(paragraph.getKey());
|
||||
// Created and deleted in the same update
|
||||
textNodeKeys.push($createTextNode('zzz').getKey());
|
||||
});
|
||||
expect(paragraphMutations.mock.calls.length).toBe(3);
|
||||
expect(textNodeMutations.mock.calls.length).toBe(2);
|
||||
|
||||
const [paragraphMutation1, paragraphMutation2, paragraphMutation3] =
|
||||
paragraphMutations.mock.calls;
|
||||
const [textNodeMutation1, textNodeMutation2] = textNodeMutations.mock.calls;
|
||||
expect(paragraphMutation1[0].size).toBe(1);
|
||||
expect(paragraphMutation1[0].get(paragraphKeys[0])).toBe('attached');
|
||||
expect(paragraphMutation1[0].size).toBe(1);
|
||||
expect(paragraphMutation2[0].get(paragraphKeys[0])).toBe('detached');
|
||||
expect(paragraphMutation3[0].size).toBe(1);
|
||||
expect(paragraphMutation3[0].get(paragraphKeys[1])).toBe('attached');
|
||||
expect(textNodeMutation1[0].size).toBe(3);
|
||||
expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('attached');
|
||||
expect(textNodeMutation1[0].get(textNodeKeys[1])).toBe('attached');
|
||||
expect(textNodeMutation1[0].get(textNodeKeys[2])).toBe('attached');
|
||||
expect(textNodeMutation2[0].size).toBe(3);
|
||||
expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('detached');
|
||||
expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('detached');
|
||||
expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('detached');
|
||||
});
|
||||
|
||||
it('mutation listener with setEditorState', async () => {
|
||||
init();
|
||||
await editor.update(() => {
|
||||
$getRoot().append($createParagraphNode());
|
||||
});
|
||||
const initialEditorState = editor.getEditorState();
|
||||
const textNodeMutations = jest.fn();
|
||||
editor.addListener('mutation', TextNode, textNodeMutations);
|
||||
|
||||
const textNodeKeys = [];
|
||||
await editor.update(() => {
|
||||
const paragraph = $getRoot().getFirstChild();
|
||||
const textNode1 = $createTextNode('foo');
|
||||
paragraph.append(textNode1);
|
||||
textNodeKeys.push(textNode1.getKey());
|
||||
});
|
||||
const fooEditorState = editor.getEditorState();
|
||||
await editor.setEditorState(initialEditorState);
|
||||
// This line should have no effect on the mutation listeners
|
||||
const parsedFooEditorState = editor.parseEditorState(
|
||||
JSON.stringify(fooEditorState),
|
||||
);
|
||||
await editor.update(() => {
|
||||
const paragraph = $getRoot().getFirstChild();
|
||||
const textNode2 = $createTextNode('bar').toggleFormat('bold');
|
||||
const textNode3 = $createTextNode('xyz').toggleFormat('italic');
|
||||
paragraph.append(textNode2, textNode3);
|
||||
textNodeKeys.push(textNode2.getKey(), textNode3.getKey());
|
||||
});
|
||||
await editor.setEditorState(parsedFooEditorState);
|
||||
|
||||
expect(textNodeMutations.mock.calls.length).toBe(4);
|
||||
const [
|
||||
textNodeMutation1,
|
||||
textNodeMutation2,
|
||||
textNodeMutation3,
|
||||
textNodeMutation4,
|
||||
] = textNodeMutations.mock.calls;
|
||||
expect(textNodeMutation1[0].size).toBe(1);
|
||||
expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('attached');
|
||||
expect(textNodeMutation2[0].size).toBe(1);
|
||||
expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('detached');
|
||||
expect(textNodeMutation3[0].size).toBe(2);
|
||||
expect(textNodeMutation3[0].get(textNodeKeys[1])).toBe('attached');
|
||||
expect(textNodeMutation3[0].get(textNodeKeys[2])).toBe('attached');
|
||||
expect(textNodeMutation4[0].size).toBe(3); // +1 newly generated key by parseEditorState
|
||||
expect(textNodeMutation4[0].get(textNodeKeys[1])).toBe('detached');
|
||||
expect(textNodeMutation4[0].get(textNodeKeys[2])).toBe('detached');
|
||||
});
|
||||
|
||||
it('mutation listeners does not trigger when other node types are mutated', async () => {
|
||||
init();
|
||||
const paragraphMutations = jest.fn();
|
||||
const textNodeMutations = jest.fn();
|
||||
editor.addListener('mutation', ParagraphNode, paragraphMutations);
|
||||
editor.addListener('mutation', TextNode, textNodeMutations);
|
||||
|
||||
await editor.update(() => {
|
||||
$getRoot().append($createParagraphNode());
|
||||
});
|
||||
|
||||
expect(paragraphMutations.mock.calls.length).toBe(1);
|
||||
expect(textNodeMutations.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('mutation listeners with normalization', async () => {
|
||||
init();
|
||||
const textNodeMutations = jest.fn();
|
||||
editor.addListener('mutation', TextNode, textNodeMutations);
|
||||
|
||||
const textNodeKeys = [];
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode1 = $createTextNode('foo');
|
||||
const textNode2 = $createTextNode('bar');
|
||||
textNodeKeys.push(textNode1.getKey(), textNode2.getKey());
|
||||
root.append(paragraph);
|
||||
paragraph.append(textNode1, textNode2);
|
||||
});
|
||||
await editor.update(() => {
|
||||
const paragraph = $getRoot().getFirstChild();
|
||||
const textNode3 = $createTextNode('xyz').toggleFormat('bold');
|
||||
paragraph.append(textNode3);
|
||||
textNodeKeys.push(textNode3.getKey());
|
||||
});
|
||||
await editor.update(() => {
|
||||
const textNode3 = $getNodeByKey(textNodeKeys[2]);
|
||||
textNode3.toggleFormat('bold'); // Normalize with foobar
|
||||
});
|
||||
|
||||
expect(textNodeMutations.mock.calls.length).toBe(3);
|
||||
const [textNodeMutation1, textNodeMutation2, textNodeMutation3] =
|
||||
textNodeMutations.mock.calls;
|
||||
expect(textNodeMutation1[0].size).toBe(1);
|
||||
expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('attached');
|
||||
expect(textNodeMutation2[0].size).toBe(1);
|
||||
expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('attached');
|
||||
expect(textNodeMutation3[0].size).toBe(1);
|
||||
expect(textNodeMutation3[0].get(textNodeKeys[2])).toBe('detached');
|
||||
});
|
||||
});
|
||||
|
@ -82,7 +82,7 @@ describe('LexicalEditorState tests', () => {
|
||||
$getRoot().append(paragraph);
|
||||
});
|
||||
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
|
||||
`{\"_nodeMap\":[[\"root\",{\"__type\":\"root\",\"__key\":\"root\",\"__parent\":null,\"__children\":[\"1\"],\"__format\":0,\"__indent\":0,\"__dir\":\"ltr\",\"__cachedText\":\"Hello world\"}],[\"1\",{\"__type\":\"paragraph\",\"__key\":\"1\",\"__parent\":\"root\",\"__children\":[\"2\"],\"__format\":0,\"__indent\":0,\"__dir\":\"ltr\"}],[\"2\",{\"__type\":\"text\",\"__key\":\"2\",\"__parent\":\"1\",\"__text\":\"Hello world\",\"__format\":0,\"__style\":\"\",\"__mode\":0,\"__detail\":0}]],\"_selection\":{\"anchor\":{\"key\":\"2\",\"offset\":6,\"type\":\"text\"},\"focus\":{\"key\":\"2\",\"offset\":11,\"type\":\"text\"}}}`,
|
||||
`{\"_nodeMap\":[[\"root\",{\"__type\":\"root\",\"__parent\":null,\"__key\":\"root\",\"__children\":[\"1\"],\"__format\":0,\"__indent\":0,\"__dir\":\"ltr\",\"__cachedText\":\"Hello world\"}],[\"1\",{\"__type\":\"paragraph\",\"__parent\":\"root\",\"__key\":\"1\",\"__children\":[\"2\"],\"__format\":0,\"__indent\":0,\"__dir\":\"ltr\"}],[\"2\",{\"__type\":\"text\",\"__parent\":\"1\",\"__key\":\"2\",\"__text\":\"Hello world\",\"__format\":0,\"__style\":\"\",\"__mode\":0,\"__detail\":0}]],\"_selection\":{\"anchor\":{\"key\":\"2\",\"offset\":6,\"type\":\"text\"},\"focus\":{\"key\":\"2\",\"offset\":11,\"type\":\"text\"}}}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -62,6 +62,7 @@ export type {
|
||||
EditorThemeClasses,
|
||||
IntentionallyMarkedAsDirtyElement,
|
||||
LexicalEditor,
|
||||
NodeMutation,
|
||||
} from './LexicalEditor';
|
||||
export type {EditorState, ParsedEditorState} from './LexicalEditorState';
|
||||
export type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
|
||||
|
@ -49,5 +49,20 @@
|
||||
"49": "append: attemtping to append self",
|
||||
"50": "Node %s and selection point do not match.",
|
||||
"51": "setIndent is not implemented on ListItemNode",
|
||||
"52": "Expected node %s to to be a ElementNode."
|
||||
"52": "Expected node %s to to be a ElementNode.",
|
||||
"53": "LexicalAutoLinkPlugin: AutoLinkNode, TableCellNode or TableRowNode not registered on editor",
|
||||
"54": "TablePlugin: TableNode, TableCellNode or TableRowNode not registered on editor",
|
||||
"55": "Grid not found.",
|
||||
"56": "Listener for type \"command\" requires a \"priority\".",
|
||||
"57": "Node %s has not been registered. Ensure node has been passed to createEditor.",
|
||||
"58": "useCharacterLimit: OverflowNode not registered on editor",
|
||||
"59": "Attached node not in nextNodeMap",
|
||||
"60": "Mutation listener on an unregistered node %s",
|
||||
"61": "Create node: Attempted to create node %s that was not previously registered on the editor. You can use register your custom nodes.",
|
||||
"62": "joinedText was not calculated",
|
||||
"63": "Bad regEx pattern found for %s",
|
||||
"64": "Expected anchor and focus offsets to have ascending character order.",
|
||||
"65": "The capture group count in the RegEx does match the actual capture group count.",
|
||||
"66": "Type %s not in registeredNodes",
|
||||
"67": "The text content string length does not correlate with insertions/deletions of new text."
|
||||
}
|
||||
|
Reference in New Issue
Block a user