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

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

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 wish. They are also often called Rich Text Editors.

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

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

Here is the running demo version of the editor we are about to code together.

Also on this page I use this rich text editor for the comments! 🙂

1. Design the HTML framework

Our main HTML task is to create the editor toolbar. For this we have an outer container .wp-webdeasy-comment-editor. This includes a container for the toolbar .toolbar and a container for the different views (Visual view & HTML view) .content-area.

<div class="wp-webdeasy-comment-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 there can be as many as you like. There are also several .box boxes in each line for a rough outline of the formatting options.

In such a box there is always a span element with a data action (data-action). This data action contains the command that is to be executed later on the selected text. In addition, some elements have a data tag name (data-tag-name). This is important later so that we can set the button active if the current text selection has a certain formatting.

This is what the two toolbar lines look like in HTML:

<div class="line">

  <div class="box">
    <span class="editor-btn icon smaller" data-action="bold" data-tag-name="b" title="Bold">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/bold.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="italic" data-tag-name="i" title="Italic">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/italic.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="underline" data-tag-name="u" title="Underline">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/underline.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="strikeThrough" data-tag-name="strike" title="Strike through">
      <img src="https://img.icons8.com/fluency-systems-filled/30/000000/strikethrough.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon has-submenu">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"/>
      <div class="submenu">
        <span class="editor-btn icon" data-action="justifyLeft" data-style="textAlign:left" title="Justify left">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"/>
        </span>
        <span class="editor-btn icon" data-action="justifyCenter" data-style="textAlign:center" title="Justify center">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-center.png"/>
        </span>
        <span class="editor-btn icon" data-action="justifyRight" data-style="textAlign:right" title="Justify right">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-right.png"/>
        </span>
        <span class="editor-btn icon" data-action="formatBlock" data-style="textAlign:justify" title="Justify block">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-justify.png"/>
        </span>
      </div>
    </span>
    <span class="editor-btn icon" data-action="insertOrderedList" data-tag-name="ol" title="Insert ordered list">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/numbered-list.png"/>
    </span>
    <span class="editor-btn icon" data-action="insertUnorderedList" data-tag-name="ul" title="Insert unordered list">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/bulleted-list.png"/>
    </span>
    <span class="editor-btn icon" data-action="outdent" title="Outdent" data-required-tag="li">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/outdent.png"/>
    </span>
    <span class="editor-btn icon" data-action="indent" title="Indent">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/indent.png"/>
    </span>

  </div>
  <div class="box">
    <span class="editor-btn icon" data-action="insertHorizontalRule" title="Insert horizontal rule">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/horizontal-line.png"/>
    </span>
  </div>

</div>
<div class="line">

  <div class="box">
    <span class="editor-btn icon smaller" data-action="undo" title="Undo">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/undo--v1.png"/>
    </span>
    <span class="editor-btn icon" data-action="removeFormat" title="Remove format">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/remove-format.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon smaller" data-action="createLink" title="Insert Link">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/add-link.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="unlink" data-tag-name="a" title="Unlink">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/delete-link.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon" data-action="toggle-view" title="Show HTML-Code">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/source-code.png"/>
    </span>
  </div>

</div>

The Data Action is the command that will be executed later on the selected text. There is a list of 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 .visual-view, which also gets the property contenteditable. This property allows us to edit content directly inline without input. Feel free to try this out if you don’t know this feature.

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

We also add 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>

This modal is opened when we want to insert a link. There you have the possibility to enter the link and choose if you want to open the link in a new window.

<div class="modal">
  <div class="modal-bg"></div>
  <div class="modal-wrapper">
    <div class="close">✖</div>
    <div class="modal-content" id="modalCreateLink">
      <h3>Insert Link</h3>
      <input type="text" id="linkValue" placeholder="Link (example: https://webdeasy.de/)">
      <div class="row">
        <input type="checkbox" id="new-tab">
        <label for="new-tab">Open in new Tab?</label>
      </div>
      <button class="done">Done</button>
    </div>
  </div>
</div>

1.4 Full HTML code

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

<div class="wp-webdeasy-comment-editor">
  <div class="toolbar">
    <div class="line">
      <div class="box">
        <span
          class="editor-btn icon smaller"
          data-action="bold"
          data-tag-name="b"
          title="Bold"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/bold.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="italic"
          data-tag-name="i"
          title="Italic"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/italic.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="underline"
          data-tag-name="u"
          title="Underline"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/underline.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="strikeThrough"
          data-tag-name="strike"
          title="Strike through"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/30/000000/strikethrough.png"
          />
        </span>
      </div>

      <div class="box">
        <span class="editor-btn icon has-submenu">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"
          />
          <div class="submenu">
            <span
              class="editor-btn icon"
              data-action="justifyLeft"
              data-style="textAlign:left"
              title="Justify left"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="justifyCenter"
              data-style="textAlign:center"
              title="Justify center"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-center.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="justifyRight"
              data-style="textAlign:right"
              title="Justify right"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-right.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="formatBlock"
              data-style="textAlign:justify"
              title="Justify block"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-justify.png"
              />
            </span>
          </div>
        </span>
        <span
          class="editor-btn icon"
          data-action="insertOrderedList"
          data-tag-name="ol"
          title="Insert ordered list"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/numbered-list.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="insertUnorderedList"
          data-tag-name="ul"
          title="Insert unordered list"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/bulleted-list.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="outdent"
          title="Outdent"
          data-required-tag="li"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/outdent.png"
          />
        </span>
        <span class="editor-btn icon" data-action="indent" title="Indent">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/indent.png"
          />
        </span>
      </div>
      <div class="box">
        <span
          class="editor-btn icon"
          data-action="insertHorizontalRule"
          title="Insert horizontal rule"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/horizontal-line.png"
          />
        </span>
      </div>
    </div>
    <div class="line">
      <div class="box">
        <span class="editor-btn icon smaller" data-action="undo" title="Undo">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/undo--v1.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="removeFormat"
          title="Remove format"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/remove-format.png"
          />
        </span>
      </div>

      <div class="box">
        <span
          class="editor-btn icon smaller"
          data-action="createLink"
          title="Insert Link"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/add-link.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="unlink"
          data-tag-name="a"
          title="Unlink"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/delete-link.png"
          />
        </span>
      </div>

      <div class="box">
        <span
          class="editor-btn icon"
          data-action="toggle-view"
          title="Show HTML-Code"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/source-code.png"
          />
        </span>
      </div>
    </div>
  </div>
  <div class="content-area">
    <div class="visuell-view" contenteditable></div>
    <textarea class="html-view"></textarea>
  </div>
</div>

<div class="modal">
  <div class="modal-bg"></div>
  <div class="modal-wrapper">
    <div class="close">✖</div>
    <div class="modal-content" id="modalCreateLink">
      <h3>Insert Link</h3>
      <input
        type="text"
        id="linkValue"
        placeholder="Link (example: https://webdeasy.de/)"
      />
      <div class="row">
        <input type="checkbox" id="new-tab" />
        <label for="new-tab">Open in new Tab?</label>
      </div>
      <button class="done">Done</button>
    </div>
  </div>
</div>

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

<a href="https://icons8.com/">Icons by Icons8</a>

2. Style WYSIWYG editor

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

But I don’t explain anything else 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;
}
/* WYSIWYG Editor */
.wp-webdeasy-comment-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 0;
}
.wp-webdeasy-comment-editor .toolbar {
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.wp-webdeasy-comment-editor .toolbar .line {
  display: flex;
  border-bottom: 1px solid #e2e2e2;
}
.wp-webdeasy-comment-editor .toolbar .line:last-child {
  border-bottom: none;
}
.wp-webdeasy-comment-editor .toolbar .line .box {
  display: flex;
  border-left: 1px solid #e2e2e2;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn {
  display: block;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: 0.2s ease all;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn:hover, .wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.active {
  background-color: #e1e1e1;
  cursor: pointer;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.icon img {
  width: 15px;
  padding: 9px;
  box-sizing: content-box;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.icon.smaller img {
  width: 16px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu {
  width: 20px;
  padding: 0 10px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu::after {
  content: '';
  width: 6px;
  height: 6px;
  position: absolute;
  background-image: url(https://img.icons8.com/ios-glyphs/30/000000/chevron-down.png);
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  right: 4px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu {
  display: none;
  position: absolute;
  top: 34px;
  left: -1px;
  z-index: 10;
  background-color: #FFF;
  border: 1px solid #b5b5b5;
  border-top: none;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu .btn {
  width: 39px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu:hover {
  display: block;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu:hover .submenu {
  display: block;
}
.wp-webdeasy-comment-editor .content-area {
  padding: 15px 12px;
  line-height: 1.5;
}
.wp-webdeasy-comment-editor .content-area .visuell-view {
  outline: none;
  min-height: 12rem;
}
.wp-webdeasy-comment-editor .content-area .visuell-view p {
  margin: 12px 0;
}
.wp-webdeasy-comment-editor .content-area .html-view {
  outline: none;
  display: none;
  width: 100%;
  height: 200px;
  border: none;
  resize: none;
}
/* Modal */
.modal {
  z-index: 40;
  display: none;
}
.modal .modal-wrapper {
  background-color: #FFF;
  padding: 1rem;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 20rem;
  min-height: 10rem;
  z-index: 41;
}
.modal .modal-wrapper .close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  cursor: pointer;
}
.modal .modal-wrapper .modal-content {
  flex-direction: column;
}
.modal .modal-wrapper .modal-content h3 {
  margin-top: 0;
}
.modal .modal-wrapper .modal-content input {
  margin: 1rem 0;
  padding: 0.5rem;
}
.modal .modal-wrapper .modal-content input[type="text"] {
  width: calc(100% - 1rem);
}
.modal .modal-wrapper .modal-content .row label {
  margin-left: 0.5rem;
}
.modal .modal-wrapper .modal-content button {
  background-color: #D2434F;
  border: 0;
  color: #FFF;
  padding: 0.5rem 1.2rem;
  cursor: pointer;
}
.modal .modal-bg {
  position: fixed;
  background-color: rgba(0, 0, 0, 0.3);
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
}

3. Programming functions in JavaScript

3.1 Declare variables

In JavaScript we now have to implement some functions. To do this, we first declare and initialize important elements of our editor:

const editor = document.getElementsByClassName('wp-webdeasy-comment-editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.editor-btn:not(.has-submenu)');
const contentArea = editor.getElementsByClassName('content-area')[0];
const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
const htmlView = contentArea.getElementsByClassName('html-view')[0];
const modal = document.getElementsByClassName('modal')[0];

3.2 Assign functions to toolbar buttons

To avoid programming each function individually we have already created a data action (data-action) in the HTML with the command. 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) {
  });
}

With the following line we read the action from the data action (in the HTML).

let action = this.dataset.action;

We include a switch-case statement because inserting a link and switching the HTML view and the visual view requires even more from us.

switch(action) {
  case 'toggle-view':
    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);
}

JavaScript provides us with a great function document.execCommand(). This allows us to apply our action to the selected text. You can find the documentation for this function here.

The execCommand() function is deprecated, but is currently still supported by all browsers. As soon as there should be problems here, I will update this post!

The second parameter of execCommand() must be set to false. With this we disable a small UI that would be displayed in old Internet Explorer versions, for example. But we don’t need this and Firefox or Google Chrome don’t support these functions anyway.

When we want to switch between the HTML view and the visual view, we fade in the other one and swap the contents.

function execCodeAction(button, editor) {

  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. For this purpose, I have already provided a modal in the HTML, i.e. a kind of pop-up.

In the following function this is shown and the current text selection of the editor is saved via saveSelection(). This is necessary because we focus another element in our popup and thus our text selection in the editor disappears. After that, the close and submit buttons are created.

function execLinkAction() {  
  modal.style.display = 'block';
  let selection = saveSelection();

  let submit = modal.querySelectorAll('button.done')[0];
  let close = modal.querySelectorAll('.close')[0];
}
function saveSelection() {
    if(window.getSelection) {
        sel = window.getSelection();
        if(sel.getRangeAt && sel.rangeCount) {
            let ranges = [];
            for(var i = 0, len = sel.rangeCount; i < len; ++i) {
                ranges.push(sel.getRangeAt(i));
            }
            return ranges;
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
}

Now we need a click event to insert the link. There we additionally save whether the link should be opened in a new window, load the selection from the text editor again with restoreSelection() and then create a new a element for it in line 13 and set the link from the link input.

In line 16 we then insert the created link around the text selection.

The modal is then closed, the link input is cleaned and all events are deregistered.

function execLinkAction() {  
  // ...  
  // done button active => add link
  submit.addEventListener('click', function(e) {
    e.preventDefault();
    let newTabCheckbox = modal.querySelectorAll('#new-tab')[0];
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    let linkValue = linkInput.value;
    let newTab = newTabCheckbox.checked;    
    
    restoreSelection(selection);
    
    if(window.getSelection().toString()) {
      let a = document.createElement('a');
      a.href = linkValue;
      if(newTab) a.target = '_blank';
      window.getSelection().getRangeAt(0).surroundContents(a);
    }

    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });  
  // ...
}
function restoreSelection(savedSel) {
    if(savedSel) {
        if(window.getSelection) {
            sel = window.getSelection();
            sel.removeAllRanges();
            for(var i = 0, len = savedSel.length; i < len; ++i) {
                sel.addRange(savedSel[i]);
            }
        } else if(document.selection && savedSel.select) {
            savedSel.select();
        }
    }
}

We also give the close button a function that simply hides the modal, clears the link input, and deregisters the two events.

function execLinkAction() {  
  // ...  
  close.addEventListener('click', function(e) {
    e.preventDefault();
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    
    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });
}

3.4 Enable toolbar buttons when formatting is selected

If a text is selected in the WYSIWYG editor, we also want to highlight the corresponding formatting button. This way we always know what formatting a word or paragraph has.

To do this, we insert the registration of the selectionchange event at the very top, directly after the declaration of the variable.

// add active tag event
document.addEventListener('selectionchange', selectionChange);

We then create the callback function, which first removes this class from all toolbar buttons with .active class. After that we check if our selection is even in our WYSIWYG editor (line 12). Then we call the parentTagActive() function and pass the first parent HTML tag of the current text selection.

function selectionChange(e) {
  
  for(let i = 0; i < buttons.length; i++) {
    let button = buttons[i];
    
    // don't remove active class on code toggle button
    if(button.dataset.action === 'toggle-view') continue;
    
    button.classList.remove('active');
  }
  
  if(!childOf(window.getSelection().anchorNode.parentNode, editor)) return false;
  
  parentTagActive(window.getSelection().anchorNode.parentNode);
}

I defined the parentTagActive() function recursively, so that there can be multiple active tags. So if a word is italic, bold and underlined all three toolbar buttons are set active. For this reason, the individual buttons in the HTML have been given the data tag name (data-tag-name).

The text alignment is handled in the same way, so we can see whether the text is left-aligned, right-aligned, justified or centered.

function parentTagActive(elem) {
  if(!elem ||!elem.classList || elem.classList.contains('visuell-view')) return false;
  
  let toolbarButton;
  
  // active by tag names
  let tagName = elem.tagName.toLowerCase();
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-tag-name="${tagName}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  // active by text-align
  let textAlign = elem.style.textAlign;
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-style="textAlign:${textAlign}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  return parentTagActive(elem.parentNode);
}

3.5 Remove formatting when pasting text (paste event)

When a user pastes something into the text editor, all formatting should be removed from this text, otherwise it can lead to unsightly formatting and complete design chaos. For this we register the paste event.

// add paste event
visuellView.addEventListener('paste', pasteEvent);

The pasteEvent() function is then executed, which prevents normal pasting, gets the content from the user’s clipboard as plain text, and pastes it into our editor.

function pasteEvent(e) {
  e.preventDefault();
  
  let text = (e.originalEvent || e).clipboardData.getData('text/plain');
  document.execCommand('insertHTML', false, text);
}

3.6 Insert p tag as line break

Another improvement is to automatically insert a <p> tag as soon as the user presses Enter. For this we register the keypress event.

// add paragraph tag on new line
contentArea.addEventListener('keypress', addParagraphTag);

The addParagraphTag() function is called. This checks whether the Enter key was pressed (keycode 13). Then the current block is automatically formatted as a <p>-tag if the current element is not a list element (<li>-tag).

function addParagraphTag(evt) {
  if (evt.keyCode == '13') {
    
    // don't add a p tag on list item
    if(window.getSelection().anchorNode.parentNode.tagName === 'LI') return;
    document.execCommand('formatBlock', false, 'p');
  }
}

3.7 Full JavaScript Code

With that, we have added all the functions. And here again the complete JavaScript code including comments:

// define vars
const editor = document.getElementsByClassName('wp-webdeasy-comment-editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.editor-btn:not(.has-submenu)');
const contentArea = editor.getElementsByClassName('content-area')[0];
const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
const htmlView = contentArea.getElementsByClassName('html-view')[0];
const modal = document.getElementsByClassName('modal')[0];

// add active tag event
document.addEventListener('selectionchange', selectionChange);

// add paste event
visuellView.addEventListener('paste', pasteEvent);

// add paragraph tag on new line
contentArea.addEventListener('keypress', addParagraphTag);

// add toolbar button actions
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 'toggle-view':
        execCodeAction(this, editor);
        break;
      case 'createLink':
        execLinkAction();
        break;
      default:
        execDefaultAction(action);
    }
    
  });
}

/** 
 * This function toggles between visual and html view
 */
function execCodeAction(button, editor) {

  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'); 
  }
}

/**
 * This function adds a link to the current selection
 */
function execLinkAction() {  
  modal.style.display = 'block';
  let selection = saveSelection();

  let submit = modal.querySelectorAll('button.done')[0];
  let close = modal.querySelectorAll('.close')[0];
  
  // done button active => add link
  submit.addEventListener('click', function(e) {
    e.preventDefault();
    let newTabCheckbox = modal.querySelectorAll('#new-tab')[0];
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    let linkValue = linkInput.value;
    let newTab = newTabCheckbox.checked;    
    
    restoreSelection(selection);
    
    if(window.getSelection().toString()) {
      let a = document.createElement('a');
      a.href = linkValue;
      if(newTab) a.target = '_blank';
      window.getSelection().getRangeAt(0).surroundContents(a);
    }

    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });  
  
  // close modal on X click
  close.addEventListener('click', function(e) {
    e.preventDefault();
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    
    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });
}

/**
 * This function executes all 'normal' actions
 */
function execDefaultAction(action) {
  document.execCommand(action, false);
}

/**
 * Saves the current selection
 */
function saveSelection() {
    if(window.getSelection) {
        sel = window.getSelection();
        if(sel.getRangeAt && sel.rangeCount) {
            let ranges = [];
            for(var i = 0, len = sel.rangeCount; i < len; ++i) {
                ranges.push(sel.getRangeAt(i));
            }
            return ranges;
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
}

/**
 *  Loads a saved selection
 */
function restoreSelection(savedSel) {
    if(savedSel) {
        if(window.getSelection) {
            sel = window.getSelection();
            sel.removeAllRanges();
            for(var i = 0, len = savedSel.length; i < len; ++i) {
                sel.addRange(savedSel[i]);
            }
        } else if(document.selection && savedSel.select) {
            savedSel.select();
        }
    }
}

/**
 * Sets the current selected format buttons active/inactive
 */ 
function selectionChange(e) {
  
  for(let i = 0; i < buttons.length; i++) {
    let button = buttons[i];
    
    // don't remove active class on code toggle button
    if(button.dataset.action === 'toggle-code') continue;
    
    button.classList.remove('active');
  }
  
  if(!childOf(window.getSelection().anchorNode.parentNode, editor)) return false;
  
  parentTagActive(window.getSelection().anchorNode.parentNode);
}

/**
 * Checks if the passed child has the passed parent
 */
function childOf(child, parent) {
  return parent.contains(child);
}

/**
 * Sets the tag active that is responsible for the current element
 */
function parentTagActive(elem) {
  if(!elem ||!elem.classList || elem.classList.contains('visuell-view')) return false;
  
  let toolbarButton;
  
  // active by tag names
  let tagName = elem.tagName.toLowerCase();
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-tag-name="${tagName}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  // active by text-align
  let textAlign = elem.style.textAlign;
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-style="textAlign:${textAlign}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  return parentTagActive(elem.parentNode);
}

/**
 * Handles the paste event and removes all HTML tags
 */
function pasteEvent(e) {
  e.preventDefault();
  
  let text = (e.originalEvent || e).clipboardData.getData('text/plain');
  document.execCommand('insertHTML', false, text);
}

/**
 * This functions adds a paragraph tag when the enter key is pressed
 */
function addParagraphTag(evt) {
  if (evt.keyCode == '13') {
    
    // don't add a p tag on list item
    if(window.getSelection().anchorNode.parentNode.tagName === 'LI') return;
    document.execCommand('formatBlock', false, 'p');
  }
}

4. Conclusion

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

On this page I’m also using this WYSIWYG Editor for the WordPress Comments. Check out the link to see how easy this works!

Related Posts
Join the Conversation

28 Comments

  1. Max says:

    Thanks for this tutorialWorks fine, just, for heading H1/2/… what’s the way ?

  2. Peter Hollingsworth says:

    This is some damned nice code

  3. Anish De says:

    Hi, thank you for the tutorial.Since execCommand() is deprecated and may not be supported by browsers in the future, would it be advisable to use execCommand() in the code? Is there any alternative to execCommand()Best regards,Anish

    1. Lorenz says:

      Hi! Actually I have no alternative for that and the execCommand() is still supported by all browsers. As soon as I find a alternative I will update this post.

  4. Barry says:

    I had a question around capturing the paragraph in a data field that is on the form.  I have code that writes the field to a DB record.  When I open the record it loads the field value. When using TextArea I just use the name=”FIELDNAME”.  <textarea name=”FIELDNAME”>[I would put code here to place current or default field value]</textarea>How would I do that with this code.  I have not been able to do the same thing with the div and contenteditable.Thanks,Barry 

    1. Lorenz says:

      Hi, thanks for your question!

      I made another post how to add this as a WP comment form. I think step 3.3 of this tutorial is the code you need: https://webdeasy.de/en/wysiwyg-editor-wordpress-comments/#pass-editor-data

      Greetings,
      Lorenz

  5. Christian Rasmussen says:

    Great tutorial, but how do I submit the input / textarea?Best regards Christian

    1. Lorenz says:

      Thank you! Check out this article: WYSIWYG Editor in WordPress Comments

      Hope this helps! 🙂

  6. timir says:

    ghilas and LH,I also faced the same issue. … probably javascript tries to find the editor before the html is rendered.Add the javascript “after” the html part ..just before closing body tag.Let me know if that helps

    1. Lorenz says:

      Good hint! I’ll add at in the post ASAP 🙂

  7. Niph says:

    Would be nice to have alternative with icons packed in some file 😉 Flaticon seems to have some trouble with working on my page – some icons are not loading and it says thet its not available in my country 😛

    1. Lorenz says:

      I don’t have permissions for these icons to publish them under “my name”. But you can take other items from the internet, just google for it 🙂

  8. Ben says:

    You save lives.

    1. Lorenz says:

      🙂

  9. Mladen says:

    Seems like a good tutorial.

    Thanks a bunch!

  10. Jay says:

    i will try this today. thanks a ton for sharing!!

  11. hder says:

    When I cut/paste the code (styles,html,script) and start the page, the SVG icons are huge. Doesn’t seem to take the CSS. The style is ahead of the html. Using Chrome browser… yet your codepen seems to work fine… what is the simple thing I’m missing here?

    1. Lorenz says:

      The other CSS is working? Check it with Chrome DevTools (F12) – search for the icons and on the right should be the CSS rules. Are they?

  12. Sol says:

    How can you convert this to become a textbox for submitting a form?

    1. Sol says:

      Great tutorial! Simple and too the point.

    2. Lorenz says:

      Thanks! What exactly do you mean..? This is just the editor, but what you’re doing with this data is not done here.

      Greetings
      LH

  13. ghilas says:

    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. Lorenz says:

      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

    2. Lorenz says:

      You can compare your HTML Code with mine on the Codepen Example.

    3. Lorenz says:

      There’s a hint from @timir: You need to include your JavaScript file after your HTML code.

      Hope this fixes your issue!

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

bold italic underline strikeThrough
insertOrderedList insertUnorderedList outdent indent
removeFormat
createLink unlink
code

This can also interest you