-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[lexical] Feature: registerMutationListener should initialize its existing nodes #6357
Changes from all commits
a560f2f
c73a70a
7e2d68b
c27ba0f
7f86a8b
b5d9f54
1bb7e1f
63723fa
a8bfbfb
9fa7bb8
314dfb9
9b19859
3d932c4
4b720a1
82913db
47f9438
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,26 +109,32 @@ function CodeActionMenuContainer({ | |
}; | ||
}, [shouldListenMouseMove, debouncedOnMouseMove]); | ||
|
||
editor.registerMutationListener(CodeNode, (mutations) => { | ||
editor.getEditorState().read(() => { | ||
for (const [key, type] of mutations) { | ||
switch (type) { | ||
case 'created': | ||
codeSetRef.current.add(key); | ||
setShouldListenMouseMove(codeSetRef.current.size > 0); | ||
break; | ||
|
||
case 'destroyed': | ||
codeSetRef.current.delete(key); | ||
setShouldListenMouseMove(codeSetRef.current.size > 0); | ||
break; | ||
|
||
default: | ||
break; | ||
} | ||
} | ||
}); | ||
}); | ||
useEffect(() => { | ||
return editor.registerMutationListener( | ||
CodeNode, | ||
(mutations) => { | ||
editor.getEditorState().read(() => { | ||
for (const [key, type] of mutations) { | ||
switch (type) { | ||
case 'created': | ||
codeSetRef.current.add(key); | ||
break; | ||
|
||
case 'destroyed': | ||
codeSetRef.current.delete(key); | ||
break; | ||
|
||
default: | ||
break; | ||
} | ||
} | ||
}); | ||
setShouldListenMouseMove(codeSetRef.current.size > 0); | ||
}, | ||
{skipInitialization: false}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CodeActionMenuPlugin did not previously handle initialization correctly |
||
); | ||
}, [editor]); | ||
|
||
const normalizedLang = normalizeCodeLang(lang); | ||
const codeFriendlyName = getLanguageFriendlyName(lang); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -183,17 +183,21 @@ function TableActionMenu({ | |
); | ||
|
||
useEffect(() => { | ||
return editor.registerMutationListener(TableCellNode, (nodeMutations) => { | ||
const nodeUpdated = | ||
nodeMutations.get(tableCellNode.getKey()) === 'updated'; | ||
|
||
if (nodeUpdated) { | ||
editor.getEditorState().read(() => { | ||
updateTableCellNode(tableCellNode.getLatest()); | ||
}); | ||
setBackgroundColor(currentCellBackgroundColor(editor) || ''); | ||
} | ||
}); | ||
return editor.registerMutationListener( | ||
TableCellNode, | ||
(nodeMutations) => { | ||
const nodeUpdated = | ||
nodeMutations.get(tableCellNode.getKey()) === 'updated'; | ||
|
||
if (nodeUpdated) { | ||
editor.getEditorState().read(() => { | ||
updateTableCellNode(tableCellNode.getLatest()); | ||
}); | ||
setBackgroundColor(currentCellBackgroundColor(editor) || ''); | ||
} | ||
}, | ||
{skipInitialization: true}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TableActionMenuPlugin did handle initialization correctly (it only listens for updates) |
||
); | ||
}, [editor, tableCellNode]); | ||
|
||
useEffect(() => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,24 +105,23 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({ | |
}, []); | ||
|
||
const checkIfLinkNodeIsEmbeddable = useCallback( | ||
(key: NodeKey) => { | ||
editor.getEditorState().read(async function () { | ||
async (key: NodeKey) => { | ||
const url = editor.getEditorState().read(function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code was very sketchy previously, and only worked at all because it used |
||
const linkNode = $getNodeByKey(key); | ||
if ($isLinkNode(linkNode)) { | ||
for (let i = 0; i < embedConfigs.length; i++) { | ||
const embedConfig = embedConfigs[i]; | ||
|
||
const urlMatch = await Promise.resolve( | ||
embedConfig.parseUrl(linkNode.__url), | ||
); | ||
|
||
if (urlMatch != null) { | ||
setActiveEmbedConfig(embedConfig); | ||
setNodeKey(linkNode.getKey()); | ||
} | ||
} | ||
return linkNode.getURL(); | ||
} | ||
}); | ||
if (url === undefined) { | ||
return; | ||
} | ||
for (const embedConfig of embedConfigs) { | ||
const urlMatch = await Promise.resolve(embedConfig.parseUrl(url)); | ||
if (urlMatch != null) { | ||
setActiveEmbedConfig(embedConfig); | ||
setNodeKey(key); | ||
} | ||
} | ||
}, | ||
[editor, embedConfigs], | ||
); | ||
|
@@ -146,7 +145,9 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({ | |
}; | ||
return mergeRegister( | ||
...[LinkNode, AutoLinkNode].map((Klass) => | ||
editor.registerMutationListener(Klass, (...args) => listener(...args)), | ||
editor.registerMutationListener(Klass, (...args) => listener(...args), { | ||
skipInitialization: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation is not compatible with |
||
}), | ||
), | ||
); | ||
}, [checkIfLinkNodeIsEmbeddable, editor, embedConfigs, nodeKey, reset]); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -234,6 +234,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element { | |
setTableOfContents(currentTableOfContents); | ||
}); | ||
}, | ||
// Initialization is handled separately | ||
{skipInitialization: true}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LexicalTableOfContentsPlugin already had an efficient and correct initialization (though this wouldn't break it) |
||
); | ||
|
||
// Listen to text node mutation updates | ||
|
@@ -258,6 +260,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element { | |
} | ||
}); | ||
}, | ||
// Initialization is handled separately | ||
{skipInitialization: true}, | ||
); | ||
|
||
return () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,6 @@ import { | |
$createParagraphNode, | ||
$getNodeByKey, | ||
$isTextNode, | ||
$nodesOfType, | ||
COMMAND_PRIORITY_EDITOR, | ||
} from 'lexical'; | ||
import {useEffect} from 'react'; | ||
|
@@ -129,17 +128,6 @@ export function TablePlugin({ | |
} | ||
}; | ||
|
||
// Plugins might be loaded _after_ initial content is set, hence existing table nodes | ||
// won't be initialized from mutation[create] listener. Instead doing it here, | ||
editor.getEditorState().read(() => { | ||
const tableNodes = $nodesOfType(TableNode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was not correct because |
||
for (const tableNode of tableNodes) { | ||
if ($isTableNode(tableNode)) { | ||
initializeTableNode(tableNode); | ||
} | ||
} | ||
}); | ||
|
||
const unregisterMutationListener = editor.registerMutationListener( | ||
TableNode, | ||
(nodeMutations) => { | ||
|
@@ -161,6 +149,7 @@ export function TablePlugin({ | |
} | ||
} | ||
}, | ||
{skipInitialization: false}, | ||
); | ||
|
||
return () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,20 +30,20 @@ This can be a simple, efficient way to handle some use cases, since it's not nec | |
|
||
## 2. Directly Attach Handlers | ||
|
||
In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that you're handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners). | ||
In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners). | ||
|
||
```js | ||
const registeredElements: WeakSet<HTMLElement> = new WeakSet(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just fixing the correctness and formatting of this relevant example, the state needs to be outside of the listener implementation! |
||
const removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => { | ||
const registeredElements: WeakSet<HTMLElement> = new WeakSet(); | ||
editor.getEditorState().read(() => { | ||
for (const [key, mutation] of mutations) { | ||
const element: null | HTMLElement = editor.getElementByKey(key); | ||
if ( | ||
// Updated might be a move, so that might mean a new DOM element | ||
// is created. In this case, we need to add and event listener too. | ||
(mutation === 'created' || mutation === 'updated') && | ||
element !== null && | ||
!registeredElements.has(element) | ||
// Updated might be a move, so that might mean a new DOM element | ||
// is created. In this case, we need to add and event listener too. | ||
(mutation === 'created' || mutation === 'updated') && | ||
element !== null && | ||
!registeredElements.has(element) | ||
) { | ||
registeredElements.add(element); | ||
element.addEventListener('click', (event: Event) => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CodeHighlighter did not previously handle initialization correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice catch