前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一个简单的编辑器

实现一个简单的编辑器

作者头像
皮小蛋
发布2021-02-07 16:14:41
9770
发布2021-02-07 16:14:41
举报
文章被收录于专栏:前端皮小蛋前端皮小蛋

一、编辑器分类

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实现编辑器

代码语言:javascript
复制
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: '&#128247;',
    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. 问题

  1. 对内容的控制不足,只能满足基本的编辑需求
  2. contenteditable=false 的元素处理存在很大的问题
  3. 对历史状态的控制完全依赖浏览器
  4. 强依赖 document.execCommand 这个不稳定的功能
  5. 对选区位置缺少控制,依赖浏览器会导致行为不符合预期
  6. ...

核心的能力依赖的都是外部的不稳定的功能

5. 脱离execCommand实现编辑器

  1. execCommand 只在编辑器中渲染,完全可以通过使用 domapi 来实现渲染功能。
  2. 更重要的一个问题是拥有一个能描述出当前文档的数据结构,并拦截或者是监听用户的输入行为,把对 dom 的操作转换成对文档结构的操作。再把文档的数据映射到 dom

实现一个parser

代码语言:javascript
复制
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 去实现已经可以很好的满足。

对于更高阶的需求,我们应该尽可能的抽象,屏蔽对外部的依赖对数据的影响,从而才能实现一个健壮的编辑器。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端皮小蛋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、编辑器分类
    • 1. 接管所有事件,有自己的排版引擎
      • 2. 接管渲染,监听/拦截 事件修正状态,有自己的模型层
        • 3. 依赖 document.execCommand
        • 二、实现一个简单的编辑器
          • 1. 什么是 contenteditable
            • 2. 什么是 Selection
              • 3. 通过execCommand实现编辑器
                • 4. 问题
                  • 5. 脱离execCommand实现编辑器
                  • 三、总结
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档