HTML5 attribute "contenteditable" to create a WYSIWYG



Using the HTML5 attribute contenteditable to create a WYSIWYG

Forms are the main interface between our visitors and our website. In order to be efficient, a form has to be as user-friendly as possible.

If we want to allow users to format their inputs, we often use a system made of custom tags like BBCode, but that it is not very convenient for a lambda user...

People are used to writing on software like Microsoft Word, where the text they write is formatted exactly as the final result they will have: we call this a WYSIWYG.

By following this tutorial, you will be able to create a WYSIWYG thanks to HTML5, with only a few lines of JavaScript!


Table Of Contents

  1. What do we want to do?
  2. The contenteditable attribute
  3. Adding the buttons
  4. Discovery of execCommand() method
  5. Simple formats
  6. Advanced formats
  7. Conclusion

1. What do we want to do?

Developing a WYSIWYG is not an easy task, so we have to define exactly what we want to develop.

We'll make a simple function: by passing any DOM element to this function, it'll become editable. To do that, we'll simply set its contenteditable attribute to true and create some buttons in order to format the content.
Below is an example of how we'll be able to call out the function:

makeWYSIWYG(my_dom_element);

And that's all!

2. The contenteditable attribute

HTML5 provides this new attribute: by setting it to true, it becomes editable!

<p contenteditable="true">You can <b>edit</b> this paragraph!</p>

This simple attribute is sufficient! The most cool part is that it works well on all current browsers, including IE7 and IE8!

We can start to build our makeWYSIWYG() function:

function makeWYSIWYG(editor)
{
    //If the DOM element we want to edit exists
    if(editor)
    {
        editor.setAttribute('contenteditable',true);
    }				
    return editor;
};

There is nothing special in this code: we just check if the function receives a not NULL parameter and, if so, we set its contenteditable attribute to true.

We can go further by adding to the element a method that makes it editable or not:

function makeWYSIWYG(editor)
{
    //If the DOM element we want to edit exists
    if(editor)
    {
        editor.isEditable=false; //By default, the element is not editable
        editor.setAttribute('contenteditable', false);
					
        //This function permits making the element editable or not
        editor.makeEditable = function(bool)
        {
            //Protect the value
            bool = bool==undefined?true:(typeof bool === 'boolean'?bool:true);
			
            //Change the editable state
            this.isEditable=bool;
            this.setAttribute('contenteditable',bool);
        };
    }	
    return editor;
};

This method will permit us to toggle the editable state of our editor!

3. Adding the buttons

The contenteditable attribute permits the user to only modify the text. If we want to allow him to format the content, bold it or underline it for example, we have to add some buttons.

Furthermore, because of our previous code, the element is not editable anymore by default: we have to add a button to fix that!

function makeWYSIWYG(editor)
{
    //If the DOM element we want to edit exists
    if(editor)
    {
        //We create the buttons container
        var buttons_container = document.createElement('div');
		
        //We define some properties to it...
        buttons_container.style.textAlign='center';
        buttons_container.style.marginTop='5px';
        buttons_container.className='makeWYSIWYG_buttons_container';
		
        //We create the buttons inside the container
        buttons_container.innerHTML=''+
            '<button class="makeWYSIWYG_editButton">Edit</button>'+
            '<button class="makeWYSIWYG_viewHTML">View HTML</button>'+
            '<div class="makeWYSIWYG_buttons" style="display: none;">'+
                '<button><b>Bold</b></button>'+
                '<button><em>Italic</em></button>'+
                '<button><ins>Underline</ins></button>'+
                '<button><del>Strike</del></button>'+
                '<button>• Unordered List</button>'+
                '<button>1. Ordered List</button>'+
                '<button>Link</ins></button>'+
                '<button>Image</button>'+
                '<button>Main title</button>'+
                '<button>Subtitle</button><br />'+
                '<button>Remove format</button>'+
            '</div>';
        
        //[...]
    }	
    return editor;
};

This code is very simple: we just set some buttons for the future interface of the WYSIWYG. You can see that some are inside a container: the goal of this is to hide them when the element is not editable.
We'll change its state thanks to the Edit button.

But wait! We have created the buttons, but we didn't insert them inside our page! We want to display them just below the editable element, so we have to call a function like insertAfter(): unfortunately, this method doesn't exist in JavaScript...

Basically, we have to get the parent of the editable element. If the last child of the parent is the editor itself, we just have to append the buttons to this parent.
Otherwise, we have to insert them before the element placed just after the editor...

//[...]
//We insert the buttons after the editor
var parent = editor.parentNode;

if(parent.lastchild == editor)
{
    parent.appendChild(buttons_container);
}
else
{
    parent.insertBefore(buttons_container, editor.nextSibling);
}

//[...]

We can now test our code with this HTML paragraph:

<p id="myParagraph">This paragraph will be editable later.</p>

And we call our wonderful function!

makeWYSIWYG(document.getElementById('myParagraph'));

4. Discovery of execCommand() method

Before doing something with these buttons, we have to think about a new problem: how to modify the style of the selected text when the user clicks on a button?

JavaScript doesn't provide an easy way to get the selected text of a webpage... And it's even more difficult to modify it!

Fortunately, here comes a very powerful function: document.execCommand().

This function acts on the selected text of editable elements. It's a very particular function, please find below its signature:

execCommand(String aCommandName, Boolean aShowDefaultUI, aValueArgument)

The most important parameter is the first one: it defines the command to execute. You can find the list of possible values here.

The second one is not well implemented by browsers, so I advise you to set it to false.

The last parameter is a value: some commands need one to work. It's the case for images which need the URL to the image to display.

So, if we want to bold the text when the user clicks on the Bold button, we just have to call this method:

document.execCommand('bold',false,null);

Pretty simple, isn't it?

In order to make our work easier, we'll put these commands directly as a custom data- attribute of our buttons: data-tag for the command name and possibly a data-value for optional values.

//[...]

'<button data-tag="bold"><b>Bold</b></button>'+
'<button data-tag="italic"><em>Italic</em></button>'+
'<button data-tag="underline"><ins>Underline</ins></button>'+
'<button data-tag="strikeThrough"><del>Strike</del></button>'+
'<button data-tag="insertUnorderedList">• Unordered List</button>'+
'<button data-tag="insertOrderedList">1. Ordered List</button>'+
'<button data-tag="createLink"><ins style="color: blue;">Link</ins></button>'+
'<button data-tag="insertImage">Image</button>'+
'<button data-value="h1" data-tag="heading">Main title</button>'+
'<button data-value="h2" data-tag="heading">Subtitle</button><br />'+
'<button data-tag="removeFormat">Remove format</button>'

//[...]

As you can see, we don't set a data-value for links and images: we prompt the users for them when they click on corresponding buttons.

5. Simple formats

Now that we know how to format the content, we can attach the click event on our buttons.

For more convenience, we'll use the Selector API of HTML5 and the method addEventListener(). This means that we'll lose the compatibility with Internet Explorer 7 and 8 !

You can easily find some fallbacks on your own: since it doesn't directly concern this tutorial we won't obstruct the code with more lines.

Let's go!

First of all, we should modify our method makeEditable(): we'll display or hide the buttons with it:

//This function permits making the element editable or not
editor.makeEditable = function(bool)
{
    //Protect the value
    bool = bool==undefined?true:(typeof bool === 'boolean'?bool:true);
							
    //Change the editable state
    this.isEditable=bool;
    this.setAttribute('contenteditable',bool);
						
    //Show/Hide the buttons
    if(bool)
    {
        buttons_container.querySelector('.makeWYSIWYG_buttons').style.display='block';
    }
    else
    {
        buttons_container.querySelector('.makeWYSIWYG_buttons').style.display='none';
    }
};

Maybe you're wondering why we used querySelector() on a class whereas we can use a basic ID with document.getElementById(). I remind you that when we develop a function: we'll be able to call it several times! If we use IDs, there will be some conflicts...

So, now that it's done, we should attach the click event on the Edit button: if the content is editable when the user clicks on this button, we'll hide the others buttons. If not, we'll display them. In other words, we just have to call the makeEditable() method!

//Click on the "Edit" button
buttons_container.querySelector('.makeWYSIWYG_editButton').addEventListener('click',function(e)
{
    if(editor.isEditable)
    {
        editor.makeEditable(false);
        this.innerHTML='Edit';
    }
    else
    {
        editor.makeEditable(true);
        this.innerHTML='Save';
    }
    e.preventDefault();
},false);

Please note that we prevent the default action of the click: since this event is attached to a <button> tag, it could submit a form... And we don't want that!

We can also add the click event on the View HTML button: it's very easy, the function will simply alert the HTML code of the editor:

//Click on the "View HTML" button
buttons_container.querySelector('.makeWYSIWYG_viewHTML').addEventListener('click',function(e)
{
    alert(editor.innerHTML);
    e.preventDefault();
},false);

Our buttons are now visible, we can attach the click event on them:

//Click on the "View HTML" button
buttons_container.querySelector('.makeWYSIWYG_viewHTML').addEventListener('click',function(e)
{
    //[...]
},false);
						
//Get the format buttons
var buttons = buttons_container.querySelectorAll('button[data-tag]');
						
//For each of them...
for(var i=0, l=buttons.length; i<l; i++)
{
    //We bind the click event
    buttons[i].addEventListener('click',function(e)
    {
        //Code here...
        e.preventDefault();
    });
}

In this function, the first thing we have to do is to retrieve the data-tag attribute. Then, we have basically three possible cases:

  • The user wants to insert a link: we'll prompt him which one.
  • The user wants to insert an image: we'll prompt him which one.
  • The user wants to insert a title: we'll make some checks.
  • The user wants to insert a basic format.

We'll start by this last case:

//Click on the "View HTML" button
buttons_container.querySelector('.makeWYSIWYG_viewHTML').addEventListener('click',function(e)
{
    //[...]
},false);
						
//Get the format buttons
var buttons = buttons_container.querySelectorAll('button[data-tag]');
						
//For each of them...
for(var i=0, l=buttons.length; i<l; i++)
{
    //We bind the click event
    buttons[i].addEventListener('click',function(e)
    {
        var tag = this.getAttribute('data-tag');
        switch(tag)
        {
            case 'createLink':
                //Code here
            break;
									
            case 'insertImage':
                //Code here
            break;
									
            case 'heading':
                //Code here
            break;
							
            default:
                document.execCommand(tag, false, this.getAttribute('data-value'));
        }
        
        e.preventDefault();
    });
}

Please note that Firefox can't insert lists within an editable paragraph!

6. Advanced formats

All the buttons work well, except of course the ones for titles, images and links. These last two are very similar: we'll prompt the user to enter the source of the image/link before inserting the tag:

//[...]
case 'createLink':
    var link = prompt('Please specify the link.');
    if(link)
    {
        document.execCommand('createLink', false, link);
    }
break;
									
case 'insertImage':
    var src = prompt('Please specify the link of the image.');
    if(src)
    {
        document.execCommand('insertImage', false, src);
    }
break;
//[...]

Titles are trickier to insert: Internet Explorer doesn't understand the command heading!

So, we have to perform a try/catch: if the original command fails, we'll use inside the catch another one: formatBlock.

//[...]
case 'heading':
    try
    {
        document.execCommand(tag, false, this.getAttribute('data-value'));
    }
    catch(e)
    {
        //The browser doesn't support "heading" command, we use an alternative
        document.execCommand('formatBlock', false, '<'+this.getAttribute('data-value')+'>');
    }
break;
//[...]

7. Conclusion

We have developed a WYSIWYG editor very simply, haven't we? But we have to admit that we can improve it. Here are some suggestions:

  • Adding some buttons: one for changing the color of text, another one for aligning the content etc. Don't forget to consult the documentation of commands!
  • Just alerting the HTML code by clicking the corresponding button is not very convenient: we should modify that!
  • Why not create a method returning the BBCode of the content?
  • A WYSIWYG is often a form element: we should allow this form to transfer our element's content during its submission.
Published March 19, 2012