Constructing A Wealthy Textual content Editor (WYSIWYG) From Scratch

No Comments

In recent times, the sphere of Content material Creation and Illustration on Digital platforms has seen an enormous disruption. The widespread success of merchandise like Quip, Google Docs and Dropbox Paper has proven how corporations are racing to construct one of the best expertise for content material creators within the enterprise area and looking for revolutionary methods of breaking the normal moulds of how content material is shared and consumed. Making the most of the large outreach of social media platforms, there’s a new wave of unbiased content material creators utilizing platforms like Medium to create content material and share it with their viewers.

As so many individuals from completely different professions and backgrounds attempt to create content material on these merchandise, it’s necessary that these merchandise present a performant and seamless expertise of content material creation and have groups of designers and engineers who develop some degree of area experience over time on this house. With this text, we attempt to not solely lay the muse of constructing an editor but additionally give the readers a glimpse into how little nuggets of functionalities when introduced collectively can create an amazing person expertise for a content material creator.

Understanding The Doc Construction

Earlier than we dive into constructing the editor, let’s take a look at how a doc is structured for a Wealthy Textual content Editor and what are the several types of knowledge buildings concerned.

Doc Nodes

Doc nodes are used to signify the contents of the doc. The frequent kinds of nodes {that a} rich-text doc may comprise are paragraphs, headings, photos, movies, code-blocks and pull-quotes. A few of these might comprise different nodes as kids inside them (e.g. Paragraph nodes comprise textual content nodes inside them). Nodes additionally maintain any properties particular to the article they signify which can be wanted to render these nodes contained in the editor. (e.g. Picture nodes comprise a picture src property, Code-blocks might comprise a language property and so forth).

There are largely two kinds of nodes that signify how they need to be rendered –

Block Nodes (analogous to HTML idea of Block-level components) which can be every rendered on a brand new line and occupy the obtainable width. Block nodes may comprise different block nodes or inline nodes inside them. An statement right here is that the top-level nodes of a doc would at all times be block nodes.
Inline Nodes (analogous to HTML idea of Inline components) that begin rendering on the identical line because the earlier node. There are some variations in how inline components are represented in several modifying libraries. SlateJS permits for inline components to be nodes themselves. DraftJS, one other standard Wealthy Textual content Enhancing library, allows you to use the idea of Entities to render inline components. Hyperlinks and Inline Pictures are examples of Inline nodes.
Void Nodes — SlateJS additionally permits this third class of nodes that we are going to use later on this article to render media.

If you wish to be taught extra about these classes, SlateJS’s documentation on Nodes is an effective place to start out.

Attributes

Just like HTML’s idea of attributes, attributes in a Wealthy Textual content Doc are used to signify non-content properties of a node or it’s kids. As an illustration, a textual content node can have character-style attributes that inform us whether or not the textual content is daring/italic/underlined and so forth. Though this text represents headings as nodes themselves, one other technique to signify them might be that nodes have paragraph-styles (paragraph & h1-h6) as attributes on them.

Beneath picture provides an instance of how a doc’s construction (in JSON) is described at a extra granular degree utilizing nodes and attributes highlighting a number of the components within the construction to the left.

A number of the issues price calling out right here with the construction are:

Textual content nodes are represented as {textual content: ‘textual content content material’}
Properties of the nodes are saved straight on the node (e.g. url for hyperlinks and caption for photos)
SlateJS-specific illustration of textual content attributes breaks the textual content nodes to be their very own nodes if the character type modifications. Therefore, the textual content ‘Duis aute irure dolor’ is a textual content node of it’s personal with daring: true set on it. Similar is the case with the italic, underline and code type textual content on this doc.

Areas And Choice

When constructing a wealthy textual content editor, it’s essential to have an understanding of how probably the most granular a part of a doc (say a personality) might be represented with some type of coordinates. This helps us navigate the doc construction at runtime to grasp the place within the doc hierarchy we’re. Most significantly, location objects give us a technique to signify person choice which is kind of extensively used to tailor the person expertise of the editor in actual time. We’ll use choice to construct our toolbar later on this article. Examples of those might be:

Is the person’s cursor at the moment inside a hyperlink, possibly we must always present them a menu to edit/take away the hyperlink?
Has the person chosen a picture? Perhaps we give them a menu to resize the picture.
If the person selects sure textual content and hits the DELETE button, we decide what person’s chosen textual content was and take away that from the doc.

SlateJS’s doc on Location explains these knowledge buildings extensively however we undergo them right here rapidly as we use these phrases at completely different cases within the article and present an instance within the diagram that follows.

Path
Represented by an array of numbers, a path is the way in which to get to a node within the doc. As an illustration, a path [2,3] represents the third youngster node of the 2nd node within the doc.
Level
Extra granular location of content material represented by path + offset. As an illustration, some extent of {path: [2,3], offset: 14} represents the 14th character of the third youngster node contained in the 2nd node of the doc.
Vary
A pair of factors (referred to as anchor and focus) that signify a variety of textual content contained in the doc. This idea comes from Net’s Choice API the place anchor is the place person’s choice started and focus is the place it ended. A collapsed vary/choice denotes the place anchor and focus factors are the identical (consider a blinking cursor in a textual content enter for example).

For instance let’s say that the person’s choice in our above doc instance is ipsum:

The person’s choice might be represented as:

{
anchor: {path: [2,0], offset: 5}, /0th textual content node contained in the paragraph node which itself is index 2 within the doc/
focus: {path: [2,0], offset: 11}, // house + ‘ipsum’
}`

Setting Up The Editor

On this part, we’re going to arrange the appliance and get a fundamental rich-text editor going with SlateJS. The boilerplate software could be create-react-app with SlateJS dependencies added to it. We’re constructing the UI of the appliance utilizing elements from react-bootstrap. Let’s get began!

Create a folder referred to as wysiwyg-editor and run the under command from contained in the listing to arrange the react app. We then run a yarn begin command that ought to spin up the native internet server (port defaulting to 3000) and present you a React welcome display screen.

npx create-react-app .
yarn begin

We then transfer on so as to add the SlateJS dependencies to the appliance.

yarn add slate slate-react

slate is SlateJS’s core bundle and slate-react contains the set of React elements we’ll use to render Slate editors. SlateJS exposes some extra packages organized by performance one would possibly take into account including to their editor.

We first create a utils folder that holds any utility modules we create on this software. We begin with creating an ExampleDocument.js that returns a fundamental doc construction that accommodates a paragraph with some textual content. This module appears like under:

const ExampleDocument = [
{
type: “paragraph”,
children: [
{ text: “Hello World! This is my paragraph inside a sample document.” },
],
},
];

export default ExampleDocument;

We now add a folder referred to as elements that can maintain all our React elements and do the next:

Add our first React part Editor.js to it. It solely returns a div for now.
Replace the App.js part to carry the doc in its state which is initialized to our ExampleDocument above.
Render the Editor contained in the app and move the doc state and an onChange handler right down to the Editor so our doc state is up to date because the person updates it.
We use React bootstrap’s Nav elements so as to add a navigation bar to the appliance as properly.

App.js part now appears like under:

import Editor from ‘./elements/Editor’;

perform App() {
const [document, updateDocument] = useState(ExampleDocument);

return (
<>
<Navbar bg=”darkish” variant=”darkish”>
<Navbar.Model href=”#”>
<img
alt=””
src=”/app-icon.png”
width=”30″
peak=”30″
className=”d-inline-block align-top”
/>{” “}
WYSIWYG Editor
</Navbar.Model>
</Navbar>
<div className=”App”>
<Editor doc={doc} onChange={updateDocument} />
</div>
</>
);

Contained in the Editor part, we then instantiate the SlateJS editor and maintain it inside a useMemo in order that the article doesn’t change in between re-renders.

// dependencies imported as under.
import { withReact } from “slate-react”;
import { createEditor } from “slate”;

const editor = useMemo(() => withReact(createEditor()), []);

createEditor provides us the SlateJS editor occasion which we use extensively by means of the appliance to entry picks, run knowledge transformations and so forth. withReact is a SlateJS plugin that provides React and DOM behaviors to the editor object. SlateJS Plugins are Javascript features that obtain the editor object and fix some configuration to it. This permits internet builders so as to add configurations to their SlateJS editor occasion in a composable method.

We now import and render <Slate /> and <Editable /> elements from SlateJS with the doc prop we get from App.js. Slate exposes a bunch of React contexts we use to entry within the software code. Editable is the part that renders the doc hierarchy for modifying. Total, the Editor.js module at this stage appears like under:

import { Editable, Slate, withReact } from “slate-react”;

import { createEditor } from “slate”;
import { useMemo } from “react”;

export default perform Editor({ doc, onChange }) {
const editor = useMemo(() => withReact(createEditor()), []);
return (
<Slate editor={editor} worth={doc} onChange={onChange}>
<Editable />
</Slate>
);
}

At this level, we’ve mandatory React elements added and the editor populated with an instance doc. Our Editor must be now arrange permitting us to sort in and alter the content material in actual time — as within the screencast under.

Character Types

Just like renderElement, SlateJS provides out a perform prop referred to as renderLeaf that can be utilized to customise rendering of the textual content nodes (Leaf referring to textual content nodes that are the leaves/lowest degree nodes of the doc tree). Following the instance of renderElement, we write an implementation for renderLeaf.

export default perform useEditorConfig(editor) {
return { renderElement, renderLeaf };
}

// …
perform renderLeaf({ attributes, kids, leaf }) {
let el = <>{kids}</>;

if (leaf.daring) {
el = <robust>{el}</robust>;
}

if (leaf.code) {
el = <code>{el}</code>;
}

if (leaf.italic) {
el = <em>{el}</em>;
}

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

return <span {…attributes}>{el}</span>;
}

An necessary statement of the above implementation is that it permits us to respect HTML semantics for character kinds. Since renderLeaf provides us entry to the textual content node leaf itself, we will customise the perform to implement a extra personalized rendering. As an illustration, you might need a technique to let customers select a highlightColor for textual content and test that leaf property right here to connect the respective kinds.

We now replace the Editor part to make use of the above, the ExampleDocument to have a couple of textual content nodes within the paragraph with combos of those kinds and confirm that they’re rendered as anticipated within the Editor with the semantic tags we used.

# src/elements/Editor.js

const { renderElement, renderLeaf } = useEditorConfig(editor);

return (

<Editable renderElement={renderElement} renderLeaf={renderLeaf} />
);

# src/utils/ExampleDocument.js

{
sort: “paragraph”,
kids: [
{ text: “Hello World! This is my paragraph inside a sample document.” },
{ text: “Bold text.”, bold: true, code: true },
{ text: “Italic text.”, italic: true },
{ text: “Bold and underlined text.”, bold: true, underline: true },
{ text: “variableFoo”, code: true },
],
},

Including A Toolbar

Let’s start by including a brand new part Toolbar.js to which we add a couple of buttons for character kinds and a dropdown for paragraph kinds and we wire these up later within the part.

const PARAGRAPH_STYLES = [“h1”, “h2”, “h3”, “h4”, “paragraph”, “multiple”];
const CHARACTER_STYLES = [“bold”, “italic”, “underline”, “code”];

export default perform Toolbar({ choice, previousSelection }) {
return (
<div className=”toolbar”>
{/* Dropdown for paragraph kinds */}
<DropdownButton
className={“block-style-dropdown”}
disabled={false}
id=”block-style”
title={getLabelForBlockStyle(“paragraph”)}
>
{PARAGRAPH_STYLES.map((blockType) => (
<Dropdown.Merchandise eventKey={blockType} key={blockType}>
{getLabelForBlockStyle(blockType)}
</Dropdown.Merchandise>
))}
</DropdownButton>
{/* Buttons for character kinds */}
{CHARACTER_STYLES.map((type) => (
<ToolBarButton
key={type}
icon={<i className={`bi ${getIconForButton(type)}`} />}
isActive={false}
/>
))}
</div>
);
}

perform ToolBarButton(props) {
const { icon, isActive, …otherProps } = props;
return (
<Button
variant=”outline-primary”
className=”toolbar-btn”
energetic={isActive}
{…otherProps}
>
{icon}
</Button>
);
}

We summary away the buttons to the ToolbarButton part that could be a wrapper across the React Bootstrap Button part. We then render the toolbar above the Editable inside Editor part and confirm that the toolbar reveals up within the software.

Listed below are the three key functionalities we want the toolbar to help:

When the person’s cursor is in a sure spot within the doc and so they click on one of many character type buttons, we have to toggle the type for the textual content they might sort subsequent.
When the person selects a variety of textual content and click on one of many character type buttons, we have to toggle the type for that particular part.
When the person selects a variety of textual content, we need to replace the paragraph-style dropdown to mirror the paragraph-type of the choice. In the event that they do choose a distinct worth from the choice, we need to replace the paragraph type of all the choice to be what they chose.

Let’s take a look at how these functionalities work on the Editor earlier than we begin implementing them.

Including A Hyperlink Button To The Toolbar

Let’s add a Hyperlink Button to the toolbar that permits the person to do the next:

Deciding on some textual content and clicking on the button converts that textual content right into a hyperlink
Having a blinking cursor (collapsed choice) and clicking the button inserts a brand new hyperlink there
If the person’s choice is inside a hyperlink, clicking on the button ought to toggle the hyperlink — that means convert the hyperlink again to textual content.

To construct these functionalities, we want a method within the toolbar to know if the person’s choice is inside a hyperlink node. We add a util perform that traverses the degrees in upward route from the person’s choice to discover a hyperlink node if there’s one, utilizing Editor.above helper perform from SlateJS.

# src/utils/EditorUtils.js

export perform isLinkNodeAtSelection(editor, choice) {
if (choice == null) {
return false;
}

return (
Editor.above(editor, {
at: choice,
match: (n) => n.sort === “hyperlink”,
}) != null
);
}

Now, let’s add a button to the toolbar that’s in energetic state if the person’s choice is inside a hyperlink node.

# src/elements/Toolbar.js

return (
<div className=”toolbar”>

{/* Hyperlink Button */}
<ToolBarButton
isActive={isLinkNodeAtSelection(editor, editor.choice)}
label={<i className={`bi ${getIconForButton(“hyperlink”)}`} />}
/>
</div>
);

If we had to do that by ourselves, we’d have to determine the vary of choice and create three new nodes (textual content, hyperlink, textual content) that substitute the unique textual content node. SlateJS has a helper perform referred to as Transforms.wrapNodes that does precisely this — wrap nodes at a location into a brand new container node. We even have a helper obtainable for the reverse of this course of — Transforms.unwrapNodes which we use to take away hyperlinks from chosen textual content and merge that textual content again into the textual content nodes round it. With that, toggleLinkAtSelection has the under implementation to insert a brand new hyperlink at an expanded choice.

# src/utils/EditorUtils.js

export perform toggleLinkAtSelection(editor) {
if (!isLinkNodeAtSelection(editor, editor.choice)) {
const isSelectionCollapsed =
Vary.isCollapsed(editor.choice);
if (isSelectionCollapsed) {
Transforms.insertNodes(
editor,
{
sort: “hyperlink”,
url: ‘#’,
kids: [{ text: ‘link’ }],
},
{ at: editor.choice }
);
} else {
Transforms.wrapNodes(
editor,
{ sort: “hyperlink”, url: ‘#’, kids: [{ text: ” }] },
{ break up: true, at: editor.choice }
);
}
} else {
Transforms.unwrapNodes(editor, {
match: (n) => Component.isElement(n) && n.sort === “hyperlink”,
});
}
}

If the choice is collapsed, we insert a brand new node there with Rework.insertNodes that inserts the node on the given location within the doc. We wire this perform up with the toolbar button and may now have a method so as to add/take away hyperlinks from the doc with the assistance of the hyperlink button.

# src/elements/Toolbar.js
<ToolBarButton

isActive={isLinkNodeAtSelection(editor, editor.choice)}
onMouseDown={() => toggleLinkAtSelection(editor)}
/>

If the textual content ’ABCDE’ was the primary textual content node of the primary paragraph within the doc, our level values could be —

cursorPoint = { path: [0,0], offset: 5}
startPointOfLastCharacter = { path: [0,0], offset: 4}

If the final character was an area, we all know the place it began — startPointOfLastCharacter.Let’s transfer to step-2 the place we transfer backwards character-by-character till both we discover one other house or the beginning of the textual content node itself.

if (lastCharacter !== ” “) {
return;
}

let finish = startPointOfLastCharacter;
begin = Editor.earlier than(editor, finish, {
unit: “character”,
});

const startOfTextNode = Editor.level(editor, currentNodePath, {
edge: “begin”,
});

whereas (
Editor.string(editor, Editor.vary(editor, begin, finish)) !== ” ” &&
!Level.isBefore(begin, startOfTextNode)
) {
finish = begin;
begin = Editor.earlier than(editor, finish, { unit: “character” });
}

const lastWordRange = Editor.vary(editor, finish, startPointOfLastCharacter);
const lastWord = Editor.string(editor, lastWordRange);

Here’s a diagram that reveals the place these completely different factors level to as soon as we discover the final phrase entered to be ABCDE.

Observe that begin and finish are the factors earlier than and after the house there. Equally, startPointOfLastCharacter and cursorPoint are the factors earlier than and after the house person simply inserted. Therefore [end,startPointOfLastCharacter] provides us the final phrase inserted.

We log the worth of lastWord to the console and confirm the values as we sort.

Now let’s give attention to caption-editing. The way in which we would like this to be a seamless expertise for the person is that after they click on on the caption, we present a textual content enter the place they’ll edit the caption. In the event that they click on outdoors the enter or hit the RETURN key, we deal with that as a affirmation to use the caption. We then replace the caption on the picture node and swap the caption again to learn mode. Let’s see it in motion so we’ve an thought of what we’re constructing.

Let’s replace our Picture part to have a state for caption’s read-edit modes. We replace the native caption state because the person updates it and after they click on out (onBlur) or hit RETURN (onKeyDown), we apply the caption to the node and swap to learn mode once more.

const Picture = ({ attributes, kids, aspect }) => {
const [isEditingCaption, setEditingCaption] = useState(false);
const [caption, setCaption] = useState(aspect.caption);

const applyCaptionChange = useCallback(
(captionInput) => {
const imageNodeEntry = Editor.above(editor, {
match: (n) => n.sort === “picture”,
});
if (imageNodeEntry == null) {
return;
}

if (captionInput != null) {
setCaption(captionInput);
}

Transforms.setNodes(
editor,
{ caption: captionInput },
{ at: imageNodeEntry[1] }
);
},
[editor, setCaption]
);

const onCaptionChange = useCallback(
(occasion) => {
setCaption(occasion.goal.worth);
},
[editor.selection, setCaption]
);

const onKeyDown = useCallback(
(occasion) => {
if (!isHotkey(“enter”, occasion)) {
return;
}

applyCaptionChange(occasion.goal.worth);
setEditingCaption(false);
},
[applyCaptionChange, setEditingCaption]
);

const onToggleCaptionEditMode = useCallback(
(occasion) => {
const wasEditing = isEditingCaption;
setEditingCaption(!isEditingCaption);
wasEditing && applyCaptionChange(caption);
},
[editor.selection, isEditingCaption, applyCaptionChange, caption]
);

return (

{isEditingCaption ? (
<Kind.Management
autoFocus={true}
className={“image-caption-input”}
dimension=”sm”
sort=”textual content”
defaultValue={aspect.caption}
onKeyDown={onKeyDown}
onChange={onCaptionChange}
onBlur={onToggleCaptionEditMode}
/>
) : (
<div
className={“image-caption-read-mode”}
onClick={onToggleCaptionEditMode}
>
{caption}
</div>
)}
</div>

With that, the caption modifying performance is full. We now transfer to including a method for customers to add photos to the editor. Let’s add a toolbar button that lets customers choose and add a picture.

# src/elements/Toolbar.js

const onImageSelected = useImageUploadHandler(editor, previousSelection);

return (
<div className=”toolbar”>
….
<ToolBarButton
isActive={false}
as={“label”}
htmlFor=”image-upload”
label={
<>
<i className={`bi ${getIconForButton(“picture”)}`} />
<enter
sort=”file”
id=”image-upload”
className=”image-upload-input”
settle for=”picture/png, picture/jpeg”
onChange={onImageSelected}
/>
</>
}
/>
</div>

As we work with picture uploads, the code may develop fairly a bit so we transfer the image-upload dealing with to a hook useImageUploadHandler that provides out a callback hooked up to the file-input aspect. We’ll talk about shortly about why it wants the previousSelection state.

Earlier than we implement useImageUploadHandler, we’ll arrange the server to have the ability to add a picture to. We setup an Specific server and set up two different packages — cors and multer that deal with file uploads for us.

yarn add categorical cors multer

We then add a src/server.js script that configures the Specific server with cors and multer and exposes an endpoint /add which we’ll add the picture to.

# src/server.js

const storage = multer.diskStorage({
vacation spot: perform (req, file, cb) {
cb(null, “./public/images/”);
},
filename: perform (req, file, cb) {
cb(null, file.originalname);
},
});

var add = multer({ storage: storage }).single(“photograph”);

app.publish(“/add”, perform (req, res) {
add(req, res, perform (err) {
if (err instanceof multer.MulterError) {
return res.standing(500).json(err);
} else if (err) {
return res.standing(500).json(err);
}
return res.standing(200).ship(req.file);
});
});

app.use(cors());
app.pay attention(port, () => console.log(`Listening on port ${port}`));

Now that we’ve the server setup, we will give attention to dealing with the picture add. When the person uploads a picture, it might be a couple of seconds earlier than the picture will get uploaded and we’ve a URL for it. Nevertheless, we do what to present the person instant suggestions that the picture add is in progress in order that they know the picture is being inserted within the editor. Listed below are the steps we implement to make this conduct work –

As soon as the person selects a picture, we insert a picture node on the person’s cursor place with a flag isUploading set on it so we will present the person a loading state.
We ship the request to the server to add the picture.
As soon as the request is full and we’ve a picture URL, we set that on the picture and take away the loading state.

Let’s start with step one the place we insert the picture node. Now, the difficult half right here is we run into the identical difficulty with choice as with the hyperlink button within the toolbar. As quickly because the person clicks on the Picture button within the toolbar, the editor loses focus and the choice turns into null. If we attempt to insert a picture, we don’t know the place the person’s cursor was. Monitoring previousSelection provides us that location and we use that to insert the node.

# src/hooks/useImageUploadHandler.js
import { v4 as uuidv4 } from “uuid”;

export default perform useImageUploadHandler(editor, previousSelection) {
return useCallback(
(occasion) => {
occasion.preventDefault();
const information = occasion.goal.information;
if (information.size === 0) {
return;
}
const file = information[0];
const fileName = file.identify;
const formData = new FormData();
formData.append(“photograph”, file);

const id = uuidv4();

Transforms.insertNodes(
editor,
{
id,
sort: “picture”,
caption: fileName,
url: null,
isUploading: true,
kids: [{ text: “” }],
},
{ at: previousSelection, choose: true }
);
},
[editor, previousSelection]
);
}

As we insert the brand new picture node, we additionally assign it an identifier id utilizing the uuid bundle. We’ll talk about in Step (3)’s implementation why we want that. We now replace the picture part to make use of the isUploading flag to indicate a loading state.

{!aspect.isUploading && aspect.url != null ? (
<img src={aspect.url} alt={caption} className={“picture”} />
) : (
<div className={“image-upload-placeholder”}>
<Spinner animation=”border” variant=”darkish” />
</div>
)}

That completes the implementation of step 1. Let’s confirm that we’re in a position to choose a picture to add, see the picture node getting inserted with a loading indicator the place it was inserted within the doc.

Shifting to Step (2), we’ll use axois library to ship a request to the server.

export default perform useImageUploadHandler(editor, previousSelection) {
return useCallback((occasion) => {
….
Transforms.insertNodes(

{at: previousSelection, choose: true}
);

axios
.publish(“/add”, formData, {
headers: {
“content-type”: “multipart/form-data”,
},
})
.then((response) => {
// replace the picture node.
})
.catch((error) => {
// Hearth one other Rework.setNodes to set an add failed state on the picture
});
}, […]);
}

We confirm that the picture add works and the picture does present up within the public/images folder of the app. Now that the picture add is full, we transfer to Step (3) the place we need to set the URL on the picture within the resolve() perform of the axios promise. We may replace the picture with Transforms.setNodes however we’ve an issue — we wouldn’t have the trail to the newly inserted picture node. Let’s see what our choices are to get to that picture —

Can’t we use editor.choice as the choice have to be on the newly inserted picture node? We can’t assure this since whereas the picture was importing, the person might need clicked someplace else and the choice might need modified.
How about utilizing previousSelection which we used to insert the picture node within the first place? For a similar purpose we will’t use editor.choice, we will’t use previousSelection since it might have modified too.
SlateJS has a Historical past module that tracks all of the modifications taking place to the doc. We may use this module to look the historical past and discover the final inserted picture node. This additionally isn’t fully dependable if it took longer for the picture to add and the person inserted extra photos in several components of the doc earlier than the primary add accomplished.
At present, Rework.insertNodes’s API doesn’t return any details about the inserted nodes. If it may return the paths to the inserted nodes, we may use that to search out the exact picture node we must always replace.

Since not one of the above approaches work, we apply an id to the inserted picture node (in Step (1)) and use the identical id once more to find it when the picture add is full. With that, our code for Step (3) appears like under —

axios
.publish(“/add”, formData, {
headers: {
“content-type”: “multipart/form-data”,
},
})
.then((response) => {
const newImageEntry = Editor.nodes(editor, {
match: (n) => n.id === id,
});

if (newImageEntry == null) {
return;
}

Transforms.setNodes(
editor,
{ isUploading: false, url: `/images/${fileName}` },
{ at: newImageEntry[1] }
);
})
.catch((error) => {
// Hearth one other Rework.setNodes to set an add failure state
// on the picture.
});

With the implementation of all three steps full, we’re prepared to check the picture add finish to finish.

With that, we’ve wrapped up Pictures for our editor. At present, we present a loading state of the identical dimension no matter the picture. This might be a jarring expertise for the person if the loading state is changed by a drastically smaller or larger picture when the add completes. comply with as much as the add expertise is getting the picture dimensions earlier than the add and displaying a placeholder of that dimension in order that transition is seamless. The hook we add above might be prolonged to help different media sorts like video or paperwork and render these kinds of nodes as properly.

Conclusion

On this article, we’ve constructed a WYSIWYG Editor that has a fundamental set of functionalities and a few micro user-experiences like hyperlink detection, in-place hyperlink modifying and picture caption modifying that helped us go deeper with SlateJS and ideas of Wealthy Textual content Enhancing on the whole. If this downside house surrounding Wealthy Textual content Enhancing or Phrase Processing pursuits you, a number of the cool issues to go after might be:

Collaboration
A richer textual content modifying expertise that helps textual content alignments, inline photos, copy-paste, altering font and textual content colours and so on.
Importing from standard codecs like Phrase paperwork and Markdown.

If you wish to be taught extra SlateJS, listed below are some hyperlinks that is likely to be useful.

SlateJS Examples
Plenty of examples that transcend the fundamentals and construct functionalities which can be normally present in Editors like Search & Spotlight, Markdown Preview and Mentions.
API Docs
Reference to a number of helper features uncovered by SlateJS that one would possibly need to maintain helpful when attempting to carry out advanced queries/transformations on SlateJS objects.

Lastly, SlateJS’s Slack Channel is a really energetic group of internet builders constructing Wealthy Textual content Enhancing purposes utilizing SlateJS and an amazing place to be taught extra concerning the library and get assist if wanted.

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment