preface

I recently saw some editor libraries on Github, so I took a moment to explore some editors and the native API document.execCommand (if you’re not familiar with the API, check out the corresponding document-mDN). A simple summary is that you can run commands to manipulate elements in the contenteditable content area. This article focuses on the shortcomings and alternatives of this API.

Directions of Thinking

  • Document. ExecCommand defects
  • Current usage of Document. ExecCommand by major editors
  • Explore the editor for custom commands

Problems with execCommand

We all know that the core editing capability of many editors (rich text editors) on the market is based on this API. However, there are two major problems with this API: compatibility and scalability.

Compatibility issues

Document. ExecCommand, MDN explicitly states that this is aObsoleteA feature that browser vendors can no longer support (is a deprecated API).

And the current compatibility of various browsers is not good. Here is the compatibility of Caniuse:

From the above compatibility, the current major browser compatibility is also very poor. For example, the popular editors Quill, UEditor and wangEditor in the market are all implemented based on this API. If you want to take the editor to the next level, you must break through this node.

Scalability problem

The capabilities that Document. ExecCommand can provide are still a little limited and not enough to meet most requirements. But it can be tricky if the user needs to extend, for example by customizing some behavior.

Analysis of the editor’s capabilities

Today’s mainstream editors generally correspond to three types, each of which presents different capabilities, extensibility, and complexity. As shown in the following table, L0 -> L1 -> L2 can also be said to be: standing on the head of the browser, standing on the shoulder of the browser and standing on the foot of the browser, gradually out of the browser, and closer to the modernization. Typical examples are draft.js and SLATE.

type describe On behalf of Pros and cons
L0
1. Browser-based contenteditable rich text input boxes

1. Use document.execCommand to run the command

Lightweight editor

Typical example: wangEditor
Good: rapid development in a short time Bad: very limited space for customization
L1
1. Contentditable rich text input box based on the browser

1. Implement operation commands independently

Typical examples: draft.js(initializing an edit area), TinyMCE, etc Good: on the basis of the browser, can meet most of the business bad: can not break through the browser itself typesetting effect
L2
1. Independently implement rich text input box

1. Rely on a small number of browser apis

Google Doc, others (Office Word Online, WPS Text Online) Good: all their own implementation, controllable degree is controlled by the developer bad: great technical difficulty

Google Doc (you can refer to: drive.googleblog.com/2010/05/wha…). From contentEditable to listening for user interactions while drawing on the DOM with tags such as divs.

Document. execCommand Use case

WangEditor is used as an example to analyze the use of document.execCommand. The wangEditor core file is command-.ts to encapsulate the command. Here are some key pseudocode:

/** * Command to perform the operation *@param name name
* @param value value
*/
public do(name: string, value? :string | DomElement): void {
  // TODO
  
	switch (name) {
    case 'insertHTML':  // Insert the HTML string
        this.insertHTML(value as string)
        break
    case 'insertElem':  // Insert a DOM element
        this.insertElem(value as DomElement)
        break
    default:
        // Default command Executes the default browser command
        this.execCommand(name, value as string)
        break
	}
  
  // TODO
}

/** * insert HTML *@param HTML The HTML character string */
private insertHTML(html: string) :void {
	// inserHTML is not available under IE and requires compatibility processing
  
  if(isNoIE) {
  	this.execCommand('insertHTML', html)
  } else {
		// Use window.selection to get the selection, and then use insertNode to implement insertHTML in IE
  
  	range.deleteContents()
  	range.insertNode()
    
	}
}


Copy the code

Inserts are compatible, and the rest is implemented using native browser apis, but at this level, the design is limited if the user wants to customize some events or behaviors. The ability to get to L1 without breaking through the editor’s L0.

Preliminary exploration

Based on the above analysis, you learned about the shortcomings of Document. execCommand and some use cases. Does anyone look at this and think: since this API is obsolete and not compatible, is there an alternative? There are, of course, browser apis such as Clipboard, but compatibility is not very good, at least for IE.

Is there an alternative? The answer is yes. Now let’s take a look at how deckDeckGo can implement commands autonomously. With that in mind, explore the editor at L1 stage.

deckdeckgo

The design of the slides is quite novel

Official: DeckDeckGo – Open source Web editor for presentations. This is a foreign open source project. It is based on @Stencil /core to do component rendering, and event management, and has mobile and PC side, some basic operations are covered. An editor that can be generalized to L1 capabilities, leveraging the capabilities of Contentditable, plus custom commands.

bold

Using bold as an example, here is a simple flowchart that abstracts the bold source logic:

Description:

  1. The user clicks the bold button to trigger the native Button click event inside the Component action-button
  2. The event emits props events passed in from the outside by executing the decorated handle. (Learn how to trigger a Web Component definition event)
  3. The same logic continues until the outermost inline-editor calls execCommand, which contains two handles: execCommandStyle(a custom command to change the style) and execCommandList.

Note: The event decorations here are provided by @stencil/ Core, so you can check out this library for yourself. @StenCIL/Core internal JSX renders better than Web Component.

execCommandStyle

This function is also light and is based on two functions: updateSelection && replaceSelection, one to update the selection and one to replace the selection.

updateSelection

The main thing this method does is to find out if there is a selection and if there is a container, if there is a style on the container, then update the style. You can see from the diagram above that the detail has a style property under it, and it’s evaluated by that property. Here is the source code:

  
if(sameSelection && ! DeckdeckgoInlineEditorUtils.isContainer(containers, container) && container.style[action.style] ! = =undefined) {
    await updateSelection(container, action, containers);

    return;
  }

async function updateSelection(container: HTMLElement, action: ExecCommandStyle, containers: string) {
  container.style[action.style] = await getStyleValue(container, action, containers);

  awaitcleanChildren(action, container); } getStyleValue does the simple thing of getting the corresponding value. Internally, the style inheritance is judged.Copy the code

The way the updateSelection is updated, omits creating the DOM all the time, which is what Document. execCommand does. But what if you don’t find it? The replaceSelection thing.

replaceSelection

This method needs to match the range of the selection. For details, see the pseudo-code below:


async function replaceSelection(container: HTMLElement, action: ExecCommandStyle, selection: Selection, containers: string) {
  const range: Range = selection.getRangeAt(0);

  const fragment: DocumentFragment = range.extractContents();

  const span: HTMLSpanElement = await createSpan(container, action, containers); // Append instructions
  
  span.appendChild(fragment);

  await cleanChildren(action, span);  // If there are child elements under span, you need to clear the action. Style of all child elements to implement inheritance.
  await flattenChildren(action, span); // Under span, there is no style child element

  range.insertNode(span);
  selection.selectAllChildren(span);
}


Copy the code

ReplaceSelection cuts the selection’s contents to the document fragment using range.Extractcontents (), and then creates a span. When creating a SPAN, Set action.style & action.value to span, insert document fragments into span, do some cleanup, and insertNode into range.

conclusion

I learned a little bit about draft.js, which doesn’t come out of the box and just provides a lot of tools to create editors. It describes HTML as a data structure in the form of a description, and does it directly in React mode by intercepting cursor and keyboard operations, then updating it to internal IMmutable state, and then rendering it in render mode. Immutable to improve rendering performance. In my opinion, this way, the editor may be too heavy, and the complexity may also increase dramatically. I haven’t seen much of tinyMCE, but the idea is block-oriented, similar to draft.js, which abstracts an element. Finally, this article may have some problems, opinions may be relatively shallow, as a brick to attract jade, if the article is written where there is a problem, I hope you see the officer give advice or two.

References:

  1. Caniuse.com/?search=doc…
  2. Developer.mozilla.org/zh-CN/docs/…
  3. www.zhihu.com/question/40…
  4. Github.com/deckgo/deck…
  5. Github.com/tinymce/tin…
  6. Github.com/facebook/dr…