preface

Evolution of rich text editor technology

Level 0 (do not know why to start from zero) is the initial stage of the editor, representing the implementation of the old generation of editors Level 1 stage 2, is developed from the first stage, has a certain degree of advanced, also introduced some mainstream programming ideas, has a certain degree of abstraction for rich text content Level 2 stage 3, Completely independent of the browser's editing capabilities, independent implementation of the cursor and typesettingCopy the code

After searching the nuggets and realizing how few articles there are in rich text editors, I decided on a whim to write a series from the Level 0 editor to the Level 2 editor.

From Level 0 to Level 2, it is understood that the control of the rich text editor is gradually transferred from the browser to the developer.

The simplest rich text editor

<div contenteditable></div>
Copy the code

Leveraging the capabilities of the browser, we have one of the simplest rich text editors that can be used for bold, italicized text using browser capabilities such as Document. execCommand.

The content of this chapter is to write a simple Level 0 rich text editor

A simple editor

For simplicity, we first divide the modules as shown below

Editor core

First, let’s define the main class Editor and think about the basic apis needed by an Editor. We initially define two apis [Get Editor content] and [set Editor content].

// editor.ts interface IEditorProps { container: HTMLElement; editable: boolean; // Can the editor edit createToolbar? : (editor: Editor) => HTMLElement[]; } class Editor {// Editor body edit area private body: HTMLELement; constructor(props: IEditorProps) { // ... } // Get rich text content getContent() {return this.body.innerhtml; } // Set the rich text content setContent(HTML: string) {this.body.innerhtml = HTML; } / / whether the focus within the editor isFocus () {return (document. ActiveElement = = = this. Body | | enclosing body! .contains(document.activeElement) ); ExecCommand (commandId, showUI?) execCommand(commandId, showUI?) , value?) { if (! this.isFocus()) { this.selection.setSelection(); } document.execCommand(commandId, showUI, value); }}Copy the code

Editor selection

Selection represents the current editor selected text Range or cursor position, selection expression is Range, currently using the browser’s native expression developer.mozilla.org/zh-CN/docs/…

// selection.ts
interface ISelectionProps {
  body: HTMLElement;
}
export class Selection {
  private range: Range | null = null;
  
  constructor (private props: ISelectionProps) {
    document.addEventListener('selectionchange', this._handleSelectionChange);
  }

  private _handleSelectionChange = () => {
    // ...
  }

  setSelection = (range?: Range) => {
    // ...
  }
  
  destroy() {
    document.removeEventListener('selectionchange', this._handleSelectionChange);
  }
}

Copy the code

The toolbar

The current approach is to pass in a createToolbar method to create the toolbar, but this may not be a very appropriate approach, and better toolbar module design is still under consideration.

In the current design, createToolbar returns an HTMLElement array, so one might wonder why you don’t just pass in the HTMLElement array and pass in a function instead of an extra layer. The main reason is for extension, although the API that the toolbar now uses is basically Document. execCommand, it is not completely independent of the current Editor instance, and at some point you may need to call an API inside the Editor. So you need to pass in the current instance of Editor.

The three toolbar buttons are currently built in

Toolbar button pit

For example, bold, when we select the text, click the bold button and expect the selected text to be bold.

We did this by binding the click event to the bold button, but when we did that, we found that the text was not bold.

  boldBtn.addEventListener('click', () => {
  	document.execCommand('bold');
  });
Copy the code

The reason is that when we click the event execution, the Windows selection is no longer on the selected text, as shown in the figure

Solution 1

Reset the selection saved in the editor back. The downside of this scheme is that the selected blue area flashes.

const selection = window.getSelection(); if (! selection) { return; } selection.removeAllRanges(); // range is the original selection. AddRange (range);Copy the code
Solution 2

We recorded a profile and found it inmousedownAfter the event triggers the edit areablurEvent. That’s easy. We stop itmousedownThe default behavior of the event is ok.

  boldBtn.addEventListener('mousedown', (e) => {
  	e.preventDefault();
  });
Copy the code

Source github.com/TGuoW/T-edi…

conclusion

This concludes the first article, and you can see that this simple editor still has many shortcomings.

Let me give you some examples

  1. Editor content is completely browser-controlled and may vary from browser to browser, making it prone to unexpected behavior

  2. The selection description is still a native Range, which is not intuitive. For example, our normal thinking might be that my cursor is in a column of a row, not an offset of an element as it is now

  3. It lacks many functions, such as inserting pictures, inserting videos, etc.

  4. It’s hard to extend, and if a developer wants to insert an iframe into an editor, he has to change the underlying code.