introduce

Slate is a rich text editor development framework that uses TypeScript. Slate was created in 2016 by Ian Storm Taylor. It has absorbed the advantages of Quill, Prosemirror and draft.js, and its core data model is very simplified and highly extensible. The latest version is V0.60.1.

The characteristics of

  • Plug-ins, as first-class citizens, can completely modify editor behavior
  • The data layer is separated from the rendering layer, and the updated data triggers the rendering
  • Document data is like a DOM tree and can be nested
  • It has an ATOMized operation API and theoretically supports collaborative editing
  • Use React as the render layer
  • Immer immers are immutable data structures

Architecture diagram

Source code analysis

Slate uses the Monorepo approach to manage the repository, with four source packages in the Packages directory.

slate

The SLATE core repository, which contains abstract data model interfaces, methods that manipulate nodes, methods that create instances, and more.

Interfaces

Under the intefaces directory is the data model defined by Slate.

Node represents different types of nodes in the Slate document tree.

export type BaseNode = Editor | Element | Text

export type Descendant = Element | Text

export type Ancestor = Editor | Element
Copy the code

The Editor object is used to store all the state of the Editor, and you can add helper functions or implement new behaviors through plug-ins.

export interface BaseEditor {
  children: Descendant[]
  selection: Selection
  operations: Operation[]
  marks: Omit<Text, 'text'> | null

  / / /...
}
Copy the code

An Element object is a node in Slate’s document tree that contains other elements or Text, which can be block-level or inline, depending on the editor configuration.

export interface ElementInterface {
  isAncestor: (value: any) = > value is Ancestor
  isElement: (value: any) = > value is Element
  isElementList: (value: any) = > value is Element[]
  isElementProps: (props: any) = > props is Partial<Element>
  matches: (element: Element, props: Partial<Element>) = > boolean
}
Copy the code

Text objects represent leaf nodes in the document tree, nodes that actually contain Text and formatting, and they cannot contain other nodes.

export interface TextInterface {
  equals: (text: Text, another: Text, options? : { loose? :boolean }) = > boolean
  isText: (value: any) = > value is Text
  isTextList: (value: any) = > value is Text[]
  isTextProps: (props: any) = > props is Partial<Text>
  matches: (text: Text, props: Partial<Text>) = > boolean
  decorations: (node: Text, decorations: Range[]) = > Text[]
}
Copy the code

Path is an indexed list that describes the specific location of a Node in the document tree, generally relative to an Editor Node, but can also be any other Node Node.

export interface PathInterface {
  ancestors: (path: Path, options? : { reverse? :boolean }) = > Path[]
  common: (path: Path, another: Path) = > Path
  compare: (path: Path, another: Path) = > -1 | 0 | 1
  endsAfter: (path: Path, another: Path) = > boolean
  endsAt: (path: Path, another: Path) = > boolean
  endsBefore: (path: Path, another: Path) = > boolean
  equals: (path: Path, another: Path) = > boolean
  / / /...
}
Copy the code

The Point object represents a text node at a specific location in the document tree.

export interface PointInterface {
  compare: (point: Point, another: Point) = > -1 | 0 | 1
  isAfter: (point: Point, another: Point) = > boolean
  isBefore: (point: Point, another: Point) = > boolean
  equals: (point: Point, another: Point) = > boolean
  isPoint: (value: any) = > value is Point
  transform: (point: Point, op: Operation, options? : { affinity? :'forward' | 'backward' | null }
  ) = > Point | null
}
Copy the code

Operation objects are low-level instructions that Slate uses to change the internal state, and Slate represents all changes as Operation.

export interface OperationInterface {
  isNodeOperation: (value: any) = > value is NodeOperation
  isOperation: (value: any) = > value is Operation
  isOperationList: (value: any) = > value is Operation[]
  isSelectionOperation: (value: any) = > value is SelectionOperation
  isTextOperation: (value: any) = > value is TextOperation
  inverse: (op: Operation) = > Operation
}
Copy the code

Transforms

Transforms are auxiliary functions that manipulate documents, including selection Transforms, node Transforms, text Transforms, and generic Transforms.

export constTransforms = { ... GeneralTransforms,// Run the Operation command. NodeTransforms,// Operate the node. SelectionTransforms,// Select. TextTransforms,// Manipulate text
}
Copy the code

createEditor

Method to create an Editor instance that returns an Editor instance object that implements the Editor interface.

/// create-editor.ts

export const createEditor = (): Editor= > {
  const editor: Editor = {}
  / / /...
  return editor
}
Copy the code

slate-react

The React component of the Slate-React editor renders document data.

Slate

A wrapper for the component context that handles the onChange event and accepts the document data value.

/// Slate.tsx

export const Slate = () = > {
  / / /...
  return (
    <SlateContext.Provider value={context}>
      <EditorContext.Provider value={editor}>
        <FocusedContext.Provider value={ReactEditor.isFocused(editor)}>
          {children}
        </FocusedContext.Provider>
      </EditorContext.Provider>
    </SlateContext.Provider>)}Copy the code

Editable

The main area of the editor, which sets tag properties and handles DOM events.

/// Editable.tsx

export const Editable = (props: EditableProps) = > {
  / / /...
  return (
    <ReadOnlyContext.Provider value={readOnly}>
      <Component>
        <Children />
      </Component>
    </ReadOnlyContext.Provider>)}Copy the code

Children

Generate render components from editor document data.

/// Children.tsx

const Children = () = > {
  const children = []
  / / /...
  return <React.Fragment>{children}</React.Fragment>
}
Copy the code

Element

Render Elment components, render elements using the renderElement method, and generate child elements using the Children component.

/// Element.tsx

const Element = () = > {
  / / /...
  return (
    <SelectedContext.Provider value={!!!!! selection}>
      {renderElement({ attributes, children, element })}
    </SelectedContext.Provider>)}Copy the code

Text

Render the text node component.

/// Text.tsx

const Text = () = > {
  / / /...
  return (
    <span data-slate-node="text" ref={ref}>
      {children}
    </span>)}Copy the code

withReact

The Slate plugin adds/overrides some methods to the editor instance.

/// with-react.ts

export const withReact = <T extends Editor>(editor: T) => {
  const e = editor as T & ReactEditor
  const { apply, onChange } = e

  e.apply = () => {
    /// ...
  }
  e.setFragmentData = () => {
    /// ...
  }
  e.insertData = () => {
    /// ...
  }
  e.onChange = () => {
    /// ...
  }

  return e
}
Copy the code

slate-history

SLATE -history SLATE plugin, provides undo and redo functions for the editor.

History

Use redos and undos arrays to store objects for all the underlying Operation commands in the editor.

/// History.ts
export interface History {
  redos: Operation[][]
  undos: Operation[][]
}
Copy the code

HistoryEditor

Editor object with history capability that has methods to manipulate history.

/// HistoryEditor.ts
export const HistoryEditor = {
  / / /...
}
Copy the code

withHistory

The Slate editor plug-in uses undos and Redos stacks to track the editor’s actions. It implements the redo and undo methods of the editor and overwrites the Apply method.

/// with-history.ts

export const withHistory = <T extends Editor>(editor: T) => {
  const e = editor as T & HistoryEditor
  const { apply } = e
  e.history = { undos: [], redos: [] }

  e.redo = () => {
    /// ...
  }

  e.undo = () => {
    /// ...
  }

  e.apply = (op: Operation) => {
    /// ...
  }

  return e
}
Copy the code

slate-hyperscript

Slate-hyperscript is a hyperscript tool that uses JSX to write SLATE documents

Plug-in mechanism

Slate’s plug-in is simply a function that returns an instance of the Editor, in which the editor behavior is modified by overwriting the editor instance methods. Just call the plug-in function when you create the editor instance.

import { Editor } from 'slate'

const myPlugin = (editor: Editor) = > {
  // This overrides some of the editor's methods to return an editor instance
  editor.apply = () = > {}
  return editor
}

export default myPlugin
Copy the code
import { createEditor } from 'slate'
import myPlugin from './myPlugin'

const editor = myPlugin(createEditor())
Copy the code

This gives the plugin complete control over the editor’s behavior, as Slate’s introduction explains

Slate is a fully customizable rich text editor framework.

Apply colours to a drawing mechanism

Rendering principle

Slate’s document data is a dom-like node tree. Slate-react recursively generates an array of children with two types of components: Element and Text. Finally, Raect renders the components of the Children array onto the page as follows.

  1. Of the editor instancechildrenattribute
/// Slate.tsx

export const Slate = (props: {
  ///... }) => { const { editor, children, onChange, value, ... rest } = props const context: [ReactEditor] = useMemo(() => { // Set the children property of the editor instance to value editor.children = value ///. }, []/ / /... }Copy the code
  1. EditableComponent passeditorInstance toChildren
/// Editable.tsx

export const Editable = (props: EditableProps) = > {
  // Get an instance of editor
  const editor = useSlate()
  / / /...
  return (
    <ReadOnlyContext.Provider value={readOnly}>
      <Component>
        <Children/ / will beeditorPassed to theChildrencomponentnode={editor}
        />
      </Component>
    </ReadOnlyContext.Provider>)}Copy the code
  1. ChildrenGenerate the render array and pass it to the React render component.
/// Children.tsx

const Children = (props: {
  ///... = > {}) ///.const children = []
  //Iterate through the Children array on the Editor instancefor (let i = 0; i < node.children.length; i++) {
    //Determine whether the data is Element or Textif (Element.isElement(n)) {
      children.push(<ElementComponent />)
    } else {
      children.push(<TextComponent />)
    }
  }

  return <React.Fragment>{children}</React.Fragment>
}
Copy the code

Suppose you have the following data

;[
  {
    type: 'paragraph'.children: [{text: 'A line of text in a paragraph.',},],}, {type: 'paragraph'.children: [{text: 'Another line of text in a paragraph.',},],},]Copy the code

The page displays as

Custom rendering

RenderElement and renderLeaf are passed to the Editable component to render elements and leaf nodes in a custom manner.

const Leaf = (props) = > {
  let { attributes, children, leaf } = props
  // Set the HTML tag according to the attribute value
  if (leaf.bold) {
    children = <strong>{children}</strong>
  }

  return <span {. attributes} >{children}</span>
}

const Element = (props) = > {
  const { element } = props
  // Returns the component by type
  switch (element.type) {
    case 'custom-type':
      return <CustomElement {. props} / >
    default:
      return <DefaultElement {. props} / >}}const renderLeaf = props= > <Leaf {. props} / >
const renderElement = props= > <Element {. props} / >

<Slate>
  <Editable// Pass custom rendering functionsrenderLeaf={renderLeaf}
    renderElement={renderElement}
  />
</Slate>
Copy the code

Trigger rendering

The withReact plugin overwrites the editor’s onChange method, calling the onContextChange function and executing setKey(key + 1) to trigger react re-rendering.

/// slate.tsx

export const Slate = () = > {
  const [key, setKey] = useState(0)

  const onContextChange = useCallback(() = > {
    onChange(editor.children)
    // Set key + 1 to trigger React rerendering
    setKey(key + 1)
  }, [key, onChange])

  // Sets onContextChange
  EDITOR_TO_ON_CHANGE.set(editor, onContextChange)
}
Copy the code
/// with.react.ts

export const withReact = <T extends Editor>(editor: T) => {const onContextChange = () => {reactdom.unstabLE_batchedupDates (() => {const onContextChange = Editor_to_on_change.get (e) if (onContextChange) {// Execute onContextChange for key + 1 onContextChange()} onChange()})} return e }Copy the code

Practical example

A basic rich text editor

Import dependencies, create<MyEditor />component

import { createEditor } from 'slate'
import React, { useMemo, useState } from 'react'
import { Slate, Editable, withReact } from 'slate-react'

const MyEditor = () = > {
  return null
}

export default MyEditor
Copy the code

Create an editor objecteditorAnd document datavalueAnd passed to the<Slate />

// ...

const MyEditor = () = > {
  const [value, setValue] = useState([])
  const editor = useMemo(() = > withReact(createEditor()), [])

  return (
    // The Slate component holds the state of the editor. The purpose is to share the state so that other components, such as toolbars, can also get the state of the editor.
    <Slate
      editor={editor}
      value={value}
      onChange={(value)= > setValue(value)}
    ></Slate>)}Copy the code

use<Editable />Render the main area of the editor

// ...

const MyEditor = () = > {
  const [value, setValue] = useState([])
  const editor = useMemo(() = > withReact(createEditor()), [])

  return (
    <Slate editor={editor} value={value} onChange={(value)= >SetValue (value)}> // The Editable component is the actual rendering area of the editor where users interact<Editable
        style={{
          width: 500.height: 300.padding: 20.border: '1px solid grey'}}placeholder="This is placeholder..."
      />
    </Slate>)}Copy the code

Add the editor’s default value, and this line of text will appear on the page

/ / /...

// The value of the editor is an array of objects from which SLATE generates the data model, which SLATE renders
const initialValue = [
  {
    type: 'paragraph'.children: [{text: 'A line of text in a paragraph.',},],},]const MyEditor = () = > {
  // Initialize the editor value as initialValue
  const [value, setValue] = useState(initialValue)
  const editor = useMemo(() = > withReact(createEditor()), [])

  return (
    <Slate editor={editor} value={value} onChange={(value)= > setValue(value)}>
      <Editable
        style={{
          width: 500.height: 300.padding: 20.border: '1px solid grey'}}placeholder="This is placeholder..."
      />
    </Slate>)}Copy the code

Create toolbar components and add bold, italic, and underline buttons.

const MyToolbar = () = > {
  return (
    <div
      style={{
        width: 500.display: 'flex',
        padding: '10px 20px',
        alignItems: 'center',
        margin: '0 auto',
        marginTop: 50.border: '1px solid grey'}} >
      <button
        style={{
          marginRight: 20,}}onMouseDown={(event)= > {
          event.preventDefault()
        }}
      >
        B
      </button>

      <button
        style={{
          marginRight: 20,}}onMouseDown={(event)= > {
          event.preventDefault()
        }}
      >
        I
      </button>

      <button
        style={{
          marginRight: 20,}}onMouseDown={(event)= > {
          event.preventDefault()
        }}
      >
        U
      </button>
    </div>)}/ / /...

<Slate editor={editor} value={value} onChange={(value) = > setValue(value)}>
  // Use it here
  <MyToolbar />
  <Editable
    style={{
      width: 500.height: 300.padding: 20.margin: '0 auto',
      border: '1px solid grey',
      borderTopWidth: 0,}}placeholder="This is placeholder..."
  />
</Slate>
Copy the code

Set bold, italic, underline render styles, passrenderLeafFunction toEditable.

/// MyEditor.jsx

// Define how specific styles are rendered
const Leaf = (props) = > {
  let { attributes, children, leaf } = props

  if (leaf.bold) {
    children = <strong>{children}</strong>
  }

  if (leaf.italic) {
    children = <i>{children}</i>
  }

  if (leaf.underline) {
    children = <u>{children}</u>
  }

  return <span {. attributes} >{children}</span>
}

const MyEditor = () = > {
  / / /...

  //
  const renderLeaf = useCallback((props) = > {
    return <Leaf {. props} / >
  }, [])

  return (
    <Slate editor={editor} value={value} onChange={(value)= > setValue(value)}>
      <MyToolbar editor={editor} />

      <Editable
        //
        renderLeaf={renderLeaf}
      />
    </Slate>)}Copy the code

Adds a method to transform node properties on the toolbar, called when clicked.

/// MyToolbar.jsx

import React from 'react'
import { Text, Editor } from 'slate'
import { Transforms } from 'slate'

// Check whether the attribute value of the node is true
const isFormatActive = (editor, format) = > {
  const [match] = Editor.nodes(editor, {
    match: (n) = > n[format] === true.universal: true,})return!!!!! match }// Toggle property values according to style
const toggleFormat = (event, editor, format) = > {
  event.preventDefault()
  const isActive = isFormatActive(editor, format)

  Transforms.setNodes(
    editor,
    { [format]: isActive ? false : true },
    { match: (n) = > Text.isText(n), split: true})}const MyToolbar = ({ editor }) = > {
  return (
    <div
      style={{
        width: 500.display: 'flex',
        padding: '10px 20px',
        alignItems: 'center',
        margin: '0 auto',
        marginTop: 50.border: '1px solid grey'}} >
      <button
        style={{
          marginRight: 20}} // called on the click eventonClick={(event)= > {
          toggleFormat(event, editor, 'bold')
        }}
      >
        B
      </button>/ / /...</div>)}Copy the code

Create a custom tree element

Slate’s power lies in its extensibility. Here’s how to define a tree element.

Define tree elements

/// TreeElement.jsx

import React, { useState } from 'react'

const TreeElement = ({ attributes, children, element }) = > {
  const { checked, label } = element
  const [isChecked, setIsChecked] = useState(checked)

  const onChange = () = >{ setIsChecked(! isChecked) }return (
    <div {. attributes} >
      <p
        style={{
          display: 'flex',
          alignItems: 'center'}}contentEditable={false}
      >
        <input
          type="checkbox"
          style={{
            width: 20,}}checked={isChecked}
          onChange={onChange}
        />
        <label>{label}</label>
      </p>
      {isChecked ? <div style={{ paddingLeft: 20}} >{children}</div> : null}
    </div>)}Copy the code

willrenderElementMethod passed to<Editable />.

/ / /...
const Element = (props) = > {
  const { element } = props

  switch (element.type) {
    case 'tree-item':
      return <TreeElement {. props} / >
    default:
      return <DefaultElement {. props} / >}}/ / /...
const renderElement = useCallback((props) = > <Element {. props} / >[]),/ / /...
<Editable
  renderElement={renderElement}
/>
Copy the code

Add tree element data

const initialValue = [
  / / /...
  {
    type: 'tree-item'.checked: true.label: 'first level'.children: [{type: 'tree-item'.checked: false.label: 'second level'.children: [{type: 'tree-item'.label: 'third level'.checked: false.children: [{type: 'paragraph'.children: [{text: 'This is a tree item',},],},],},],},Copy the code

Create a plug-in that controls input

How do you create a Slate plug-in

To create awithEmojisThe plug-in

/// with-emojis.ts

import { ReactEditor } from 'slate-react'

const letterEmojis = {
  a: '🐜'.b: '🐻'.c: '🐱'.d: '🐶'.e: '🐘'.f: '🦊'.g: '🐦'.h: '🐵'.i: '🦄'.j: '🦋'.k: '🦀'.l: '🦁'.m: '🐭'.n: '🐮'.o: '🐋'.p: '🐼'.q: '🐧'.r: '🐰'.s: '🕷'.t: '🐯'.u: '🐍'.v: '🦖'.w: '🦕'.x: '🦛'.y: '🐳'.z: '🦓',}const withEmojis = (editor: ReactEditor) = > {
  const { insertText } = editor

  // Rewrite the insertText method of the editor
  editor.insertText = (text: string) = > {
    if (letterEmojis[text.toLowerCase()]) {
      text = letterEmojis[text]
    }
    // Execute the original insertText method
    insertText(text)
  }

  return editor
}

export default withEmojis
Copy the code

Use plug-ins when creating new editor objects

/// MyEditor.tsx

const editor = useMemo(() = > withEmojis(withReact(createEditor())), [])
Copy the code

Deficiency in

  • There is no official release yet, it is in Beta and the API is subject to change
  • React is currently the only render layer available, but it needs to be implemented in other frameworks
  • Data rendering separation requires complete control over user input behavior, otherwise data and rendering may be out of sync
  • Typesetting in browsers is impossible with Contenteditable
  • Insufficient support for Chinese input, see this link
  • Community-driven development, problems may not be fixed in time

conclusion

Slate is a well-designed rich text editor development framework with high scalability. If you need a rich text editor that you can quickly access and use, you can use ckeditor4, Tinymce, and UEditor that provide out-of-the-box functionality. If you’re developing a feature-rich editor that requires customization, Slate is your first choice.

reference

The evolution of open source rich text editor technology (2020 1024