Slate document editor-Node node and Path path mapping
We talked about it beforeslate
inDecorator
Decorator implementation, the decorator can be conveniently processed for us when the editor renders and schedulesrange
Rendering, which is very useful in implementing search and replacement, code highlighting and other scenarios. So in this article, let’s talk aboutNode
node withPath
Path mapping, hereNode
Refers to the rendered node object,Path
Then the node object is currentlyJSON
The path in , that is, the focus of this article is how to determine the position of the rendered node in the document data definition.
- Online editing:/DocEditor
- Open source address:/WindRunnerMax/DocEditor
aboutslate
Related articles about the Document Editor project:
- Building a document editor based on Slate
- Slate document editor-WrapNode data structure and operation transformation
- Slate document editor-TS type expansion and node type checking
- Slate Document Editor-Decorator Decorator Rendering Scheduling
- Slate document editor-Node node and Path path mapping
Rendering and commands
existslate
in the documentation of03-defining-custom-elements
section, we can see we can seeslate
inElement
Nodes can be customized for rendering, and the rendering logic requires us toprops
ofelement
object to determine the type, if the type iscode
If so, then we need to render our predefinedCodeElement
component, otherwise renderDefaultElement
Components, heretype
It's our defaultinit
Data structure value is the formal convention of data structure.
// /walkthroughs/03-defining-custom-elements
const App = () => {
const [editor] = useState(() => withReact(createEditor()))
// Define a rendering function based on the element passed to `props`. We use
// `useCallback` here to memoize the function for subsequent renders.
const renderElement = useCallback(props => {
switch () {
case 'code':
return <CodeElement {...props} />
default:
return <DefaultElement {...props} />
}
}, [])
return (
<Slate editor={editor} initialValue={initialValue}>
<Editable
// Pass in the `renderElement` function.
renderElement={renderElement}
/>
</Slate>
)
}
So there will naturally be no problem with the rendering here. Our editor must not only render content, but also execute commands to change the document structure/content. It is also very important. Then in05-executing-commands
In the middle section, we can see that the switching between bold text content and code blocks is executed respectively.addMark/removeMark
as well asfunction to execute.
// /walkthroughs/05-executing-commands
toggleBoldMark(editor) {
const isActive = (editor)
if (isActive) {
(editor, 'bold')
} else {
(editor, 'bold', true)
}
}
toggleCodeBlock(editor) {
const isActive = (editor)
(
editor,
{ type: isActive ? null : 'code' },
{ match: n => (editor, n) }
)
}
path mapping
There seems to be no problem in the above example. It seems that we have completed the basic node rendering and change execution of the editor. However, we may overlook a question here, why when we execute the commandslate
It's a very interesting question to know which node we want to operate on. If you run the above example, you can find that our direct execution of the above operation is very dependent on the position of the cursor. This is because when the default parameters are set, the selection position is taken to perform the change operation. This is naturally no problem for ordinary node rendering, but when we want to implement more complex modules or interactions, such as table modules and asynchronous upload of images, this may not be enough to allow us to complete these functions.
Of course, our document editor is not a particularly simple scenario. So if we need to implement complex operations of the editor in depth, it is obviously not realistic to rely entirely on the selection to perform operations. For example, we want to insert a blank line under the code block element. Since the constituency must be inText
On the node, we cannot directly operate the selection toNode
On the node, this implementation cannot be accomplished directly by relying on the constituency. It is also not easy to know the current position of the table in the cell, because the rendering scheduling at this time is implemented by the framework and we cannot obtain it directly.parent
data object. so often usedslate
All my classmates know that whether it isRenderElementProps
stillRenderLeafProps
When rendering, exceptattributes
as well aschildren
Other than waiting for data, there is noPath
Data transfer.
export interface RenderElementProps {
children: any;
element: Element;
attributes: {
// ...
};
}
export interface RenderLeafProps {
children: any;
leaf: Text;
text: Text;
attributes: {
// ...
};
}
This problem actually occurs not only in rich text editors, but also in front-end editing scenarios, such as low-code editors. The commonality is that we usually use a plug-in form to implement the editor, so the nodes rendered at this time are not components we write directly, but the content that is scheduled for rendering by the core layer and the plug-in. A single defined component will be rendered.N
times, then if we need to operate the data of the component, we need to know which position of the data object is to be updated, that is, how to know where I am in the data object at this time in the rendered component. It is true that for each rendered object it is definedid
is a feasible solution, but this requires iterating the entire object to find the location. Our implementation here is more efficient.
Then when we operate on dataPath
is very important. In daily interaction processing, we useIt can meet most functions. However, in many cases, simply use
selection
to process the target to be operated onPath
It's a bit stretched. Then in the passed data structure at this time we can see the same asPath
The most relevant data iselement/text
is worth it, then at this time we can more easily remember that inReactEditor
exist infindPath
method that allows us to passNode
to find the correspondingPath
。
// /ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/plugin/#L90
findPath(editor: ReactEditor, node: Node): Path {
const path: Path = []
let child = node
while (true) {
const parent = NODE_TO_PARENT.get(child)
if (parent == null) {
if ((child)) return path
else break
}
const i = NODE_TO_INDEX.get(child)
if (i == null) break
(i)
child = parent
}
}
The code is simply compressed, and the implementation here is through twoWeakMap
Very cleverly allows us to obtain the node'sPath
. So here we need to think about a question, why don’t we directlyRenderProps
directlyPath
Passed to the rendering method, it has to be searched again every time and wastes part of the performance. In fact, if we just render the document data, then there will be no problem. However, we usually need to edit the document, and problems will occur at this time. For example, suppose we are[10]
There is a table at the location, and at this time we are at[6]
added to the location1
blank row, then our table at this timePath
It should be[11]
However, since we are not actually editing the table-related content, we should not refresh the table-related content itself. Naturally,Props
It will not change. If we get the value directly at this time, we will get[10]
instead of[11]
。
Then the same thing, even if we useWeakMap
RecordNode
andPath
corresponding relationship, even if the tableNode
There is no actual change, and we cannot easily iterate through all nodes to update theirPath
. Therefore, we can use this method to search when needed. Then a new question comes again. Since we mentioned before that the content related to the table will not be updated, how should we update it?index
As for the value ofimmutable
The model is consistent, then we can update all affected index values at this time.
So how to avoid updates of other nodes? Obviously we can based onkey
To control this behavior, assign uniqueid
That’s it. In addition, it can be seen here thatuseChildren
is defined asHooks
, then the number of calls will definitely not be low, and here each componentrender
will existfindPath
Call, so there is no need to worry too much about the performance of this method, because the number of iterations here is determined by our level. Usually we do not have too many levels of nesting, so the performance is still controllable. .
// /ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/hooks/#L90
const path = (editor, node)
const children = []
for (let i = 0; i < ; i++) {
const p = (i)
const n = [i] as Descendant
const key = (editor, n)
// ...
if ((n)) {
(
< key={`provider-${}`} value={!!sel}>
<ElementComponent />
</>
)
} else {
(<TextComponent />)
}
NODE_TO_INDEX.set(n, i)
NODE_TO_PARENT.set(n, node)
}
We can also use this concept to process tables. When we need to implement complex interactions with table nodes, we can find it difficult to determine the location of the rendering node.[RowIndex, ColIndex]
, that is, the position of the current cell in the table. We need this information to implement functions such as cell selection and resizing. useCan be used based on
Node
Get the latestPath
, but when there are many levels of data nesting, such as tables nested within tables, there are many "unnecessary" iterations. In fact, two levels can meet the needs, but usingwill continue to iterate until
Editor Node
, which occurs in frequently triggered operations such asResize
may cause some performance issues.
And if we use this concept, we can also achieve twoWeakMap
, at the top node that isTable
The mapping relationship is established when the node is rendered, and it can be completely iterated at this time.Tr + Cell
ofelement
object inimmutable
With the support of , we can get the index value of the current cell. Of course in the later stagesslate
Hit these twoWeakMap
It has been exported, we don’t need to establish the mapping relationship ourselves, we just need to take it out.
// /ianstormtaylor/slate/pull/5657
export const Table: FC = () => {
useMemo(() => {
const table = ;
((tr, index) => {
NODE_TO_PARENT.set(tr, table);
NODE_TO_INDEX.set(tr, index);
&&
((cell, index) => {
NODE_TO_PARENT.set(cell, tr);
NODE_TO_INDEX.set(cell, index);
});
});
}, []);
}
export const Cell: FC = () => {
const parent = NODE_TO_PARENT.get();
(
"RowIndex - CellIndex",
NODE_TO_INDEX.get(parent!),
NODE_TO_INDEX.get()
);
}
But to get it this wayNode
andPath
There is no problem in obtaining the position through the mapping of the node. The efficient search scheme makes us have to rely on rendering to know the latest position of the node. That is to say, after we update the node object, if we call it immediatelyfindPath
The method is unable to get the latestPath
, because the rendering behavior at this time is asynchronous. Then if necessary, you must iterate the entire data object to obtainPath
, of course I think there is no need to iterate the entire object here. When usingTransforms
After changing the content we should not get the path value immediately but wait untilReact
Complete rendering before proceeding to the next step. This way we can perform related operations in sequence, sinceslate
There are no additional asynchronous operations in the<Editable />
ofuseEffect
Determines when the current rendering is complete.
export const WithContext: FC<{ editor: EditorKit }> = props => {
const { editor, children } = props;
const isNeedPaint = useRef(true);
// Ensure that it will be re-rendered every time Apply is triggered
// /ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/components/#L29
useSlate();
useEffect(() => {
const onContentChange = () => {
= true;
};
(EDITOR_EVENT.CONTENT_CHANGE, onContentChange, 1);
return () => {
(EDITOR_EVENT.CONTENT_CHANGE, onContentChange);
};
}, [editor]);
useEffect(() => {
if () {
().then(() => {
// /ianstormtaylor/slate/issues/5697
(EDITOR_EVENT.PAINT, {});
});
}
= false;
});
return children as ;
};
at last
Here we mainly discussNode
node withPath
Path mapping, that is, how to determine the position of the rendered node in the document data definition, this isslate
Important expressions when implementing data changes, especially in complex operations that cannot be achieved using selections alone, and also analyzedslate
Source code to explore the implementation of related issues. So in the following articles, we will continue the table mentioned currently but search for cell positions to talk about the design and interaction of the table module.
Daily question
- /WindRunnerMax/EveryDay
refer to
- /
- /WindRunnerMax/DocEditor
- /ianstormtaylor/slate/blob/25be3b/