【记】一个简单的文本编辑器实现过程

1,234 阅读4分钟

使用typescript&webpack从0搭建

实现功能

  • 加粗
  • 设置标题
  • 设置颜色

如何启用

  1. js提供了一个 api:contenteditable 允许我们对内容进行编辑。即在标签中加入contenteditable
  2. document对象暴露出一个execCommand方法去操纵当前的可编辑区域。即document.execCommand()

实现步骤

  1. 搭建工程目录
  • 新建目录: editor
  • 进入目录 执行 npm init初始化目录默认即可
  • 安装所需插件
npm install webpack webpack-cli dev-server clean-webpack-plugin html-webpack-plugin cross-env typescript ts-loader style-loader css-loader less-loader less --save-dev
# 这里踩了个坑 后来把webpack版本改为了^4.43.0 webpack-cli版本改为了^3.3.11
# 重新npm i了
  • 初始化ts文件tsc --init 会出现
  • webpack配置 新增目录build并创建webpack.config.js文件
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin }  = require('clean-webpack-plugin')
//使用node的模块
module.exports = {
    //这就是我们项目编译的入口文件
    entry: "./src/index.ts",
    output: {
        filename: "main.js"
    },
    resolve: {
      extensions: ['.ts', '.js', '.json'],
    },
    //这里可以配置一些对指定文件的处理
    //这里匹配后缀为ts或者tsx的文件
    //使用exclude来排除一些文件
    module:{
        rules:[
            {
                test:/\.tsx?$/,
                use:'ts-loader',
                exclude: /node_modules/
            },
            {
              test:/\.less$/,
              use:['style-loader','css-loader','less-loader']
            }
        ]
    },
    //这个参数就可以在webpack中获取到了
    devtool: process.env.NODE_ENV === 'production'? false : 'inline-source-map',
    devServer:{
        //这个本地开发环境运行时是基于哪个文件夹作为根目录
        contentBase:'./dist',
        //当你有错误的时候在控制台打出
        stats: 'errors-only',
        //不启动压缩
        compress: false,
        host: 'localhost',
        port: 8081
    },
    //这里就是一些插件
    plugins:[
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['./dist']
        }),
        new HtmlWebpackPlugin({
            template: './src/index.html'
        })
    ]
}
  • 新建文件 新建src 并创建index.ts,index.html 新建css文件夹 并创建index.less 目录结构如下:
├── README.md
├── build
│   └── webpack.config.js
├── package.json
├── src
│   ├── css
│   │   └── index.less
│   ├── index.html
│   └── index.ts
└── tsconfig.json
  • 在package.json文件中的scripts下新增一条打包命令
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.config.js"
  },

至此 目录搭建完成

2.编辑页面 在index.html中编写页面骨架

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>富文本编辑器</title>
</head>
<body>
  <div>
    <div class="btn_box" id="blod">加粗</div>
    <div class="btn_box">
      <div>设置标题</div>
      <ul>
        <li class="title_name">h1</li>
        <li class="title_name">h2</li>
        <li class="title_name">h3</li>
        <li class="title_name">h4</li>
        <li class="title_name">正文</li>
      </ul>
    </div>
    <div class="btn_box">
      <div>设置颜色</div>
      <ul>
        <li class="color_name">red</li>
        <li class="color_name">blue</li>
        <li class="color_name">yellow</li>
        <li class="color_name">black</li>
      </ul>
    </div>
  </div>
  <div id="editor" contenteditable>
</body>
</html>
  • index.less样式
ul li {
  list-style-type: none;
}

#editor {
  width: 400px;
  height: 200px;
  border: 1px solid #ddd;
  margin-top: 10px;
}

.btn_box {
  display: inline-block;
  border: 1px solid #ddd;
  padding: 5px 10px;
  position: relative;
  cursor: pointer;
  margin-right: 10px;

  ul {
    display: none;
    position: absolute;
    left: 0;
    top: 15px;
    padding: 0;
    border: 1px solid #ddd;
    z-index: 99;
    background: #fff;
    width: 100%;
    text-align: center;

    li {
      .title_name, .color_name {
        cursor: pointer;
      }

      &:hover {
        background: #eee;
      }
    }
  }

  &:hover {
    ul {
      display: block;
    }
  }
}
  • 实现ts中的逻辑

踩坑指南

最早我是尝试使用botton实现加粗功能一切正常
后来因为增加了标题和设置颜色的下拉菜单 统一更改为div组件 这个时候div的click事件其实是正常执行了
但是click事件会导致富文本编辑框失去焦点,因此execCommand函数无能为力
最后改用了mousedown来作为监听同时还要组织默认事件,否则富文本编辑框的选中区域会消失
对了还要阻止默认事件,否则富文本编辑框的选中区域会消失`e.preventDefault()`否则

code

import './css/index.less'

class Menus {
  public element: HTMLElement
  public value: string
  public target: string

  constructor(element: HTMLElement, target: string, value: string) {
    this.element = element as HTMLDivElement
    this.value = value
    this.target = target
  }

  // 初始化
  init () {
    this.element.onmousedown = (e: Event) => {
      document.execCommand(this.target, false, this.value)
      e.preventDefault()
    }
  }
}

window.onload = () => {
  let blod = document.querySelector('#blod') as HTMLElement
  let title = document.querySelectorAll('.title_name')
  let color = document.querySelectorAll('.color_name')
  // 加粗
  let blodMenu = new Menus(blod, 'bold', 'null')
  blodMenu.init()

  // 设置标题
  title.forEach(item => {
    let li = item as HTMLElement
    let titleMenu: Menus
    if (li.innerHTML === '正文') {
      titleMenu = new Menus(li, 'formatblock', '<p>')
    } else {
      titleMenu = new Menus(li, 'formatblock', li.innerHTML)
    }
    titleMenu.init()
  })

  // 设置颜色
  color.forEach(item => {
    let li = item as HTMLElement
    let colorMenu = new Menus(li, 'forecolor', li.innerHTML)
    colorMenu.init()
  })
}

记录选区

以上有个缺陷

  1. 在编辑区写入内容然后选中一段文本,俗称拖蓝
  2. 鼠标在编辑区外点击
  3. 点击加粗

加粗功能无效了

解决思路

  • onmouseup,onkeyup,onmouseout编辑区的时候获取选区并存储range
saveSelection () {
  const selection = window.getSelection() as Selection
  if (selection.rangeCount === 0) {
      return
  }

  const range: Range | null | undefined = selection.getRangeAt(0)
  let elem: Node
  let containerElem
  if (range) {
    elem = range.commonAncestorContainer
    containerElem = elem.nodeType === 1 ? elem : elem.parentNode
  }

  if (!containerElem) return
  // 编辑区是否包含选区内容
  if(this.editor.contains(containerElem)) {
    this.range = range
  }
}
  • 当点击加粗按钮时读取上次的range并判断该range内容是否存在于当前的编辑器区的内容中存在则我们回复选区
resetSelection () {
  const selection = window.getSelection() as Selection
  selection.removeAllRanges()
  if (this.range) {
    selection.addRange(this.range)
  }
}

Menus中的init

// 初始化
  init (): void {
    this.element.onmousedown = (e: Event) => {
      this.selectionRange.resetSelection()
      document.execCommand(this.target, false, this.value)
      e.preventDefault()
    }
    this.element.onmouseup = (e: Event) => {
      this.selectionRange.saveSelection()
    }
  }

重复以下动作

1. 在编辑区写入内容然后选中一段文本,俗称拖蓝
2. 鼠标在编辑区外点击
3. 点击加粗

拖蓝区已可以加粗了。

参考文献