background

Documents are hot these days, so is the boss. I was also interested, so I went into the pit to learn and practice. A year has passed in the blink of an eye, and the project has achieved initial results, but the difficulties and challenges are becoming more and more intractable. So in-depth study to adapt the source code, to prepare for the back to rewrite the source code.

Screenshots of the results of our project, town house:

At the end of the article there is demo source, welcome to comment exchange.

The data structure

Since it is to learn SLATE source code also do not want to innovate a data structure, along the way of predecessors to go first. Considering that later large documents need to be loaded with Windows, I think a SINGLE JSON document is too crude, and may be converted into multiple arrays to form a single document.

Day one, the simplest demo

First, write a simple P tag that shows how we can take over user text input from the browser.

[{type: 'p', the children: [{text: 'big orange'}]}]Copy the code

Results the following

If I want two big oranges in a row, I need something like this:

[{type:'p',children:[{text:' children '}]}]Copy the code

We need an operation insertText:

Export function insertText(root, text, path) {var node = getNodeByPath(root, path); if (text) { node.text = node.text + text; } } function getNodeByPath(root, path) { // return root[0].children[0] var node = root; console.log(window.root === root) for (var i = 0; i < path.length; i++) { const p = path[i] node = node[p] || node.children[p]; } return node; } const root = [{ type: 'p', children: [{ text: 'big orange'}}]] insertText (root, 'big orange, [0]) to the console. The log (JSON. Stringify (root)) / / / {" type ":" p ", "children" : [{" text ", "big orange orange"}]}]Copy the code

The simplest logic for an editor is ok.

View to show

So I’m going to react

Create a project

NPM install -g create-react-app create-react-app day001 CD day001 NPM startCopy the code

Write the following code in app.js

import './App.css';

import {useEffect} from 'react'

import {getString, insertText} from './insertText'

window.root =[{ type: 'p'.children: [{ text: ' '}}]]function App() {

    // const [root, setRoot] = useState(initRoot)

    useEffect(() = > {

        const input = document.getElementById('editor');

        input.addEventListener('beforeinput', updateValue);

        function updateValue(e) {

            e.preventDefault()

            e.stopPropagation()

            insertText(window.root, e.data, [0.0])

            console.log(e.data, window.root)

            input.textContent = getString(window.root)

        }

    }, [])

    return (

    <div className="App">This is a demo editor<div id='editor' contentEditable onInput={(e)= >{

            e.preventDefault()

            e.stopPropagation()

            console.log(e)

            return

        }}>
        </div>

    </div>

    );

}



export default App;


Copy the code

Effect:

The next day, control the cursor in your browser

After taking over the input text, how can we enter text at the rich text cursor position?

On day one, we’ve implemented listening for user input and selectively selecting what to input. While the principle it uses is valuable, the editor is a bit low in that no matter where the user enters in the editor, the content can only be appended to the end of the text. As a rich text editor this is unforgivable.

So now, let’s fix this problem.

First, we know how to get the cursor position and the corresponding text position. Here we use the window.getSelection() API to get the DOM where the cursor is and the position of the cursor in the DOM text.

The insertText code is modified as follows

export function insertText(root, text, path) {
    const domSelection = window.getSelection()
    console.log('domSelection', domSelection, domSelection.isCollapsed, domSelection.anchorNode, domSelection.anchorOffset, JSON.stringify(domSelection))
    // Get element of the specified path
    var node = getNodeByPath(root, path);
    if (domSelection.isCollapsed) {
        if (text) {
            const before = node.text.slice(0, domSelection.anchorOffset)
            const after = node.text.slice(domSelection.anchorOffset)
            node.text = before + text + after
        }
    } else {
        // TODO if the cursor selects a range
    }
    // console.log(root[0].children[0] === node, root[0].children[0], node)

}

Copy the code

We have inserted text at the cursor position, but the cursor is not in the right position after typing, so we need to change the cursor.

A brief introduction to the setBaseAndExtent method

 // dom refers to the dom node to be selected, and offset refers to the position of the text inside the DOM node
 window.getSelection().setBaseAndExtent(
        dom, offset, dom2, offset2)
Copy the code

Rewrite our app.js file, mainly modifying the two useEffect methods and rendering the text to state to change.

import './App.css';
import { useState, useEffect } from 'react'
import { getString, insertText } from './insertText'
window.root = [{ type: 'p'.children: [{ text: ' '}}]]function App() {
  // Record what we typed
  const [txt, setTxt] = useState(' ')
  // Offset of the cursor
  const [txtOffset, setTxtOffset] = useState(0)
  // Is responsible for registering to listen for beforeInput events and blocking default events. Modify window.root in listener, and update TXT and txtO in it, and finally clear cursor, prevent TXT update caused cursor flash.
  useEffect(() = > {
    const input = document.getElementById('editor');

    input.addEventListener('beforeinput', updateValue);
    function updateValue(e) {
      e.preventDefault()
      e.stopPropagation()
      insertText(window.root, e.data, [0.0])
      // console.log(e.data, window.root)
      const getText = getString(window.root)
      const { anchorOffset } = window.getSelection()
      setTxtOffset(anchorOffset + e.data.length)
      setTxt(getText)
      window.getSelection().removeAllRanges()
    }
    return () = > {
      input.removeEventListener('beforeinput', updateValue); }}, [])// Listen on txtOffset and update the cursor position with setBaseAndExtent. SetTimeout is used because the cursor position will be changed after the page is rendered
  useEffect(() = > {
    const { anchorNode } = window.getSelection()
    setTimeout(() = > {
      if(! anchorNode) {return
      }
      let dom = anchorNode
      if (dom.childNodes && dom.childNodes[0]) {
        dom = dom.childNodes[0]}window.getSelection().setBaseAndExtent(
        dom, txtOffset, dom, txtOffset)
    })
  }, [txtOffset])


  return (
    <div className="App">This is a demo editor<div id='editor' contentEditable onInput={(e)= > {
        e.preventDefault()
        e.stopPropagation()
        console.log(e)
        return
      }}>

        {txt}
      </div>
    </div>
  );
}

export default App;

Copy the code

At this point, our editor can normally enter English and numbers. But what about the Chinese problem?

Subsequent updates ~

Source: github.com/PangYiMing/…