一、编辑器分类
1. 接管所有事件,有自己的排版引擎
-
Google Docs
-
光标 kix-cursor-caret
-
输入 docs-texteventtarget-iframe contenteditable
-
-
金山文档
-
光标 cursor-item
-
输入virtual-input
-
-
Tbus
- Tbus-selection
2. 接管渲染,监听/拦截 事件修正状态,有自己的模型层
-
Prosemirror
-
Slate
-
Draft
3. 依赖 document.execCommand
-
ueditor
-
kindeditor
二、实现一个简单的编辑器
1. 什么是 contenteditable
HTML中的 contenteditable
的属性可以打开某些元素的可编辑状态.也许你没用过 contenteditable
属性.甚至从未听说过. contenteditable
的作用相当神奇.可以让 div
或整个网页,以及 span
等等元素设置为可写。我们最常用的输入文本内容便是 inpu
t与t extarea
,使用 contenteditable
属性后,可以在 div
, table
, p
, span
, body
,等等很多元素中输入内容。
即通过 contenteditable
可以让普通的元素实现可编辑状态。
2. 什么是 Selection
Selection
对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。要获取用于检查或修改的 Selection
对象,请调用 window.getSelection() 。
3. 通过execCommand实现编辑器
const formatBlock = 'formatBlock'
const appendChild = (parent, child) => parent.appendChild(child)
const createElement = tag => document.createElement(tag)
const queryCommandValue = command => document.queryCommandValue(command)
export const exec = (command, value = null) => document.execCommand(command, false, value)
const tools = {
bold: {
icon: 'B',
title: 'Bold',
handler: () => exec('bold')
},
heading1: {
icon: 'H1',
title: 'Heading 1',
handler: () => {
if (queryCommandValue(formatBlock) === 'h1') {
exec(formatBlock, '<p>')
} else {
exec(formatBlock, '<h1>')
}
}
},
paragraph: {
icon: 'P',
title: 'Paragraph',
handler: () => exec(formatBlock, '<p>')
},
quote: {
icon: '“',
title: 'Quote',
handler: () => {
exec(formatBlock, '<blockquote>')
const { focusNode } = window.getSelection();
const textBlock = createElement('p');
const blockquote = focusNode.nodeType === 3 ? focusNode.parentElement : focusNode;
textBlock.appendChild(focusNode.nodeType === 3 ? focusNode : focusNode.firstChild);
blockquote.appendChild(textBlock)
}
},
olist: {
icon: '<small>1<small>—',
title: 'Ordered List',
handler: () => exec('insertOrderedList')
},
link: {
icon: '?',
title: 'Link',
handler: () => {
const url = window.prompt('Enter the link URL')
if (url) exec('createLink', url)
}
},
image: {
icon: '📷',
title: 'Image',
handler: () => {
const url = window.prompt('Enter the image URL')
if (url) exec('insertImage', url)
}
}
}
const editor = document.querySelector('#editor');
const toolbar = document.querySelector('#toolbar')
editor.focus();
const wrapParagraph = () => {
if (!editor.firstChild || editor.firstChild.nodeType === 3) exec(formatBlock, `<p>`)
}
wrapParagraph();
editor.onkeydown = event => {
if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') {
setTimeout(() => exec(formatBlock, `<p>`), 0)
}
}
Object.values(tools).forEach((tool) => {
const button = createElement('button')
button.innerHTML = tool.icon
button.title = tool.title
button.setAttribute('type', 'button')
button.onclick = () => tool.handler() && editor.focus()
appendChild(toolbar, button)
})
exec('defaultParagraphSeparator', 'p')
实现了一个完备的编辑器,但是存在一些问题
4. 问题
-
对内容的控制不足,只能满足基本的编辑需求
-
对
contenteditable=false
的元素处理存在很大的问题 -
对历史状态的控制完全依赖浏览器
-
强依赖
document.execCommand
这个不稳定的功能 -
对选区位置缺少控制,依赖浏览器会导致行为不符合预期
-
...
核心的能力依赖的都是外部的不稳定的功能
5. 脱离execCommand实现编辑器
-
execCommand
只在编辑器中渲染,完全可以通过使用dom
的api
来实现渲染功能。 -
更重要的一个问题是拥有一个能描述出当前文档的数据结构,并拦截或者是监听用户的输入行为,把对
dom
的操作转换成对文档结构的操作。再把文档的数据映射到dom
上
实现一个parser
class Node {
constructor(name, data, children = []) {
this.name = name;
this.data = data;
this.children = children;
}
}
class TextNode extends Node {
constructor(data) {
super('text', data)
}
}
class DOMNode extends Node {
constructor(name, data) {
super(name, data)
}
}
class EDOMParser {
constructor() {
this.parser = new DOMParser();
this.top = new DOMNode('body');
}
parse(html) {
const dom = this.parser.parseFromString(html, 'text/html').body;
for(let i = 0; i < dom.childNodes.length; i++) {
const context = new ParseContext(dom.childNodes[i], '')
if (context.content) {
this.top.children.push(context.content);
}
}
return this.top;
}
}
class ParseContext {
constructor(dom) {
this.dom = dom;
this.start();
}
start() {
if (this.dom.nodeType === 1 && this.dom.nodeName === 'P') {
this.content = new DOMNode('P');
this.parseInner(this.dom)
}
}
parseInner(dom) {
for(let i = 0; i < dom.childNodes.length; i++) {
this.addNode(dom.childNodes[i]);
}
}
addNode(dom) {
if (dom.nodeType === 3) {
this.addTextNode(dom);
} else {
this.parseInner(dom)
}
}
addTextNode(dom) {
this.content.children.push(new TextNode(dom.textContent))
}
}
export { EDOMParser }
现在我们就实现了一个简单的编辑器,但还不成熟,我们还应补充:对输入的处理、对粘贴剪切的处理、对选区的处理...
三、总结
对于绝大多数的编辑需求,依赖于 contenteditable
去实现已经可以很好的满足。对于更高阶的需求,我们应该尽可能的抽象,屏蔽对外部的依赖对数据的影响,从而才能实现一个健壮的编辑器。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!