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.
- Of the editor instance
children
attribute
/// 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
Editable
Component passeditor
Instance 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
Children
Generate 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 objecteditor
And document datavalue
And 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, passrenderLeaf
Function 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
willrenderElement
Method 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 awithEmojis
The 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