Program your own WYSIWYG editor – with HTML, CSS & JavaScript

Program your own WYSIWYG editor – with HTML, CSS & JavaScript Thumbnail
Published on 21. July 2019Last updated on 16. June 2020

Are you annoyed by missing or unnecessary functions in WYSIWYG editors? No problem! Here I show you how to program your own WYSIWYG editor with HTML, CSS and JavaScript.

Advertisement

WYSIWYG stands for “What you see is what you get”. This refers to text editors that directly display a text with all formatting and we can change the formatting as we like. They are also often referred to as rich text editors.

Many of the available editors like TinyMCE work really well and are great for most projects. However, you might find some of them a bit overloaded, too complicated or just want to create your own WYSIWYG editor.

The following demo is created with pure HTML, CSS and pure JavaScript. In the next steps I will go into detail about the implementation of this WYSIWYG editor and at the end you will be able to program your own editor – we only need about 10 minutes!

Here is the current demo version of the editor we want to code together now.

See the Pen WYSIWYG Editor by WebDEasy (@WebDEasy) on CodePen.

Advertisement

1. Design the HTML framework

Our main task in HTML is to create the Editor Toolbar. For this we have an outer container .editor. This includes a container for the toolbar .toolbar and a container for the different views (visual representation & HTML view) .content-area.

<div class="editor">
  <div class="toolbar">
  </div>
  <div class="content-area">
  </div>
</div>

1.1 The Toolbar

I have arranged the toolbar in two lines (.line), but it can also be more than that. In addition there are several .box boxes in each line for a rough outline of the formatting possibilities.

In such a box there is always a chip element with a Data Action. This data action contains the command that is to be executed later on the selected text. This is how the two toolbar lines look like in HTML:

<div class="line">
  
  <div class="box">
    <span class="btn icon smaller" data-action="bold" title="Bold">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25432.svg">
    </span>
    <span class="btn icon smaller" data-action="italic" title="Italic">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25392.svg">
    </span>
    <span class="btn icon smaller" data-action="underline" title="Underline">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25433.svg">
    </span>
    <span class="btn icon smaller" data-action="strikeThrough" title="Strike through">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25626.svg">
    </span>
  </div>
  
  <div class="box">
    <span class="btn icon has-submenu">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25351.svg">
      <div class="submenu">
        <span class="btn icon" data-action="justifyLeft" title="Justify left">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25351.svg">  
        </span>
        <span class="btn icon" data-action="justifyCenter" title="Justify center">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25440.svg">  
        </span>
        <span class="btn icon" data-action="justifyRight" title="Justify right">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25288.svg">  
        </span>
        <span class="btn icon" data-action="formatBlock" title="Justify block">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25181.svg">  
        </span>
      </div>
    </span>
    <span class="btn icon" data-action="insertOrderedList" title="Insert ordered list">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25242.svg">  
    </span>
    <span class="btn icon" data-action="insertUnorderedList" title="Insert unordered list">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25648.svg">  
    </span>
    <span class="btn icon" data-action="outdent" title="Outdent">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25410.svg">  
    </span>
    <span class="btn icon" data-action="indent" title="Indent">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25233.svg">  
    </span>
  </div>

  <div class="box">
    <span class="btn icon" data-action="insertHorizontalRule" title="Insert horizontal rule">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25232.svg">  
    </span>
  </div>
  
</div>

<div class="line">

  <div class="box">
    <span class="btn icon smaller" data-action="undo" title="Undo">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25249.svg">
    </span>
    <span class="btn icon" data-action="removeFormat" title="Remove format">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25454.svg">  
    </span>
  </div>
      
  <div class="box">
    <span class="btn icon smaller" data-action="createLink" title="Insert Link">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25385.svg">
    </span>
    <span class="btn icon smaller" data-action="unlink" title="Unlink">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25341.svg">
    </span>
  </div>
  
  <div class="box">
    <span class="btn icon" data-action="code" title="Show HTML-Code">
      <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25185.svg">
    </span>
  </div>
  
</div>

The Data Action is the command that is later executed on the selected text. There is a list of the MDN web docs for this purpose. So you can easily extend the editor with more commands here.

1.2 Visual and HTML view

In the content area we have two sections: An HTML view and a visual view. For this we create a container .visuell-view, which additionally gets the property contenteditable. This property ensures that we can edit content directly inline without input. If you don’t know this function, please try it out.

<div class="visuell-view" contenteditable>
</div>

We also insert a textarea .html-view for the HTML view, because we want to switch between HTML and visual view later in the editor.

<textarea class="html-view"></textarea>

And this is how the whole HTML code looks like at a glance:

Advertisement
<div class="editor">
  <div class="toolbar">
    <div class="line">
      
      <div class="box">
        <span class="btn icon smaller" data-action="bold" title="Bold">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25432.svg">
        </span>
        <span class="btn icon smaller" data-action="italic" title="Italic">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25392.svg">
        </span>
        <span class="btn icon smaller" data-action="underline" title="Underline">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25433.svg">
        </span>
        <span class="btn icon smaller" data-action="strikeThrough" title="Strike through">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25626.svg">
        </span>
      </div>
      
      <div class="box">
        <span class="btn icon has-submenu">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25351.svg">
          <div class="submenu">
            <span class="btn icon" data-action="justifyLeft" title="Justify left">
              <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25351.svg">  
            </span>
            <span class="btn icon" data-action="justifyCenter" title="Justify center">
              <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25440.svg">  
            </span>
            <span class="btn icon" data-action="justifyRight" title="Justify right">
              <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25288.svg">  
            </span>
            <span class="btn icon" data-action="formatBlock" title="Justify block">
              <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25181.svg">  
            </span>
          </div>
        </span>
        <span class="btn icon" data-action="insertOrderedList" title="Insert ordered list">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25242.svg">  
        </span>
        <span class="btn icon" data-action="insertUnorderedList" title="Insert unordered list">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25648.svg">  
        </span>
        <span class="btn icon" data-action="outdent" title="Outdent">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25410.svg">  
        </span>
        <span class="btn icon" data-action="indent" title="Indent">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25233.svg">  
        </span>
        
      </div>
      <div class="box">
        <span class="btn icon" data-action="insertHorizontalRule" title="Insert horizontal rule">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25232.svg">  
        </span>
      </div>
      
    </div>
    <div class="line">
      
      <div class="box">
        <span class="btn icon smaller" data-action="undo" title="Undo">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25249.svg">
        </span>
        <span class="btn icon" data-action="removeFormat" title="Remove format">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25454.svg">  
        </span>
      </div>
      
      <div class="box">
        <span class="btn icon smaller" data-action="createLink" title="Insert Link">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25385.svg">
        </span>
        <span class="btn icon smaller" data-action="unlink" title="Unlink">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25341.svg">
        </span>
      </div>

      <div class="box">
        <span class="btn icon" data-action="code" title="Show HTML-Code">
          <img loading="lazy" src="https://image.flaticon.com/icons/svg/25/25185.svg">
        </span>
      </div>
      
    </div>
  </div>
  <div class="content-area">
    <div class="visuell-view" contenteditable>
    </div>
    <textarea class="html-view"></textarea>
  </div>
</div>

In my editor I use icons from Flaticon. Therefore I have to insert a corresponding note on my page. If you use your own icons this is not necessary for you.

<p>Icons made by <a href="https://www.flaticon.com/authors/dave-gandy" title="Dave Gandy">Dave Gandy</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></p>

2. Style the WYSIWYG Editor

Here I have converted my SCSS code into normal CSS so that everyone can understand it.

But I won’t explain anything about this, because CSS basics should be clear if you want to program such an editor. Of course you can also use your own styles here.

body {
  margin: 0;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: 'Helvetica Neue', 'Helvetica', arial, sans-serif;
}
.editor {
  width: 40rem;
  min-height: 18rem;
  box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.3);
  border-top: 6px solid #4a4a4a;
  border-radius: 3px;
  margin: 2rem;
}
.editor .toolbar {
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.editor .toolbar .line {
  display: flex;
  border-bottom: 1px solid #e2e2e2;
}
.editor .toolbar .line:last-child {
  border-bottom: none;
}
.editor .toolbar .line .box {
  display: flex;
  border-left: 1px solid #e2e2e2;
}
.editor .toolbar .line .box .btn {
  display: block;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: 0.2s ease all;
}
.editor .toolbar .line .box .btn:hover, .editor .toolbar .line .box .btn.active {
  background-color: #e1e1e1;
  cursor: pointer;
}
.editor .toolbar .line .box .btn.icon img {
  width: 15px;
  padding: 10px;
}
.editor .toolbar .line .box .btn.icon.smaller img {
  width: 12px;
}
.editor .toolbar .line .box .btn.has-submenu {
  width: 20px;
  padding: 0 10px;
}
.editor .toolbar .line .box .btn.has-submenu::after {
  content: '';
  width: 6px;
  height: 6px;
  position: absolute;
  background-image: url(https://image.flaticon.com/icons/svg/25/25243.svg);
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  right: 4px;
}
.editor .toolbar .line .box .btn.has-submenu .submenu {
  display: none;
  position: absolute;
  top: 36px;
  left: -1px;
  z-index: 10;
  background-color: #fff;
  border: 1px solid #b5b5b5;
  border-top: none;
}
.editor .toolbar .line .box .btn.has-submenu .submenu .btn {
  width: 39px;
}
.editor .toolbar .line .box .btn.has-submenu:hover .submenu {
  display: block;
}
.editor .content-area {
  padding: 15px 12px;
  line-height: 1.5;
}
.editor .content-area .visuell-view {
  outline: none;
}
.editor .content-area .visuell-view p {
  margin: 12px 0;
}
.editor .content-area .html-view {
  outline: none;
  display: none;
  width: 100%;
  height: 200px;
  border: none;
  resize: none;
}

3. Programming functions in JavaScript

In JavaScript we now have to assign functions to our toolbar. For this purpose we first declare and initialize important elements of our editor:

const editor = document.getElementsByClassName('editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.btn:not(.has-submenu)');

In order not to program each function individually, we have already created a Data Action with the command in HTML. Now we simply register the click on these buttons in a loop:

for(let i = 0; i < buttons.length; i++) {
  let button = buttons[i];
  
  button.addEventListener('click', function(e) {
  });
}

JavaScript provides us with a great function document.execCommand(). It allows us to apply our action to the selected text. The documentation for this function can be found here.

We read the action from the Data Action (in HTML) and execute the command. The second parameter must be set to false. This deactivates a small UI that would be displayed in old versions of Internet Explorer, for example. But we don’t need this and Firefox or Google Chrome don’t support these functions anyway.

Advertisement
let action = this.dataset.action;
document.execCommand(action, false);

Now our editor is already functional. However, the function to switch between HTML view and visual view and the input of a link expect even more from us. Therefore we build in a switch-case statement.

switch(action) {
  case 'code':
    execCodeAction(this, editor);
    break;
  case 'createLink':
    execLinkAction();
    break;
  default:
    execDefaultAction(action);
}

For “normal” functions we use the execDefaultAction(action) function. There only the execCommand() function of JavaScript is executed with the data action of the respective button.

function execDefaultAction(action) {
  document.execCommand(action, false);
}

If we want to switch between the HTML view and the visual view, we show the other one and swap the contents.

function execCodeAction(button, editor) {
  const contentArea = editor.getElementsByClassName('content-area')[0];
  const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
  const htmlView = contentArea.getElementsByClassName('html-view')[0];

  if(button.classList.contains('active')) { // show visuell view
    visuellView.innerHTML = htmlView.value;
    htmlView.style.display = 'none';
    visuellView.style.display = 'block';

    button.classList.remove('active');     
  } else {  // show html view
    htmlView.innerText = visuellView.innerHTML;
    visuellView.style.display = 'none';
    htmlView.style.display = 'block';

    button.classList.add('active'); 
  }
}

Next, we want to be able to insert a link. Of course we have to specify which one. We do this with the JavaScript prompt() function.

function execLinkAction() {
  let linkValue = prompt('Link (e.g. https://webdeasy.de/)');
  document.execCommand('createLink', false, linkValue);
}

With this we have added all functions. And here again the complete JavaScript code:

const editor = document.getElementsByClassName('editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.btn:not(.has-submenu)');

for(let i = 0; i < buttons.length; i++) {
  let button = buttons[i];
  
  button.addEventListener('click', function(e) {
    let action = this.dataset.action;
    
    switch(action) {
      case 'code':
        execCodeAction(this, editor);
        break;
      case 'createLink':
        execLinkAction();
        break;
      default:
        execDefaultAction(action);
    }
    
  });
}

function execCodeAction(button, editor) {
  const contentArea = editor.getElementsByClassName('content-area')[0];
  const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
  const htmlView = contentArea.getElementsByClassName('html-view')[0];

  if(button.classList.contains('active')) { // show visuell view
    visuellView.innerHTML = htmlView.value;
    htmlView.style.display = 'none';
    visuellView.style.display = 'block';

    button.classList.remove('active');     
  } else {  // show html view
    htmlView.innerText = visuellView.innerHTML;
    visuellView.style.display = 'none';
    htmlView.style.display = 'block';

    button.classList.add('active'); 
  }
}

function execLinkAction() {
  let linkValue = prompt('Link (e.g. https://webdeasy.de/)');
  document.execCommand('createLink', false, linkValue);
}

function execDefaultAction(action) {
  document.execCommand(action, false);
}

4. Conclusion

As you can see now, you can relatively quickly program your own WYSIWYG editor and style and program according to your own ideas. If you liked this post, I would be happy if you support my blog by your repeated visit. 🙂

Related Posts
NEW 🚀
Join the Conversation

3 Comments

  1. Hello, thank you for your tutorial, it is very helpful.
    I have an error, when i try to implement your code
    “Uncaught TypeError: Cannot read property ‘getElementsByClassName’ of undefined”
    in the line : const toolbar = editor.getElementsByClassName(‘toolbar’)[0];
    I make some research to fix this problem, but i didn’t find a good solution. Could you help me

    Best regards

    1. Hi, glad to hear it. 🙂

      It seems that the editor variable was not declared correctly. Do you have this line (const editor = document.getElementsByClassName('editor')[0];) before that?
      And above all: Do you have the div with the editor class?
      The error seems to come because .editor is not found.

      Best regards
      LH

Your email address will not be published. Required fields are marked *