preface
- The final results
- Play online: Brilliant-js.com/
- GitHub address: github.com/brilliant-j…
I. Project background
It was a triumph of finches editor everyday, so love language, because it put typora moved to online, the real-time rendering Markdown really very practical and it’s too high, from the long term Chelsea has been a such idea: if which day I project need online editor, be sure to have a language finches.
That day has come, received a new business requirements, the need to introduce a online editor React, remember the last time in the company project, use the editor component or two years ago, it was a same good rich text editor, but does not support real-time Markdown, then I have no choice, now I’m going to Markdown of real-time rendering.
The so-called Markdown real-time rendering looks something like this (example from Wordfinch) :
But as the famous saying goes,
Nothing good comes easy.
After searching a number of open source editor components, the most discussed and common one is still the pure rich text editor, followed by the left and right column preview pure Markdown editor. Although there are still plenty of scenarios for these two modes, I believe that in today’s world of Markdown popularity, Real-time rendering schemes that combine Markdown with rich text are the editors of the future.
With an obsession with real-time rendering of language and an option for colleagues who also like real-time rendering of Markdown editors, the team decided to formally launch a project to develop a customized real-time Markdown rendering of React editor.
Two, technology selection
This is not to create an editor from scratch, but to encapsulate and customize functions in the popular lib editor. Common lib customization solutions include code-mirror, slate.js, quill.js, etc. Finally, SLate. js and draft.js are adopted. Both of them are in line with the react functional programming concept and rely on state to manage page data. If you are familiar with the React development mode, the learning cost is much lower in the encapsulation process. It is not friendly to Chinese input, sometimes the input method switch can cause a bug that causes the editor to crash (I don’t know if the official has fixed this bug for a long time now), so draft.js won out in the end.
Feasibility analysis
Can I achieve the Markdown real-time rendering mode I want with draft.js?
The design concept and characteristics of Draft.js:
- React Pro Son, also facebook open source, at least guarantees some robustness
draft.js
Is an editor infrastructure based on native Dom encapsulation- using
immutable
The type ofstate
To centrally manage all content in the editor - You can customize any information displayed on a page by matching block styles and inline styles patterns
Iv. Development practice
Use draft. js to encapsulate the editor, which is similar to the react development mode, and use immutable data structure to manage the top EditorState object. In order to facilitate undo/redo snapshot generation, the original EditorState object cannot be changed. You must return a new editorState object after each operation. The editorState property received by the Editor object is a controlled property, and you must change the editorState object in real time by passing an onChange method to the Editor component that changes the editorState.
1. Introduction of native components:
A basic draft.js component usage rule is shown in the following code:
import { Editor, EditorState } from 'draft-js'
import { useState } from 'react';
import 'draft-js/dist/Draft.css'
function App() {
const [editorState, setEditorState] = useState(() = > EditorState.createEmpty())
const onChange = (editorState) = > setEditorState(editorState)
return (
<div className="App">
<Editor editorState={ editorState } onChange={ onChange} ></Editor>
</div>
);
}
export default App;
Copy the code
You can’t see anything on the page. Try moving the cursor to the top line and clicking, and you’ll see a jumping cursor style:
2. The top-level state object editorState
EditorState is the most important object in draft.js and is used to store all the content and state of a rich text editor. This object is entered as a component property to the Editor component. Once the user does something, such as hitting a return, the Editor component’s onChange event is triggered. The onChange function returns a new EditorState instance, and the Editor receives this new input. Render the new content, so the simplest way to write it is as shown in the previous code.
What it contains:
(1) current ContentState (2) current SelectionState (3) All content decorators (4) Undo and redo stack (5) type of last change operation.Copy the code
EditorState is essentially immutable data, and you can call the toJS method to view the original data structure.
.function App() {
const [editorState, setEditorState] = useState(() = > EditorState.createEmpty())
const onChange = (editorState) = > setEditorState(editorState)
const logData = () = > {
// editorState is immutable data. The toJS method can be called to print the original data structure
console.log(editorState.toJS());
}
return (
<div className="App">
<Editor editorState={ editorState } onChange={ onChange } ></Editor>
<button onClick={logData}>The print information</button>
</div>); }...Copy the code
You can observeeditorState
The above attributes exist in the object, among which three more important attributes are:
currentContent
Is aContentState
Object that holds the content in the current editor, called a content description objectselection
Is aSelectionState
Object, which is the description object of the current selected stateredoStack
和undoStack
It’s the undo/redo stack, and it’s an array that holdsContentState
Type editor state
3. Content description object contentState
ContentState
Is also an Immutable Record object, which holds all the contents of the editor and two selected states before and after rendering. Can be achieved byEditorState.getCurrentContent()
To get the currentContentState
, also called.toJS()
Then print it out and have a look:
4. Block description object blockMap
- Is a very important object that holds all current blocks of content in the editor, with the unique key of the block as the key. The structure type of each block is
ContentBlock
Represents each independent block in the content of an editor, that is, a visually independent piece.
- The specific structure of each content block is (please refer to the above figure) :
- Key: unique key of a block
- Type: indicates the block type
- Text: indicates the text of the block
- The key in the block corresponds to the DOM structure attribute of the current block
data-offset-key
After that, the operation data is determined by the key to determine the content of the block to be operated on
- EntityMap: Stores information such as image link address, link text and link address
5. Add controls
Once the core API is clear, we can further encapsulate it. Since we are going to make a Markdown real-time rendering + rich text support editor, the next step is to add controls like any other rich text editor.
Controls fall into two categories: inline style controls and block-level style controls.
Bold, highlighted, italic, inline code like this is implemented using inline style controls.
Such titles, unordered lists, and references are implemented by block-style controls.
(1) Set inline style controls
First define the control type and title
const controls = [
{ label: "B".item: "BOLD".type: "inline" },
{ label: "I".item: "ITALIC".type: "inline" },
{ label: "RED".item: "RED".type: "inline"},];Copy the code
Handling logic for in-line style switching, referencing draft.js internalRichUtils.toggleInlineStyle
, the principle is to obtain the currently selected content and record the offset, and the offset and style information corresponding to the selected content (style
) to join theinlineStyleRanges
In the.The code implementation is as follows:
import { RichUtils } from 'draft-js'.// Handle inline styles
const toggleInlineStyle = (inlineStyle: string) = >{ setEditorState(RichUtils.toggleInlineStyle(editorState, inlineStyle)); }; . <div className='App'>
<div>
{controls.map((item) => {
return (
<button
onMouseDown={(e)= > {
e.preventDefault();
toggleInlineStyle(item.type);
}}
>
{item.label}
</button>
);
}}
</div>
<Editor editorState={editorState} onChange={onChange}></Editor>
</div>
Copy the code
- Note that for styles added to linestyleranges to work, you also need to set up the mapping rules for inline styles
editorStyleMap
To join theEditor
Properties of the editor component
// Customize inline styles
const editorStyleMap: DraftStyleMap = {
/ / font
bold: {
fontWeight: "bold",},italic: {
fontStyle: "italic",},red: {
color: "red",}}; . <Editor customStyleMap={editorStyleMap} editorState={editorState} onChange={onChange}></Editor>Copy the code
At this point, the inline style control function has been implemented:
Draft.js’s open customStyleMap API is extremely freeform, allowing you to implement any inline styles you want.
(2) Set the block-level control style
The block-level style setting is a little more straightforward, just change the type of the block where the current row resides.
In the Draft. Jsblock
的 type
有 unstyled
.paragraph
.header-one
.atomic
… The equivalent is in the draft.js documentationheader-one
The type corresponds to theta<h1 />
Elements,blockquote
The corresponding is<pre></pre>
Elements, and so on… We also chose it to implement the function of multi-level titles and code blocks.
Introducing block-level code controls:
const controls = [
{ label: "H1".type: "header-one".category: "block" },
{ label: "H2".type: "header-two".category: "block" },
{ label: "H3".type: "header-three".category: "block" },
{ label: "< >".type: "blockquote".category: "block"},];<div className='App'>
<div>
{controls.map((item) => {
return (
<button
onMouseDown={(e)= > {
e.preventDefault();
toggleBlockType(item.type);
}}
>
{item.label}
</button>); }}</div>
<Editor customStyleMap={editorStyleMap} editorState={editorState} onChange={onChange}></Editor>
</div>
Copy the code
Similarly, block-level style Settings rely on RichUtils’s toggleBlockType API, which internally toggles the type value of the block in the cursor line.
// Handle block-level styles
const toggleBlockType = (blockType: string) = > {
onChange(RichUtils.toggleBlockType(editorState, blockType));
};
Copy the code
Also, you can use blockRenderFn to customize any block-level styles you want.
6.Markdown rendering in real time
- The next step, the Markdown live preview, is to give the whole editor its soul
(1) Analysis of ideas
For example, a # is a #, not a title. In Markdown syntax, the # is followed by a space, and the content after the space is a level 1 title. We need to do two things: 1. 2, delete # space; 3. Return the new editorState
At this point, it would be nice if the draft.js editor could expose an event that listens for user input and passes it back to us. For simplicity’s sake, this event should be called handleBeforeInput. Guess what, there is an event called handleBeforeInput, one of the properties in the draft.js editor. It is of type function and is fired during user input. The parameter is what the user typed.
Once you take the user’s input, you can match it with the corresponding Markdown syntax
The new editorState is returned after processing
Then render in real time!
Description: forgive me can’t intercept code completion, is only simple example above, the actual process is very complex, want to consider a variety of different Markdown grammar is respective processing, but also accurate fixed cursor position in the operation, cursor under complex scene management should be the editor of the most difficult in the process of encapsulation, refer to below open source address source further research
Five, some problems and solution process
1. Lack of development resources
- Is the biggest a little scene: draft official documentation concise, organized, to find the API is very convenient, the biggest drawback is too simple, some core API is a phrase to describe, neither demo case, also have no a system structure that fog, learn up very difficult, in many situations, a bunch of apis don’t know which one to use.
- Solution:
- First climb a ladder to watch about
draft.js
React Developer Conference talk, learn about design concepts - Then look for the open source package editor and learn the packaging ideas inside
- I also referred to some videos and began to comb draft.jsAPI in the form of mind mapping. I also tested and learned at the minimum scale and organized many articles introducing core API for future reference.
- First climb a ladder to watch about
2. Bracket matching during code input
-
Scenario: The user wraps a line at the cursor position in the code block with a non-closing {or [or ((curly braces, curly braces, curly braces), requiring two Spaces to indent, counting the number of non-closing parentheses, and counting the amount of indentation
-
Solution:
- It was handled in a very inelegant way, with a lot of redundant code
- And there’s a bug. If it’s closed before, you can’t tell
- Later brush algorithm, brush to accidentallyMatch valid parenthesesThe collection of current business scenarios, the refactoring algorithm using the stack idea is elegantly solved
- At that time, it was the scattered brush algorithm. After this problem, I deeply realized that the algorithm problem was of great help to the daily business coding, so I began to make my own road map according to my own ideas and brush the problem systematically
- It was handled in a very inelegant way, with a lot of redundant code
3. Function test of picture paste method
- Scenario: During development, the automatic uploading function of pasted images needs to be tested
- Solution: use KOA2 to build a minimalist image resource hosting server, with the help of
multer
The module performs middleware processing, uploads pictures and returns the resource address, which is deployed to its own cloud server for students in the project team to share. Since the test pictures do not need persistence, this project also adopts the node timing module to clear data at zero o ‘clock every day, which does not occupy the server memory.
4. Cursor management
At the beginning, the bug caused by the misplaced cursor really made me bald, but after a lot of work, I gradually found a pattern.
6. Achievements display and summary
The team’s open source Brilliant editor, packaged with draft.js and completely open source, is available at:Github.com/brilliant-j…, has been publishedNPM warehouseSupport zero configuration quick reference, wySIWYG editing mode, to give you the ultimate input experience, welcome to watch and use, but also welcome everyone to participate in the exchange and construction, the project will be long-term maintenance, gradually improve the new function, please continue to follow ✨
Experience online at Brilliant-js.com and issue exchange if you have questions.
Vii. Reference materials
- Draft.js Official document
- React.js Conf 2016 – Isaac Salier-Hellendag – Rich Text Editing with React – YouTube
- Rich Text Editing with Draft.js — Nikolaus Graf — YouTube
- The draft.js rich text editor is introduced from the implementation of the function of inserting pictures
This article is participating in the “Nuggets 2021 Spring Recruitment Campaign”, click to see the details of the campaign