This tutorial shows you how to create your own html editor with pure JavaScript. We are going to build a simple html editor that is supposed to be used by website administrators.
This simple editor (plugin) works with html input and has a button to toggle a preview of the visual apperance. Html tags will be placed at the cursor position or surround selected text.
This plugin has been tested and is working with Google Chrome (75.0.3770.100), Mozilla Firefox (67.0.4), Microsoft Edge (42.17134.1.0) and Internet Explorer (11.829.17134.0), without any polyfill. If you want to support older browsers, check out our post on transpilation and polyfilling of JavaScript.
JavaScript
This class includes a constructor to create a editor, you can have many editors in the same form. We have added event handlers for all buttons in the plugin and included public methods to add a tag, to toggle preview, to minify html and to unminify html.
var annytab = annytab || {};
annytab.html = annytab.html || {};
annytab.html.editor = (function () {
'use_strict';
// Constructor
function editor(opts)
{
// Set default values for parameters
opts = opts || {};
// Set options
this.options = { button_container: 'htmlButtonPanel', textarea: 'txtContents', preview: 'txtPreview', button_class: 'annytab-form-button' };
for (var option in this.options)
{
if (opts.hasOwnProperty(option) === true)
{
this.options[option] = opts[option];
}
}
// Get references
this.container = document.getElementById(this.options.button_container);
this.ta = document.getElementById(this.options.textarea);
this.pw = document.getElementById(this.options.preview);
// Add buttons
var html = '<input type="button" class="' + this.options.button_class + '" style="font-weight:bold;" value="b" />';
html += '<input type="button" class="' + this.options.button_class + '" style="font-style:italic" value="i" />';
html += '<input type="button" class="' + this.options.button_class + '" value="br" />';
html += '<input type="button" class="' + this.options.button_class + '" value="h1" />';
html += '<input type="button" class="' + this.options.button_class + '" value="h2" />';
html += '<input type="button" class="' + this.options.button_class + '" value="h3" />';
html += '<input type="button" class="' + this.options.button_class + '" value="h4" />';
html += '<input type="button" class="' + this.options.button_class + '" value="p" />';
html += '<input type="button" class="' + this.options.button_class + '" value="code" />';
html += '<input type="button" class="' + this.options.button_class + '" value="span" />';
html += '<input type="button" class="' + this.options.button_class + '" value="div" />';
html += '<input type="button" class="' + this.options.button_class + '" value="line" />';
html += '<input type="button" class="' + this.options.button_class + '" value="space" />';
html += '<input type="button" class="' + this.options.button_class + '" value="url" />';
html += '<input type="button" class="' + this.options.button_class + '" value="img" />';
html += '<input type="button" class="' + this.options.button_class + '" value="font" />';
html += '<input type="button" class="' + this.options.button_class + '" value="list" />';
html += '<input type="button" class="' + this.options.button_class + '" value="table" /><br />';
html += '<div id="annytab-html-editor-preview" class="' + this.options.button_class + '">☊</div>';
html += '<div id="annytab-html-editor-minify" class="' + this.options.button_class + '">−</div>';
html += '<div id="annytab-html-editor-unminify" class="' + this.options.button_class + '">+</div>';
// Add html to the container (fastest way)
this.container.insertAdjacentHTML('beforeend', html);
// Add events
addEvents(this, this.container);
} // End of the constructor
// Toggle preview
editor.prototype.togglePreview = function (button) {
// Get buttons
var buttons = this.container.querySelectorAll('input');
// Check if the textarea is visible
if (isVisible(this.ta) === true) {
// Set preview window content
this.pw.innerHTML = this.ta.value;
// Set button text
button.innerHTML = '<>';
// Disable buttons
for (var i = 0; i < buttons.length; i++) {
buttons[i].setAttribute('disabled', true);
}
// Display preview window
this.ta.setAttribute('hidden', true);
this.pw.removeAttribute('hidden');
}
else
{
// Set button text
button.innerHTML = '☊';
// Enable buttons
for (i = 0; i < buttons.length; i++)
{
buttons[i].removeAttribute('disabled');
}
// Display textarea
this.ta.removeAttribute('hidden');
this.pw.setAttribute('hidden', true);
}
}; // End of the togglePreview method
// Minify html
editor.prototype.minify = function () {
// Remove tabs and line breaks
this.ta.value = this.ta.value.replace(/(?!<pre[^>]*?>)([\t](?!@))(?![^<]*?<\/pre>)/g, '');
this.ta.value = this.ta.value.replace(/(?!<pre[^>]*?>)([\r\n](?!@))(?![^<]*?<\/pre>)/g, '');
this.ta.value = this.ta.value.replace(/(?!<pre[^>]*?>)([\r](?!@))(?![^<]*?<\/pre>)/g, '');
this.ta.value = this.ta.value.replace(/(?!<pre[^>]*?>)([\n](?!@))(?![^<]*?<\/pre>)/g, '');
}; // End of the minify method
// Unminify html
editor.prototype.unminify = function () {
// Variables
var step = 0;
var content = '';
// Get all nodes
var nodes = this.ta.value.replace(/<\//g, '~~~</').replace(/</g, '~~~<').replace(/>/g, '>~~~').split('~~~');
// Loop all nodes
for (var i = 0; i < nodes.length; i++)
{
// Do not parse empty strings
if (nodes[i] === '') {
continue;
}
// Decrease step
if (nodes[i].search(/<\/[^/>]+>/g) !== -1) // End tag
{
step -= 1;
}
// Append to content
content += getTabs(step) + nodes[i] + '\n';
// Increase step
if (nodes[i].search(/<[^/>]+>/g) !== -1) // Start tag
{
step += 1;
}
}
// Set unminified content
this.ta.value = content;
}; // End of the unminify method
// Add a tag
editor.prototype.addTag = function (button)
{
// Insert the tag
if (button.value === "b") {
surroundSelectedText(this.ta, '<b>', '</b>');
}
else if (button.value === "i") {
surroundSelectedText(this.ta, '<i>', '</i>');
}
else if (button.value === "br") {
insertText(this.ta, '<br />');
}
else if (button.value === "h1") {
surroundSelectedText(this.ta, '<h1>', '</h1>');
}
else if (button.value === "h2") {
surroundSelectedText(this.ta, '<h2>', '</h2>');
}
else if (button.value === "h3") {
surroundSelectedText(this.ta, '<h3>', '</h3>');
}
else if (button.value === "h4") {
surroundSelectedText(this.ta, '<h4>', '</h4>');
}
else if (button.value === "p") {
surroundSelectedText(this.ta, '<p>', '</p>');
}
else if (button.value === "code") {
surroundSelectedText(this.ta, '<pre class="prettyprint annytab-code-container">', '</pre>');
}
else if (button.value === "span") {
surroundSelectedText(this.ta, '<span>', '</span>');
}
else if (button.value === "div") {
surroundSelectedText(this.ta, '<div>', '</div>');
}
else if (button.value === "line") {
surroundSelectedText(this.ta, '<div class="annytab-basic-line">', '</div>');
}
else if (button.value === "space") {
surroundSelectedText(this.ta, '<div class="annytab-basic-space">', '</div>');
}
else if (button.value === "url") {
surroundSelectedText(this.ta, '<a href="http://www.annytab.se" rel="nofollow" target="_blank">', '</a>');
}
else if (button.value === "img") {
surroundSelectedText(this.ta, '<img src="/source.jpg" style="max-width:100%;" />', '');
}
else if (button.value === "font") {
surroundSelectedText(this.ta, '<span style="font-family:Arial;color:#ff0000;">', '</span>');
}
else if (button.value === "list") {
insertText(this.ta, '<ul><li>r1</li><li>r2</li></ul>');
}
else if (button.value === "table") {
insertText(this.ta, '<table style="width:400px;text-align:center;"><tr><th>r1:c1</th><th>r1:c2</th></tr><tr><td>r2:c1</td><td>r2:c2</td></tr></table>');
}
}; // End of the addTag method
// Add events
function addEvents(ec, container)
{
// Get buttons
var buttons = container.querySelectorAll('input');
var pwb = container.querySelector('#annytab-html-editor-preview');
var umb = container.querySelector('#annytab-html-editor-unminify');
var mb = container.querySelector('#annytab-html-editor-minify');
// Add toggle preview click event
window.onload = pwb.addEventListener('click', function (event) {
// Prevent default click behavior
event.preventDefault();
// Preview
ec.togglePreview(this);
}, false);
// Add minify click event
window.onload = mb.addEventListener('click', function (event) {
// Prevent default click behavior
event.preventDefault();
// Minify html
ec.minify();
}, false);
// Add unminify click event
window.onload = umb.addEventListener('click', function (event) {
// Prevent default click behavior
event.preventDefault();
// Unminify html
ec.unminify();
}, false);
// Loop buttons
for (var i = 0; i < buttons.length; i++)
{
// Add a click event
window.onload = buttons[i].addEventListener('click', function (event) {
// Prevent default click behavior
event.preventDefault();
// Add a tag
ec.addTag(this);
}, false);
}
} // End of the addEvents method
// Get a selection
function getSelection(el)
{
var start = el.selectionStart;
var end = el.selectionEnd;
return {
start: start,
end: end,
length: end - start,
text: el.value.slice(start, end)
};
} // End of the getSelection method
// Surround selected text
function surroundSelectedText(el, before, after)
{
// Get selection
var selection = getSelection(el);
// Replace text
el.value = el.value.slice(0, selection.start) + before + selection.text + after + el.value.slice(selection.end);
// Set cursor
el.selectionEnd = selection.end + before.length + after.length;
el.blur();
el.focus();
} // End of the surrondSelectedText method
// Insert text at selection
function insertText(el, text) {
// Get selection
var selection = getSelection(el);
// Insert text
el.value = el.value.slice(0, selection.start) + text + el.value.slice(selection.end);
// Set cursor
el.selectionEnd = selection.end + text.length;
el.blur();
el.focus();
} // End of the insertText method
// Check if an element is visible
function isVisible(el)
{
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
} // End of the isVisible method
// Get tabs
function getTabs(step) {
var tabs = '';
for (var i = 0; i < step; i++) {
tabs += '\t';
}
return tabs;
} // End of the getTabs method
// Return this object
return editor;
})();
How to use the plugin
You need to add a container for buttons, a textarea and a preview container for each editor that you want to add to your form. Each container needs an id, these id:s is added as options to the constructor.
<div class="annytab-form-label">Textarea</div>
<div id="htmlButtonPanel" style="border: 1px solid #808080;border-bottom: 0;background-color: #e9e9e9;padding: 0 10px 10px 10px;"></div>
<textarea id="txtContents" name="txtContents" style="width:100%;height:400px;padding:10px;border: 1px solid #808080;font-family:Arial;font-size:16px;line-height:24px;"></textarea>
<div id="txtPreview" style="width:100%;height:400px;padding:10px;border: 1px solid #808080;font-size:16px;line-height:24px;overflow:auto;" hidden></div>
<div class="annytab-form-label">Textarea</div>
<div id="htmlButtonPanel2" style="border: 1px solid #808080;border-bottom: 0;background-color: #e9e9e9;padding: 0 10px 10px 10px;"></div>
<textarea id="txtContents2" name="txtContents" style="width:100%;height:400px;padding:10px;border: 1px solid #808080;font-family:Arial;font-size:16px;line-height:24px;"></textarea>
<div id="txtPreview2" style="width:100%;height:400px;padding:10px;border: 1px solid #808080;font-size:16px;line-height:24px;overflow:auto;" hidden></div>
Create new editors in a script tag and add options to the constructor, default values is used if options not is added. You may want to minify html before you submit your form, you can call the public minify method on the plugin to do so.
<script src="/js/annytab-shared/annytab.html.editor.js"></script>
<script>
var options = { button_container: 'htmlButtonPanel', textarea: 'txtContents', preview: 'txtPreview', button_class: 'annytab-form-button' };
var panel = new annytab.html.editor(options);
var panel2 = new annytab.html.editor({ button_container: 'htmlButtonPanel2', textarea: 'txtContents2', preview: 'txtPreview2', button_class: 'annytab-form-button' });
// Public methods
panel.minify();
panel.unminify();
</script>