I always thought that doing rich text editors was something from rocket science 😆. First, what comes to my mind is getting a text from an invisible textarea
and appending typed text into HTML behind the input. And when you select something, wrap the selection somehow with tags, or replace it with regular expressions. But everything is much easier!
All magic happens because of adding an attribute contenteditable=”true”
. You can add it to any element, so this element and its children will be editable through a browser.
Editable content in browser with contenteditable=” true”:
As you can see, we can modify existing elements. Also, when we click enter, we add a block element div
as a new line. But how can we change selected text and make some words, for example, bold or italic?
How to implement a rich text editor
The easiest way is to use document.execCommand()
.
Here I have to say that the _document.execCommand_
is deprecated. Some browsers cannot standardize it, and in documentation you will see that this feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time.
But it’s deprecated for many years. Despite that, it has no alternative, and many developers still use it in the WYSIWYG editors.
With this function, to make selected text bold, we need to call document.execCommand('bold', false);
function, and that’s it.
So our mission is to create a toolbar with buttons. Then call a single line of code when the user press on any of them.
Now, let’s create a rich text editor as a custom component with some format buttons. I like the Lit library, and currently, it’s a good choice for our demo.
I have already written some articles about this library. If you hadn’t a chance to read about this one of the most performance libraries for making web components, here are they:
5 Reasons Why You Should Try LitElement in Your Next Project
State Management for Complex Web Components
Building a rich text web component with Lit library
It’s just a tutorial, so we will download the Lit component starter kit to save time on the dev server configuration. Unarchive the folder and install dependencies npm i
.
Also, we need to install one additional package for simple displaying material icons in buttons.
npm i @material/mwc-icon-button
We already have src/my-element.ts
. I didn’t even change the name of the class. I just imported the icon component, added some styles, and in the render function, I added a toolbar and tag where we will edit the text.
For rendering the toolbar, I created an array with material icons names and commands they should call.
import {LitElement, html, customElement, css} from 'lit-element';
import '@material/mwc-icon-button';
const commands = [
{
icon: 'format_bold',
command: 'bold',
},
{
icon: 'format_italic',
command: 'italic',
},
{
icon: 'format_underlined',
command: 'underline',
},
{
icon: 'format_strikethrough',
command: 'strikethrough',
},
{
icon: 'format_align_left',
command: 'justifyleft',
},
{
icon: 'format_align_center',
command: 'justifycenter',
},
{
icon: 'format_align_right',
command: 'justifyright',
},
{
icon: 'format_list_numbered',
command: 'insertorderedlist',
},
{
icon: 'format_list_bulleted',
command: 'insertunorderedlist',
},
{
icon: 'undo',
command: 'undo',
},
{
icon: 'redo',
command: 'redo',
},
];
@customElement('my-element')
export class MyElement extends LitElement {
content = '';
root: Element | null = null;
static override styles = css`
:host {
--editor-width: 100%;
--editor-height: 100%;
}
:host * {
box-sizing: border-box;
}
main {
width: var(--editor-width);
height: var(--editor-height);
display: flex;
flex-direction: column;
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
#btns {
padding: 10px;
background: #fafafa;
border-bottom: 1px solid #efefef;
}
#editor {
background-color: #f9f9f9;
flex: 1;
padding: 20px;
outline: 0px solid transparent;
}
mwc-icon-button {
color: #333;
--mdc-icon-size: 20px;
--mdc-icon-button-size: 30px;
}
`;
override render() {
return html`<main>
<div id="btns">
${toolbar((command) => {
document.execCommand(command, false);
})}
</div>
<div id="editor" contenteditable="true" spellcheck="false"></div>
</main> `;
}
}
function toolbar(command: (c: string) => void) {
return html`
${commands.map((button) => {
return html`<mwc-icon-button
.icon="${button.icon}"
@click="${() => {
command(button.command);
}}"
></mwc-icon-button>`;
})}
`;
}
Now we only need to modify the demo index.html with icons import and some CSS styles.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Rich Text Demo</title>
<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script src="../node_modules/lit/polyfill-support.js"></script>
<script type="module" src="../my-element.js"></script>
<link
href="https://fonts.googleapis.com/css?family=Material+Icons&display=block"
rel="stylesheet"
/>
<style>
body {
margin: 0;
height: 100vh;
background: rgb(233, 233, 233);
}
my-element {
width: 900px;
height: 400px;
display: block;
margin: 30px auto;
}
</style>
</head>
<body>
<my-element></my-element>
</body>
</html>
You can play with the component on the lit playground.
Alternative approaches
It seems the replacement for the document.execCommand
will be Input Events level 2. But in the future and currently, it doesn’t cover some functionality like undo/redo.
For now, it’s also possible to implement manual HTML modification with getting selection and wrapping it into the format tag.
For example, something like this.
function bold() {
const boldEl = document.createElement("b");
const textSelection = window.getSelection();
userSelection.getRangeAt(0).surroundContents(strongElement);
}
But it will cover only wrapping functionality. To undo changes, we need to implement something more complex.
So as you see, currently, in 2022, a document.execCommand
function is almost the single and straightforward choice for a rich text editor.
Thanks for reading!