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/…