学习
实践
活动
专区
工具
TVP
写文章
专栏首页前端皮小蛋实现一个简单的编辑器

实现一个简单的编辑器

一、编辑器分类

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: '&#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

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 删除。
登录 后参与评论
0 条评论

相关文章

  • 带你实现一个简单的多边形编辑器

    多边形编辑器少数见于一些图片标注需求,常见于地图应用,用来绘制区域,比如高德地图:

    街角小林
  • 从零开始实现一个简单的低代码编辑器

    低代码编辑器作为一种能够极大地提升开发效率的PaaS软件,近些年来一直收到各大公司以及各路投资方的追捧。而对于我们前端开发者来说,编辑器也是为数不多的拥有较深前...

    Tecvan
  • 编写一个非常简单的 JavaScript 编辑器

    当然,我们已经有可以使用的很好的Web编辑器:你只需下载,并插入页面即可。我以前习惯于使用CodeMirror和ACE。例如,我为CodeMirror写了一个插...

    哲洛不闹
  • Python做一个简单的在线编辑器[通俗易懂]

    主要使用了pywebio程序,实现了Python的简陋在线编辑器。 相对C++编辑器就比较复杂,需要调用g++.exe,可能在您的电脑上,就不见得能用了,需要...

    全栈程序员站长
  • 实现一个简单的redux

    上面的代码虽然实现了修改, 但是却没有通知到所有使用到name的地方上,我们通过发布订阅来实践一下

    JianLiang
  • 实现一个简单的JS效果

    ,一开始我以为只是用一个i标签创建出一个三角符号出来后,然后通过JS来把它的颜色和方向换过,但后来发现并不是这样。直接在原来的i标签的地方在创建多一个i标签创建...

    PHY_68
  • 自己实现一个简单的链表

    用户1215919
  • 实现一个简单的登录页面

    将登录页面和注册页面通过定位叠在一起,再将注册页面旋转180度,再用一个外层盒子包裹着这2个页面,这样只需转动外层盒子就能实现2个页面的交替出现效果

    小丞同学
  • Spring AOP的一个简单实现

    首先配置XML:service采用和之前一样的代码,只是没有通过实现接口来实现,而是直接一个实现类。transactionManager依旧为之前的事务管理器。

    Rekent
  • 如何实现一个简单的IOC

    在之前的文章中,楼主和大家一起分析spring的 IOC 实现,剖析了Spring的源码,看的出来,源码异常复杂,这是因为Spring的设计者需要考虑到框架的扩...

    Bug开发工程师
  • python实现一个简单的dnspod

    dnspod api地址:https://www.dnspod.cn/docs/records.html#record-create

    py3study
  • Netty实现一个简单的 RPC

    众所周知,dubbo 底层使用了 Netty 作为网络通讯框架,而 Netty 的高性能我们之前也分析过源码,对他也算还是比较了解了。今天我们就自己用 Nett...

    用户5224393
  • 基于浏览器实现最最简单的富文本编辑器,一个文件全搞定

    这是一个超级简单的编辑器,你可以把它用在任何你需要的项目里面,只花费喝咖啡的时间。

    爱吃大橘
  • 如何实现一个简单的rpc

    为了实现一个自定义的rpc,如果想实现一个rpc,其本质是将远程调用可以和本地调用一样。而要实现这样的功能,首先我们需要一个解码器Decoder和一个编码器En...

    路行的亚洲
  • 如何实现一个简单的-IOC

    我们还记得Spring中最重要的有哪些组件吗?BeanFactory 容器,BeanDefinitionBean的基本数据结构,当然还需要加载Bean的资源加载...

    三哥
  • 一个简单实用的SSAO实现

    原文链接: http://www.gamedev.net/reference/programming/features/simpleSSAO/

    逍遥剑客

扫码关注腾讯云开发者

领取腾讯云代金券