mirror of
https://github.com/facebook/lexical.git
synced 2025-05-21 09:07:44 +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 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 ErrorListener = (error: Error) => void;
|
||||||
export type UpdateListener = ({
|
export type UpdateListener = ({
|
||||||
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
||||||
@ -150,6 +154,7 @@ export type RootListener = (
|
|||||||
prevRootElement: null | HTMLElement,
|
prevRootElement: null | HTMLElement,
|
||||||
) => void;
|
) => void;
|
||||||
export type TextContentListener = (text: string) => void;
|
export type TextContentListener = (text: string) => void;
|
||||||
|
export type MutationListener = (nodes: Map<NodeKey, NodeMutation>) => void;
|
||||||
export type CommandListener = (
|
export type CommandListener = (
|
||||||
type: string,
|
type: string,
|
||||||
payload: CommandPayload,
|
payload: CommandPayload,
|
||||||
@ -176,6 +181,7 @@ type Listeners = {
|
|||||||
command: Array<Set<CommandListener>>,
|
command: Array<Set<CommandListener>>,
|
||||||
decorator: Set<DecoratorListener>,
|
decorator: Set<DecoratorListener>,
|
||||||
error: Set<ErrorListener>,
|
error: Set<ErrorListener>,
|
||||||
|
mutation: MutationListeners,
|
||||||
root: Set<RootListener>,
|
root: Set<RootListener>,
|
||||||
textcontent: Set<TextContentListener>,
|
textcontent: Set<TextContentListener>,
|
||||||
update: Set<UpdateListener>,
|
update: Set<UpdateListener>,
|
||||||
@ -187,6 +193,7 @@ export type ListenerType =
|
|||||||
| 'root'
|
| 'root'
|
||||||
| 'decorator'
|
| 'decorator'
|
||||||
| 'textcontent'
|
| 'textcontent'
|
||||||
|
| 'mutation'
|
||||||
| 'command';
|
| 'command';
|
||||||
|
|
||||||
export type TransformerType = 'text' | 'decorator' | 'element' | 'root';
|
export type TransformerType = 'text' | 'decorator' | 'element' | 'root';
|
||||||
@ -338,6 +345,7 @@ class BaseLexicalEditor {
|
|||||||
command: [new Set(), new Set(), new Set(), new Set(), new Set()],
|
command: [new Set(), new Set(), new Set(), new Set(), new Set()],
|
||||||
decorator: new Set(),
|
decorator: new Set(),
|
||||||
error: new Set(),
|
error: new Set(),
|
||||||
|
mutation: new Map(),
|
||||||
root: new Set(),
|
root: new Set(),
|
||||||
textcontent: new Set(),
|
textcontent: new Set(),
|
||||||
update: new Set(),
|
update: new Set(),
|
||||||
@ -366,31 +374,60 @@ class BaseLexicalEditor {
|
|||||||
}
|
}
|
||||||
addListener(
|
addListener(
|
||||||
type: ListenerType,
|
type: ListenerType,
|
||||||
listener:
|
arg1:
|
||||||
| ErrorListener
|
| ErrorListener
|
||||||
| UpdateListener
|
| UpdateListener
|
||||||
| DecoratorListener
|
| DecoratorListener
|
||||||
| RootListener
|
| RootListener
|
||||||
| TextContentListener
|
| TextContentListener
|
||||||
| CommandListener,
|
| CommandListener
|
||||||
priority: CommandListenerPriority,
|
| Class<LexicalNode>,
|
||||||
|
arg2: MutationListener | CommandListenerPriority,
|
||||||
): () => void {
|
): () => void {
|
||||||
const listenerSetOrMap = this._listeners[type];
|
const listenerSetOrMap = this._listeners[type];
|
||||||
if (type === 'command') {
|
if (type === 'command') {
|
||||||
|
// $FlowFixMe: TODO refine
|
||||||
|
const listener: CommandListener = arg1;
|
||||||
|
// $FlowFixMe: TODO refine
|
||||||
|
const priority = (arg2: CommandListenerPriority);
|
||||||
if (priority === undefined) {
|
if (priority === undefined) {
|
||||||
invariant(false, 'Listener for type "command" requires a "priority".');
|
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 commands: Array<Set<CommandListener>> = listenerSetOrMap;
|
||||||
const commandSet = commands[priority];
|
const commandSet = commands[priority];
|
||||||
// $FlowFixMe: cast
|
|
||||||
commandSet.add(listener);
|
commandSet.add(listener);
|
||||||
return () => {
|
return () => {
|
||||||
// $FlowFixMe: cast
|
|
||||||
commandSet.delete(listener);
|
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 {
|
} else {
|
||||||
|
const listener:
|
||||||
|
| ErrorListener
|
||||||
|
| UpdateListener
|
||||||
|
| DecoratorListener
|
||||||
|
| RootListener
|
||||||
|
| TextContentListener
|
||||||
|
// $FlowFixMe: TODO refine
|
||||||
|
| CommandListener = arg1;
|
||||||
// $FlowFixMe: TODO refine this from the above types
|
// $FlowFixMe: TODO refine this from the above types
|
||||||
listenerSetOrMap.add(listener);
|
listenerSetOrMap.add(listener);
|
||||||
|
|
||||||
@ -601,6 +638,11 @@ declare export class LexicalEditor {
|
|||||||
addListener(type: 'root', listener: RootListener): () => void;
|
addListener(type: 'root', listener: RootListener): () => void;
|
||||||
addListener(type: 'decorator', listener: DecoratorListener): () => void;
|
addListener(type: 'decorator', listener: DecoratorListener): () => void;
|
||||||
addListener(type: 'textcontent', listener: TextContentListener): () => void;
|
addListener(type: 'textcontent', listener: TextContentListener): () => void;
|
||||||
|
addListener(
|
||||||
|
type: 'mutation',
|
||||||
|
klass: Class<LexicalNode>,
|
||||||
|
listener: MutationListener,
|
||||||
|
): () => void;
|
||||||
addListener(
|
addListener(
|
||||||
type: 'command',
|
type: 'command',
|
||||||
listener: CommandListener,
|
listener: CommandListener,
|
||||||
|
@ -31,10 +31,10 @@ import {
|
|||||||
getActiveEditorState,
|
getActiveEditorState,
|
||||||
} from './LexicalUpdates';
|
} from './LexicalUpdates';
|
||||||
import {
|
import {
|
||||||
$generateKey,
|
|
||||||
$getCompositionKey,
|
$getCompositionKey,
|
||||||
$getNodeByKey,
|
$getNodeByKey,
|
||||||
$setCompositionKey,
|
$setCompositionKey,
|
||||||
|
$setNodeKey,
|
||||||
internalMarkNodeAsDirty,
|
internalMarkNodeAsDirty,
|
||||||
internalMarkSiblingsAsDirty,
|
internalMarkSiblingsAsDirty,
|
||||||
} from './LexicalUtils';
|
} from './LexicalUtils';
|
||||||
@ -131,8 +131,8 @@ export class LexicalNode {
|
|||||||
|
|
||||||
constructor(key?: NodeKey): void {
|
constructor(key?: NodeKey): void {
|
||||||
this.__type = this.constructor.getType();
|
this.__type = this.constructor.getType();
|
||||||
this.__key = key || $generateKey(this);
|
|
||||||
this.__parent = null;
|
this.__parent = null;
|
||||||
|
$setNodeKey(this, key);
|
||||||
|
|
||||||
// Ensure custom nodes implement required methods.
|
// Ensure custom nodes implement required methods.
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -11,6 +11,8 @@ import type {
|
|||||||
EditorConfig,
|
EditorConfig,
|
||||||
IntentionallyMarkedAsDirtyElement,
|
IntentionallyMarkedAsDirtyElement,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
|
MutatedNodes,
|
||||||
|
RegisteredNodes,
|
||||||
} from './LexicalEditor';
|
} from './LexicalEditor';
|
||||||
import type {NodeKey, NodeMap} from './LexicalNode';
|
import type {NodeKey, NodeMap} from './LexicalNode';
|
||||||
import type {RangeSelection} from './LexicalSelection';
|
import type {RangeSelection} from './LexicalSelection';
|
||||||
@ -40,6 +42,7 @@ import {
|
|||||||
getDOMTextNode,
|
getDOMTextNode,
|
||||||
getTextDirection,
|
getTextDirection,
|
||||||
isSelectionWithinEditor,
|
isSelectionWithinEditor,
|
||||||
|
setMutatedNode,
|
||||||
} from './LexicalUtils';
|
} from './LexicalUtils';
|
||||||
|
|
||||||
let subTreeTextContent = '';
|
let subTreeTextContent = '';
|
||||||
@ -47,6 +50,7 @@ let subTreeDirectionedTextContent = '';
|
|||||||
let editorTextContent = '';
|
let editorTextContent = '';
|
||||||
let activeEditorConfig: EditorConfig<{...}>;
|
let activeEditorConfig: EditorConfig<{...}>;
|
||||||
let activeEditor: LexicalEditor;
|
let activeEditor: LexicalEditor;
|
||||||
|
let activeEditorNodes: RegisteredNodes;
|
||||||
let treatAllNodesAsDirty: boolean = false;
|
let treatAllNodesAsDirty: boolean = false;
|
||||||
let activeEditorStateReadOnly: boolean = false;
|
let activeEditorStateReadOnly: boolean = false;
|
||||||
let activeTextDirection = null;
|
let activeTextDirection = null;
|
||||||
@ -55,6 +59,7 @@ let activeDirtyLeaves: Set<NodeKey>;
|
|||||||
let activePrevNodeMap: NodeMap;
|
let activePrevNodeMap: NodeMap;
|
||||||
let activeNextNodeMap: NodeMap;
|
let activeNextNodeMap: NodeMap;
|
||||||
let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
|
let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
|
||||||
|
let mutatedNodes: MutatedNodes;
|
||||||
|
|
||||||
function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
|
function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
|
||||||
const node = activePrevNodeMap.get(key);
|
const node = activePrevNodeMap.get(key);
|
||||||
@ -72,6 +77,9 @@ function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
|
|||||||
const children = node.__children;
|
const children = node.__children;
|
||||||
destroyChildren(children, 0, children.length - 1, null);
|
destroyChildren(children, 0, children.length - 1, null);
|
||||||
}
|
}
|
||||||
|
if (node !== undefined) {
|
||||||
|
setMutatedNode(mutatedNodes, activeEditorNodes, node, 'detached');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyChildren(
|
function destroyChildren(
|
||||||
@ -199,6 +207,7 @@ function createNode(
|
|||||||
// Freeze the node in DEV to prevent accidental mutations
|
// Freeze the node in DEV to prevent accidental mutations
|
||||||
Object.freeze(node);
|
Object.freeze(node);
|
||||||
}
|
}
|
||||||
|
setMutatedNode(mutatedNodes, activeEditorNodes, node, 'attached');
|
||||||
return dom;
|
return dom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +621,7 @@ function reconcileRoot(
|
|||||||
dirtyType: 0 | 1 | 2,
|
dirtyType: 0 | 1 | 2,
|
||||||
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
|
||||||
dirtyLeaves: Set<NodeKey>,
|
dirtyLeaves: Set<NodeKey>,
|
||||||
): void {
|
): MutatedNodes {
|
||||||
subTreeTextContent = '';
|
subTreeTextContent = '';
|
||||||
editorTextContent = '';
|
editorTextContent = '';
|
||||||
subTreeDirectionedTextContent = '';
|
subTreeDirectionedTextContent = '';
|
||||||
@ -622,12 +631,15 @@ function reconcileRoot(
|
|||||||
activeTextDirection = null;
|
activeTextDirection = null;
|
||||||
activeEditor = editor;
|
activeEditor = editor;
|
||||||
activeEditorConfig = editor._config;
|
activeEditorConfig = editor._config;
|
||||||
|
activeEditorNodes = editor._nodes;
|
||||||
activeDirtyElements = dirtyElements;
|
activeDirtyElements = dirtyElements;
|
||||||
activeDirtyLeaves = dirtyLeaves;
|
activeDirtyLeaves = dirtyLeaves;
|
||||||
activePrevNodeMap = prevEditorState._nodeMap;
|
activePrevNodeMap = prevEditorState._nodeMap;
|
||||||
activeNextNodeMap = nextEditorState._nodeMap;
|
activeNextNodeMap = nextEditorState._nodeMap;
|
||||||
activeEditorStateReadOnly = nextEditorState._readOnly;
|
activeEditorStateReadOnly = nextEditorState._readOnly;
|
||||||
activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
|
activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
|
||||||
|
const currentMutatedNodes = new Map();
|
||||||
|
mutatedNodes = currentMutatedNodes;
|
||||||
reconcileNode('root', null);
|
reconcileNode('root', null);
|
||||||
|
|
||||||
// We don't want a bunch of void checks throughout the scope
|
// We don't want a bunch of void checks throughout the scope
|
||||||
@ -637,6 +649,8 @@ function reconcileRoot(
|
|||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
activeEditor = undefined;
|
activeEditor = undefined;
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
|
activeEditorNodes = undefined;
|
||||||
|
// $FlowFixMe
|
||||||
activeDirtyElements = undefined;
|
activeDirtyElements = undefined;
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
activeDirtyLeaves = undefined;
|
activeDirtyLeaves = undefined;
|
||||||
@ -648,6 +662,10 @@ function reconcileRoot(
|
|||||||
activeEditorConfig = undefined;
|
activeEditorConfig = undefined;
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
activePrevKeyToDOMMap = undefined;
|
activePrevKeyToDOMMap = undefined;
|
||||||
|
// $FlowFixMe
|
||||||
|
mutatedNodes = undefined;
|
||||||
|
|
||||||
|
return currentMutatedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateEditorState(
|
export function updateEditorState(
|
||||||
@ -658,8 +676,9 @@ export function updateEditorState(
|
|||||||
pendingSelection: RangeSelection | null,
|
pendingSelection: RangeSelection | null,
|
||||||
needsUpdate: boolean,
|
needsUpdate: boolean,
|
||||||
editor: LexicalEditor,
|
editor: LexicalEditor,
|
||||||
): void {
|
): null | MutatedNodes {
|
||||||
const observer = editor._observer;
|
const observer = editor._observer;
|
||||||
|
let reconcileMutatedNodes = null;
|
||||||
|
|
||||||
if (needsUpdate && observer !== null) {
|
if (needsUpdate && observer !== null) {
|
||||||
const dirtyType = editor._dirtyType;
|
const dirtyType = editor._dirtyType;
|
||||||
@ -668,7 +687,7 @@ export function updateEditorState(
|
|||||||
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
try {
|
try {
|
||||||
reconcileRoot(
|
reconcileMutatedNodes = reconcileRoot(
|
||||||
currentEditorState,
|
currentEditorState,
|
||||||
pendingEditorState,
|
pendingEditorState,
|
||||||
editor,
|
editor,
|
||||||
@ -698,6 +717,8 @@ export function updateEditorState(
|
|||||||
domSelection,
|
domSelection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return reconcileMutatedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollIntoViewIfNeeded(node: Node, rootElement: ?HTMLElement): void {
|
function scrollIntoViewIfNeeded(node: Node, rootElement: ?HTMLElement): void {
|
||||||
|
@ -11,6 +11,7 @@ import type {
|
|||||||
CommandPayload,
|
CommandPayload,
|
||||||
EditorUpdateOptions,
|
EditorUpdateOptions,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
|
MutatedNodes,
|
||||||
Transform,
|
Transform,
|
||||||
} from './LexicalEditor';
|
} from './LexicalEditor';
|
||||||
import type {ParsedEditorState} from './LexicalEditorState';
|
import type {ParsedEditorState} from './LexicalEditorState';
|
||||||
@ -346,7 +347,7 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
|
|||||||
editor._updating = true;
|
editor._updating = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateEditorState(
|
const mutatedNodes = updateEditorState(
|
||||||
rootElement,
|
rootElement,
|
||||||
currentEditorState,
|
currentEditorState,
|
||||||
pendingEditorState,
|
pendingEditorState,
|
||||||
@ -355,6 +356,14 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
|
|||||||
needsUpdate,
|
needsUpdate,
|
||||||
editor,
|
editor,
|
||||||
);
|
);
|
||||||
|
if (mutatedNodes !== null) {
|
||||||
|
triggerMutationListeners(
|
||||||
|
editor,
|
||||||
|
currentEditorState,
|
||||||
|
pendingEditorState,
|
||||||
|
mutatedNodes,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Report errors
|
// Report errors
|
||||||
triggerListeners('error', editor, false, error);
|
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(
|
export function triggerListeners(
|
||||||
type: 'update' | 'error' | 'root' | 'decorator' | 'textcontent',
|
type: 'update' | 'error' | 'root' | 'decorator' | 'textcontent',
|
||||||
|
|
||||||
|
@ -10,7 +10,10 @@
|
|||||||
import type {
|
import type {
|
||||||
IntentionallyMarkedAsDirtyElement,
|
IntentionallyMarkedAsDirtyElement,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
|
MutatedNodes,
|
||||||
|
NodeMutation,
|
||||||
RegisteredNode,
|
RegisteredNode,
|
||||||
|
RegisteredNodes,
|
||||||
} from './LexicalEditor';
|
} from './LexicalEditor';
|
||||||
import type {EditorState} from './LexicalEditorState';
|
import type {EditorState} from './LexicalEditorState';
|
||||||
import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
|
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);
|
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();
|
errorOnReadOnly();
|
||||||
errorOnInfiniteTransforms();
|
errorOnInfiniteTransforms();
|
||||||
const editor = getActiveEditor();
|
const editor = getActiveEditor();
|
||||||
@ -172,7 +179,7 @@ export function $generateKey(node: LexicalNode): NodeKey {
|
|||||||
}
|
}
|
||||||
editor._cloneNotNeeded.add(key);
|
editor._cloneNotNeeded.add(key);
|
||||||
editor._dirtyType = HAS_DIRTY_NODES;
|
editor._dirtyType = HAS_DIRTY_NODES;
|
||||||
return key;
|
node.__key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalMarkParentElementsAsDirty(
|
function internalMarkParentElementsAsDirty(
|
||||||
@ -779,3 +786,22 @@ export function getCachedClassNameArray<Theme: {...}>(
|
|||||||
}
|
}
|
||||||
return classNames;
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(fn).toHaveBeenCalledWith('foobar');
|
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);
|
$getRoot().append(paragraph);
|
||||||
});
|
});
|
||||||
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
|
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,
|
EditorThemeClasses,
|
||||||
IntentionallyMarkedAsDirtyElement,
|
IntentionallyMarkedAsDirtyElement,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
|
NodeMutation,
|
||||||
} from './LexicalEditor';
|
} from './LexicalEditor';
|
||||||
export type {EditorState, ParsedEditorState} from './LexicalEditorState';
|
export type {EditorState, ParsedEditorState} from './LexicalEditorState';
|
||||||
export type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
|
export type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
|
||||||
|
@ -49,5 +49,20 @@
|
|||||||
"49": "append: attemtping to append self",
|
"49": "append: attemtping to append self",
|
||||||
"50": "Node %s and selection point do not match.",
|
"50": "Node %s and selection point do not match.",
|
||||||
"51": "setIndent is not implemented on ListItemNode",
|
"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