How to have unique node ids? #4462
Replies: 2 comments
-
I have a similar problem in my project where each node has a unique id, but it would not work correctly when copying and pasting. My solution was to override /*
In packages\slate-react\src\components\editable.tsx, it would be called on these events:
1. onDOMBeforeInput, when event.inputType === insertFromPaste
2. onDrop
3. onPaste
*/
editor.insertFragment = (fragment: Node[]) => {
fragment.forEach(fg => {
fg.children.forEach(node => {
if(node.type && node.type === 'my-custome-node') {
node.isPasted = true
}
})
})
insertFragment(fragment)
}
function App() {
const [editor] = useState(() => overrideEditor(withReact(createEditor())))
cpmst [value, setValue] = useState([])
//...
const onChange = (value) => {
value.forEach(p => {
(p.children || []).forEach(node => {
if(node.isPasted) {
// modify duplicate node ids
}
})
})
setValue({
...value
})
}
// ...
return <Slate editor={editor} value={value} onChange={onChange}>
<Editable renderElement={renderElement} renderLeaf={renderLeaf}/>
</Slate>
} |
Beta Was this translation helpful? Give feedback.
-
I'm not a huge fan of code-dumping but this should give you a general idea on how to archive something like this in a performant way by intercepting import { getMarks } from '@slite/slate-convert'
import { PluginEditor } from '@slite/slate-plugins'
import { produce } from 'immer'
import { isEqual, omit } from 'lodash'
import {
Descendant,
Editor,
Element,
Node,
NodeEntry,
Operation,
Path,
Text,
Transforms,
} from 'slate'
export type KeyToPathsTable = Record<string, Path>
export type NodeKeyEditor = PluginEditor & {
__nodeKeyEditor: true
}
const NODE_ID_COUNTER: WeakMap<Editor, number> = new WeakMap()
const DOCUMENT_KEYS: WeakMap<Editor, string> = new WeakMap()
const KEY_TO_PATHS_TABLES: WeakMap<Editor, KeyToPathsTable> = new WeakMap()
export const NodeKeyEditor = {
isNodeKeyEditor(v: unknown): v is NodeKeyEditor {
return KEY_TO_PATHS_TABLES.has(v as NodeKeyEditor)
},
keyToPathsTable: (editor: NodeKeyEditor): KeyToPathsTable => {
const keyToPathsTable = KEY_TO_PATHS_TABLES.get(editor)
if (!keyToPathsTable) {
throw new Error("editor isn't a NodeKeyEditor")
}
return keyToPathsTable
},
setKeyToPathsTable: (editor: NodeKeyEditor, table: KeyToPathsTable) => {
KEY_TO_PATHS_TABLES.set(editor, table)
},
setDocumentKey: (editor: NodeKeyEditor, key: string) => {
DOCUMENT_KEYS.set(editor, key)
const keyToPathsTable = NodeKeyEditor.keyToPathsTable(editor)
NodeKeyEditor.setKeyToPathsTable(editor, { ...keyToPathsTable, [key]: [] })
},
documentKey: (editor: NodeKeyEditor) => DOCUMENT_KEYS.get(editor),
nextKey: (
editor: NodeKeyEditor,
tmpMappings: Record<string, Path> = {}
): string => {
const idCounter = (NODE_ID_COUNTER.get(editor) ?? 0) + 1
NODE_ID_COUNTER.set(editor, idCounter)
const key = `_${idCounter.toString(36)}`
// If blocks have manually assigned keys, we could run into
// conflicts. Get the next key if we already have a path ref
// under the current.
if (NodeKeyEditor.path(editor, key) || key in tmpMappings) {
return NodeKeyEditor.nextKey(editor, tmpMappings)
}
return key
},
path: (editor: NodeKeyEditor, key: string): Path =>
NodeKeyEditor.keyToPathsTable(editor)[key],
unref: (editor: NodeKeyEditor, key: string) => {
const keyToPathsTable = NodeKeyEditor.keyToPathsTable(editor)
NodeKeyEditor.setKeyToPathsTable(editor, omit(keyToPathsTable, key))
},
}
export function withNodeKeys<T extends PluginEditor>(
editor: T
): T & NodeKeyEditor {
const e = editor as T & NodeKeyEditor
e.__nodeKeyEditor = true
KEY_TO_PATHS_TABLES.set(editor, {})
NODE_ID_COUNTER.set(editor, 0)
NodeKeyEditor.setDocumentKey(e, NodeKeyEditor.nextKey(e))
const { setChildren } = e
e.setChildren = (children) => {
const keyToPathRefsTable: KeyToPathsTable = {}
const childrenWithKeys = produce(children, (draft) => {
for (const [node, path] of Node.descendants({ children: draft })) {
let key = node.key as string
if (typeof key !== 'string' || key in keyToPathRefsTable) {
key = NodeKeyEditor.nextKey(e, keyToPathRefsTable)
}
keyToPathRefsTable[key] = path
node.key = key
}
})
NodeKeyEditor.setKeyToPathsTable(e, keyToPathRefsTable)
NodeKeyEditor.setDocumentKey(e, NodeKeyEditor.nextKey(e))
setChildren(childrenWithKeys)
}
const { construct } = e
e.construct = () => {
const { normalizeNode, apply } = e
e.apply = (op: Operation) => {
const toRef: Record<string, Path> = {}
const keyToPathsTable = NodeKeyEditor.keyToPathsTable(e)
// eslint-disable-next-line default-case
switch (op.type) {
case 'merge_node': {
if ('key' in op.properties && op.properties.key !== undefined) {
NodeKeyEditor.unref(e, op.properties.key)
}
break
}
case 'insert_node': {
const { path } = op
op = produce(op, (draft) => {
if (!Editor.isEditor(draft.node)) {
let { key } = draft.node
if (typeof key !== 'string' || key in keyToPathsTable) {
key = NodeKeyEditor.nextKey(e)
}
draft.node.key = key
toRef[key] = path
}
for (const [descendant, relPath] of Node.descendants(draft.node)) {
let { key: childKey } = descendant
if (
typeof childKey !== 'string' ||
childKey in keyToPathsTable ||
childKey in toRef
) {
childKey = NodeKeyEditor.nextKey(e, toRef)
}
descendant.key = childKey
toRef[childKey] = [...path, ...relPath]
}
})
break
}
case 'split_node': {
// Will always be Partial<Descendant> since you can't split the editor
const properties = op.properties as Partial<Descendant>
let { key } = properties
if (typeof key !== 'string' || key in keyToPathsTable) {
key = NodeKeyEditor.nextKey(e)
}
if (properties.key !== key) {
op = produce(op, (draft) => {
;(draft.properties as Partial<Descendant>).key = key
})
}
toRef[key] = Path.next(op.path)
break
}
case 'remove_node': {
if ('key' in op.node && op.node.key !== undefined) {
NodeKeyEditor.unref(e, op.node.key as string)
for (const [descendant] of Node.descendants(op.node)) {
NodeKeyEditor.unref(e, descendant.key as string)
}
}
break
}
case 'set_node': {
// Will always be Partial<Descendant> since you can't set editor
if (
(op.newProperties as Partial<Descendant>).key !==
(op.properties as Partial<Descendant>).key
) {
let { key } = op.newProperties as Partial<Descendant>
const { key: prevKey } = op.newProperties as Partial<Descendant>
if (typeof key !== 'string' || key in keyToPathsTable) {
key = NodeKeyEditor.nextKey(e)
}
op = produce(op, (draft) => {
;(draft.properties as Partial<Descendant>).key = key
})
if (prevKey) {
NodeKeyEditor.unref(e, prevKey)
}
toRef[key] = op.path
}
break
}
}
apply(op)
const transformed = Object.fromEntries(
Object.entries(NodeKeyEditor.keyToPathsTable(e))
.map(([key, path]) => [
key,
Path.transform(path, op, { affinity: 'backward' }),
])
.filter(([, path]) => path)
)
NodeKeyEditor.setKeyToPathsTable(e, { ...transformed, ...toRef })
}
e.normalizeNode = (entry: NodeEntry) => {
// -- Normalize node keys --
const [node, path] = entry
if (Editor.isEditor(node)) {
return normalizeNode(entry)
}
const keyToPathsTable = NodeKeyEditor.keyToPathsTable(e)
const expectedPath = keyToPathsTable[node.key as string]
if (!expectedPath || !Path.equals(path, expectedPath)) {
let key = node.key as string
if (typeof key !== 'string') {
key = NodeKeyEditor.nextKey(e)
}
// Check if the expected node exists, if so a new node with an existing key
// was inserted => assign it a new key.
if (expectedPath?.length > 0 && Node.has(editor, expectedPath)) {
const originKey = (Node.get(editor, expectedPath) as Descendant)
.key as string
if (originKey === key) {
key = NodeKeyEditor.nextKey(e)
}
}
NodeKeyEditor.setKeyToPathsTable(e, { ...keyToPathsTable, [key]: path })
if (key !== node.key) {
// eslint-disable-next-line no-console
console.warn(
`Encountered editor value with duplicate node key or invalid key path mapping: ${JSON.stringify(
node
)}`
)
Transforms.setNodes(editor, { key }, { at: path })
return
}
}
// -- Re-Implement text merging to ignore keys --
if (Element.isElement(node)) {
// Since we'll be applying operations while iterating, keep track of an
// index that accounts for any added/removed nodes.
let n = 0
for (let i = 0; i < node.children.length; i++, n++) {
const child = node.children[i] as Descendant
const prev = node.children[i - 1] as Descendant
if (prev && Text.isText(prev) && Text.isText(child)) {
if (isEqual(getMarks(prev), getMarks(child))) {
Transforms.mergeNodes(editor, { at: [...path, n], voids: true })
n--
}
}
}
if (n !== node.children.length) {
return
}
}
return normalizeNode(entry)
}
construct()
}
return e
} |
Beta Was this translation helpful? Give feedback.
-
I'm writing an app where, essentially, I need to know if two arbitrary nodes are equal to each other. I think the best way to solve this is to have a unique, stable identifier for each node. I know Slate used to have a
key
for each node and a function that could be overridden for generating this key, which I think would have worked perfectly for my use case. Unfortunately, I don't think this capability exists anymore.My question is: how would I go about making sure that each node that is created has a unique, stable identifier? I think this is kind of difficult to do because in certain scenarios, Slate creates nodes automatically, and I'm not able to override these node creations (e.g. when you're copying and pasting text that has newlines in it). In that case, multiple blocks get created with the same id, which is not what I want.
One way that could potentially work is by adding a normalization rule that ensures that each node has a unique id. However, this seems like it could be inefficient, and I feel like there would still be ways for duplicate node ids to slip in undetected, since (to my knowledge) normalization is only run on the current node/subtree being edited.
What would be the best way to have a unique, stable id for each node?
Beta Was this translation helpful? Give feedback.
All reactions