Rich text editor

The project requirements

Don’t use any framework to develop a simple rich text editor from 0, achieve “bold” “set title” “set color” three functions, using typescript and Webpack, compiled and packaged.

Implementation approach

Webpack configuration

  1. entry outputGeneral configuration, because we intend to make a third-party package, so the packaged file is useless (hashcontent– Prevent caching) for file name padding.
  2. resolveConfigure some common path matching rules and the resolution suffix is.tsWebpack can’t find ts files with typescript
  3. loader:
  • sass-loaderSupports SCSS file parsingpostcss-loaderAdd browser vendor-specific prefixes to support future CSS syntaxcss-loaderThe modules parameter is configured so that CSS can be imported modularly, preventing style contamination.
  • asset/resource webpack5Send a separate file and export the URL. This was previously implemented using file-loader.
  • babel-loader + ts-loader tsFile compilation + Babel supports ES5
  1. Plugins:
  • useMiniCssExtractPluginPS: Webpack packs CSS into output files by default
  • HtmlWebpackPluginPackaging automatically generates HTML from templates
  • CleanWebpackPluginClear the files in the dist folder before packing
    const path = require('path')
    const resolve = (dir) = > path.join(path.resolve(__dirname, '.. / '), dir)
    module.exports = {entry:resolve('src/index.ts'),
      output: {
        path: resolve('dist'),
        filename:'index.js'
      },
      resolve: {
        extensions: ['.tsx'.'.ts'.'.js'].alias: {
          The '@': resolve('src/'),}},module: {
        rules: [{// Componentize moudle SCSS configuration
            test: /\.scss$/i,
            include:resolve("src/components"),
            use: [
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader'.options: {
                  importLoaders:2.modules: {
                     localIdentName: "leo-[local]"}}},'postcss-loader'
              , 'sass-loader'],}, {test: /\.(png|svg|jpg|jpeg|gif)$/i,
             type: 'asset/resource'}, {test: /\.(woff|woff2|eot|ttf|otf)$/i,
            type: 'asset/resource'}, {test: /\.ts$/,
            use: ['babel-loader'.'ts-loader'].exclude: /node-modules/},],},plugins: [
        new HtmlWebpackPlugin({template:resolve('src/index.html')}),
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin()
      ]
   }   
Copy the code

ts.config.js

 {
  "compilerOptions": {
    "outDir": "./dist/"."noImplicitAny": true."module": "es6"."target": "es5"."jsx": "react"."allowJs": true."allowSyntheticDefaultImports": true."moduleResolution": "node",},"include": [
    "src/"]."exclude": [
    "node_modules"]},Copy the code
  • .babelrc

Babel/plugin-transform-Runtime supports polify and helper import of some functions on demand

{
  "presets": [["@babel/preset-env",
      {
        "useBuiltIns": "usage"."corejs":3}]],"plugins": [["@babel/plugin-transform-runtime",
      {
        "corejs": 3}}]]Copy the code

Information acquisition before code implementation

Refer to the relevant documentation for the following information

Contenteditable indicates whether the MDN document should be edited by the user

 <div contenteditable="true"></div>

Copy the code

The let Selection = window.getSelection() method is used to manipulate the cursor. Selection. GetRangeAt () returns a set of all cursors. Because rich text has only one cursor, usually selection. GetRangeAt (0) gets the current cursor Range object. MDN document

The figure is the selected Range object that contains the

  • startoffset.endoffsetIs the start and end position of mouse select drag.
  • startContainer.endContainerreturnRangeStart and end nodes.
  • collapsed:startoffsetandendoffsetEqual returnstrueThe current cursor position.

Document. ExecCommand (aCommandName, aShowDefaultUI, aValueArgument) can use these commands to implement the current Range object region. Although officially Deprecated… MDN document

document.execCommand("formatBlock".false."H1") // Set the header to H1
document.execCommand("foreColor".false."#D4F2E7") // Change the color
document.execCommand("bold".false) / / bold
Copy the code

The implementation code

Project realization diagram

Encapsulate cursor manipulation functions

This is because in practice we often need to save the current Range object and then reset it to the last saved node at some point, such as when we click on the Toolbar and lose focus. At this point, the Range object is not the same as the one we edited in the editor, so there will be no response to executing the command

utils.ts
/ * * *@function Save the current Range object */
export const saveSelection = () = > {
  let selection = window.getSelection();
  if (selection.rangeCount > 0) {
    return selection.getRangeAt(0);
  }
  return null;
};
/ * * *@function Reset Range object *@param SelectedRange The Range object before the cursor */
export const restoreSelection = (selectedRange: Range) = > {
  let selection = window.getSelection();
  if(selectedRange) { selection.removeAllRanges(); selection.addRange(selectedRange); }};Copy the code

ExecCommand simple encapsulation

export const execCommand = (id: string, val? :string) = > {
  return document.execCommand(id, false, val);
};

Copy the code

Code implementation template

Then pull out the following common interfaces: for example, all components need create initialization, template template HTML is mounted, and handler adds listening events

export interface LComponent {
  readonly create: () = > void;
  readonly template: () = > void;
  readonly handler: () = > void;
} 
Copy the code

Define the props implementation interface, here I don’t post all the code implementation, here there is pseudocode!

interface EditContainerConfig {
  height: string;
  focus: boolean;
  zIndex: number;
}
export default class EditContianer implements LComponent {
  elem: HTMLElement;
  config: EditContainerConfig;
  selectedRange: Range;
  currentSeleted: HTMLElement;
  constructor(selector: string) {
    this.elem = document.querySelector(selector);
    this.config = {
      height: "400px".zIndex: 10001.focus: true}; }create() {
     this.template()
     this.handler()
  }

  template() {
     this.elem.innerHTML = ` 
      



`
; } handler() { const toolBar: HTMLDivElement = document.querySelector(".leo-tool-bar"); this.elem.addEventListener( "keyup".(e) = > { // Listen to delete key keep p tag if (e.keyCode === 8 && keepP.innerHTML === "<br>") { leoEditor.innerHTML = `<p id="keep-p"><br></p>`; } this.selectedRange = saveSelection(); }); }// Event listener delegate(toolBar, "click"."i".(e) = > { if(xxx) execCommand(); }); }}Copy the code

conclusion

Although it has been simply completed, there are a lot of bugs. It is the first time to peep into the pit of the rich text editor. Many of my code is not a good implementation scheme. If you really want to solve these problems one by one, it will take a lot of effort, and custom cursor selector may be the mainstream of the future.

Project Github links to LeoEditor