Location>code7788 >text

Slate document editor-Node node and Path path mapping

Popularity:364 ℃/2025-01-20 10:32:44

Slate document editor-Node node and Path path mapping

We talked about it beforeslateinDecoratorDecorator implementation, the decorator can be conveniently processed for us when the editor renders and schedulesrangeRendering, which is very useful in implementing search and replacement, code highlighting and other scenarios. So in this article, let’s talk aboutNodenode withPathPath mapping, hereNodeRefers to the rendered node object,PathThen the node object is currentlyJSONThe 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

aboutslateRelated 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

existslatein the documentation of03-defining-custom-elementssection, we can see we can seeslateinElementNodes can be customized for rendering, and the rendering logic requires us topropsofelementobject to determine the type, if the type iscodeIf so, then we need to render our predefinedCodeElementcomponent, otherwise renderDefaultElementComponents, heretypeIt's our defaultinitData 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-commandsIn the middle section, we can see that the switching between bold text content and code blocks is executed respectively.addMark/removeMarkas 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 commandslateIt'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 inTextOn the node, we cannot directly operate the selection toNodeOn 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.parentdata object. so often usedslateAll my classmates know that whether it isRenderElementPropsstillRenderLeafPropsWhen rendering, exceptattributesas well aschildrenOther than waiting for data, there is noPathData 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.Ntimes, 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 definedidis 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 dataPathis very important. In daily interaction processing, we useIt can meet most functions. However, in many cases, simply useselectionto process the target to be operated onPathIt's a bit stretched. Then in the passed data structure at this time we can see the same asPathThe most relevant data iselement/textis worth it, then at this time we can more easily remember that inReactEditorexist infindPathmethod that allows us to passNodeto 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 twoWeakMapVery cleverly allows us to obtain the node'sPath. So here we need to think about a question, why don’t we directlyRenderPropsdirectlyPathPassed 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 location1blank row, then our table at this timePathIt should be[11]However, since we are not actually editing the table-related content, we should not refresh the table-related content itself. Naturally,PropsIt 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 useWeakMapRecordNodeandPathcorresponding relationship, even if the tableNodeThere 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?indexAs for the value ofimmutableThe 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 onkeyTo control this behavior, assign uniqueidThat’s it. In addition, it can be seen here thatuseChildrenis defined asHooks, then the number of calls will definitely not be low, and here each componentrenderwill existfindPathCall, 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 onNodeGet 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 untilEditor Node, which occurs in frequently triggered operations such asResizemay cause some performance issues.

And if we use this concept, we can also achieve twoWeakMap, at the top node that isTableThe mapping relationship is established when the node is rendered, and it can be completely iterated at this time.Tr + Cellofelementobject inimmutableWith the support of , we can get the index value of the current cell. Of course in the later stagesslateHit these twoWeakMapIt 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 wayNodeandPathThere 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 immediatelyfindPathThe 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 usingTransformsAfter changing the content we should not get the path value immediately but wait untilReactComplete rendering before proceeding to the next step. This way we can perform related operations in sequence, sinceslateThere are no additional asynchronous operations in the<Editable />ofuseEffectDetermines 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 discussNodenode withPathPath mapping, that is, how to determine the position of the rendered node in the document data definition, this isslateImportant expressions when implementing data changes, especially in complex operations that cannot be achieved using selections alone, and also analyzedslateSource 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/