最近文档很火,老板也要。我也很感兴趣,于是入坑学习实践了一番。一眨眼就是一年过去了,项目初见成效,但是发现困难和挑战也越来越棘手。于是深入研究改编了一下源码,为后面重写源码做准备。
我们的项目的成果截图,镇宅一下:
文章末尾有demo源码,欢迎评论交流。
既然是学习slate源码也就不想创新一个数据结构了,沿着前人的路先走一下吧。考虑到后续的大文档需要视窗加载,我认为一个JSON搞定文档过于粗糙了,后续可能会改造成多个数组组成一个文档。
首先,写一个最简单p标签,又叫我们可以怎样从浏览器手中接管用户文本输入。
[{type:'p',children:[{text:'大橘'}]}]
效果如下
如果是想要一行两个大橘,我需要的结构是这样:
[{type:'p',children:[{text:'大橘大橘'}]}]
此时需要一个操作insertText:
export function insertText(root, text, path) {
// 获取指定path的element
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: '大橘' }] }]
insertText(root, '大橘', [0])
console.log(JSON.stringify(root)) //[{"type":"p","children":[{"text":"大橘大橘"}]}]
好了一个编辑器最简单的逻辑ok了。
这里我选择了react
创建项目
(1)npm install -g create-react-app
(2)create-react-app day001
(3)cd day001
(4)npm start
在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">
这是一个demo编辑器
<div id='editor' contentEditable onInput={(e)=>{
e.preventDefault()
e.stopPropagation()
console.log(e)
return
}}>
</div>
</div>
);
}
export default App;
效果图:
小标题又可以叫做在接管输入文字以后,我们可以怎样在富文本光标位置输入文本?
在第一天,我们已经实现了,监听用户输入,并选择性的输入内容。虽然它使用的原理很有价值,但是这个编辑器有点low,不管用户在编辑器哪里输入,内容都只能在文本末尾追加。作为一个富文本编辑器这是不可饶恕的。
那么现在,我们来完善这个问题。
首先,我们知道如何获取光标的位置,以及对应文本的位置。这里我们会用到window.getSelection() api来获取光标所在的dom,以及光标在dom中文本的位置。
insertText代码修改如下
export function insertText(root, text, path) {
const domSelection = window.getSelection()
console.log('domSelection', domSelection, domSelection.isCollapsed, domSelection.anchorNode, domSelection.anchorOffset, JSON.stringify(domSelection))
// 获取指定path的element
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 如果光标选中一个范围
}
// console.log(root[0].children[0] === node, root[0].children[0], node)
}
我们实现了在光标位置插入文本,但是光标在输入后位置不对了,我们接下来要改变光标。
简单介绍一下setBaseAndExtent
方法
// dom 是指要选中的dom节点,offset是指dom节点里面文字的位置
window.getSelection().setBaseAndExtent(
dom, offset, dom2, offset2)
重新写一下我们的APP.js文件,主要修改了两个useEffect
方法,以及把文本渲染交给state来改变。
import './App.css';
import { useState, useEffect } from 'react'
import { getString, insertText } from './insertText'
window.root = [{ type: 'p', children: [{ text: '' }] }]
function App() {
// 记录我们输入的内容
const [txt, setTxt] = useState('')
// 光标的offset
const [txtOffset, setTxtOffset] = useState(0)
// 负责注册监听beforeinput事件,并阻止默认事件。在监听中修改window.root,并在里面更新txt和txtO,最后清除光标,防止txt更新导致光标闪动。
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);
}
}, [])
// 监听txtOffset,并且用setBaseAndExtent更新光标位置,使用setTimeout是因为要在页面渲染后,再改变光标位置
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">
这是一个demo编辑器
<div id='editor' contentEditable onInput={(e) => {
e.preventDefault()
e.stopPropagation()
console.log(e)
return
}}>
{txt}
</div>
</div>
);
}
export default App;
此时,我们的编辑已经可以正常输入英文和数字。但是中文的问题如何解决呢?
后续更新~