HTML5 did bring some new elements and attributes, but
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!
<i> for italics,
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
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;
- Creating the menu
- 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-->
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
<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).
Wrong! We have dataset
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
data-my-property="Hello World!" would be
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
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
execCommand, contextmenus, and
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
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
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
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
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