HTML中的 contenteditable
的属性可以打开某些元素的可编辑状态.也许你没用过 contenteditable
属性.甚至从未听说过. contenteditable
的作用相当神奇.可以让 div
或整个网页,以及 span
等等元素设置为可写。我们最常用的输入文本内容便是 inpu
t与t extarea
,使用 contenteditable
属性后,可以在 div
, table
, p
, span
, body
,等等很多元素中输入内容。即通过 contenteditable
可以让普通的元素实现可编辑状态。
Selection
对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。要获取用于检查或修改的 Selection
对象,请调用 window.getSelection() 。
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')
实现了一个完备的编辑器,但是存在一些问题
contenteditable=false
的元素处理存在很大的问题document.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
去实现已经可以很好的满足。
对于更高阶的需求,我们应该尽可能的抽象,屏蔽对外部的依赖对数据的影响,从而才能实现一个健壮的编辑器。