HTML5 WYSIWYG Editor


Screen shot of WYSIWYG contenxt-menu

Demo here. Gist here

Most of the articles you will come across about the latest in web development talk almost exclusively about HTML5, almost as though it were the only technology that existed in making web site. You will hear/read much less about CSS3, JavaScript, and the DOM (which has a lot of overlap with HTML, but is a separate thing).

HTML5 did bring some new elements and attributes, but <canvas> for example, is useless without JavaScript. Among the new features, I find very little information talking about features not relating to media and gaming (WebGL, Canvas, Audio, Video), and I find that these are often the most interesting and the most useful.

Contenteditable

contenteditable magically turns your browser into a WYSIWYG editor. Set this attribute to true, and you suddenly have the ability to write HTML with no coding skills required. Just click and type. Surprisingly, this is not at all a new feature, having been supported in Internet Explorer back as far as version 5.5! It can be set on any element that has content, and has a sibling attribute called desginMode that can be applied to document to make the entire page editable. It is also worth noting that you get little red squiggly lines on spelling errors.

Both of these are useful, but we want a fully featured WYSIWYG editor, not a simple text editor!

Enter execCommand()

document.execComand() is the JavaScript component of our WYSIWYG editor. It is the DOM API that allows us to add style, links, images, and other formatting to something that is contenteditable. The function accepts one to three arguments (the second of which can always be set to null), and provides features ranging from styling selected text using tags for text style (<i> for italics,<b>  for bold, etc) and other attributes where necessary. You can also set fonts, font sizes, add images and links. It has undo and redo, as well as clipboard features for copy and paste (though those require special configurations in the browser or signed JavaScript).

It will use the selected text if available, or insert a new element at the cursor in many case if not. Things like links and images which require additional attributes such as src or href as the third argument to the function. See the Mozilla Developer Network documentation for details.

But with such an extensive variety of features, how can we make all of this fit?

That's where contextmenu comes in

Context menus (what pops up when you right-click) are a great way to provide a whole bunch of things to click on while taking up literally no room at all... at least until you right-click on something.

Using a custom context-menu comes in two parts;

  1. Creating the menu
  2. Setting an attribute on what that menu will be used on.

Setting the attribute is pretty easy… Just set the contextmenu attribute to the ID of the menu you want to use there. It really is that easy!

<menu type="context" id="my_menu">
<!--Menu items here-->
</menu>
<div contextmenu="my_menu">
<!--Content here-->
</div>

Creating the menu is pretty straight-forward as well. First, you need a <menu> element with it's type set to context and it's ID set to match the contextmenu attribute of whatever will be using it. It has two types of children, another <menu> or <menuitem>. The parent menu element requires that you set the type and id attributes, all menus under that should have a label attribute (which becomes the text displayed). Menu items should have a label as well, and can also have icon and type attributes. Available types are default of command, checkbox, or radio.

Unfortunately, contextmenus are only supported in Firefox for now. If you want this to work, you will have to find a way of adding support (contextmenu listeners) to other browsers or create drop-down menus, or just create a whole mess of buttons.

For further reading, see w3schools.com and the article by David Walsh. Also, I use contextmenus here (if you are using Firefox, right-click to see a simple example and take a look in Inspector... It will not be visible in source code).

So, we now have a context menu, but it requires a bit of JavaScript using on* attributes or addEventListner. That sure sounds like a lot of JavaScript, right?

Wrong! We have dataset

Using dataset, we can add another short attribute into the <menuitem>s and have the event listeners use data-* attributes, meaning another attribute or two on the <menuitem> saves us from writing each case in JavaScript.

To use dataset, you will need to set the attribute on any elements you'd like to use it with, and you'll need a little bit of simple JavaScript to access the data it contains. The attributes must begin with data- followed by lowercase characters (no uppercase!) and hyphens (-). The JavaScript component drops the data- part and converts the rest into camelCase (so, data-my-property="Hello World!" would be Element.dataset.myProperty).

If necessary (which won't be the case here, but you might find it useful elsewhere), you can easily add support for dataset to unsupported using getAttribute() and setAttribute(). I personally like the idea of using [data-*]:after in CSS and setting content: ' 'attr(data-icon)' ' on it and setting the font-family to something that is useful as icons.

Further reading, again at Mozilla Developer Network.

Putting it all together

At this point, you should have at least a basic understanding of contenteditable, execCommand, contextmenus, and data-*/dataset. We can combine all of these together to make a WYSIWYG editor that requires minimal code and can have new features added by adding a single line of HTML.

First, create a menu. This menu should contain several other <menu> elements with short and descriptive labels (E.G., Indentation). Give this an ID that relates to its use, and I would suggest tacking on a _menu to avoid situations where something else might have that same ID.

Next, create menuitems for each of the commands that fall into each of these categories. If you happen to have a few commands that don't fit into any of your categories, that is fine, but keep in mind that space and organization will make using this thing a lot more pleasant. Each of these should have short and descriptive label (E.G., Italics), one or more data-* attributes, and an optional icon.

Next, do something like document.querySelectorAll('#wysiwyg_menu menuitem[data-exec-command]'  and loop through the NodeList doing menuitem.addEventListener('click', ...) on each of the results. You can write the function directly into the listener or give it an existing function as the callback.

In the callback for this listener, you will want have a section that runs execCommand(this.dataset.execCommand, null, [optional third argument]) , using menuitem.dataset to get the arguments. Remember that execCommand() takes one to three arguments, and the second can always be left as null (it actually isn't even implemented in Firefox, which is the only browser that uses contextmenu to begin with, so we can simply ignore that).

Some commands, such as settings links, require the third command and some others such as italics do not. Additionally, something like a link should not get its value directly from menuitem.dataset (could you imagine adding a menuitem for every possibly link?) but should instead ask for more input. In cases such as links, I set data-prompt to the message to display to a user, and use the return value (user input). In your listener, check for this.dataset.prompt and get the value of the third argument from prompt(this.dataset.prompt).

Other commands, such as heading, have only a small number of possibilities and it does make sense to write cases for each of these. Headings only range from H1 to H6, so we can set something like data-exec-third  (or whatever you want to call it) for the third argument to execCommand().

Finally, if there is nothing requiring user input or static values for the third argument, we can ignore it or set it to null. We can finally execute execCommand(this.dataset.execCommand, null, valueFromOtherDataSet) .

All that remains now is to set the contenteditable="true" and contextmenu="wysiwyg_menu" on whatever we want the WYSIWYG editor to work on. If we only want some of the content to be editable, we can set contenteditable on each section that we want to work with out editor, and contextment on the parent element, since both of these properties are inherited.

"That's all folks!"

Actually doing something with whatever content you create with this is easily worth its own separate post. As a short and simple suggestion, you could use AJAX and formData(), using form.append('some_name', Element.innerHTML).

Being as this is only my third post and I've learned quite a few things that I think are worth sharing, there are a few things that I have in mind for next time. Likely candidates for my next article include Mutation Observers and sending json_encode'd responses in PHP and handling them in JavaScript in AJAX requests.