Jump to main content

Using JavaScript & contenteditable

  • Published:
  • Updated:
  • Category: JavaScript

Over the past few months I’ve been making more time to teach myself JavaScript without the guardrails of jQuery.

It’s been fun. A little confusing at times. But, I’m actually surprised at how much JavaScript I “knew” just from meddling around with other people’s code, and from my firm grasp on Sass’s Control Directives. The Control Directives in particular really made some of the parallel concepts of JavaScript click for me.

I see what you did there… but what about this?

In a lot of the tutorial websites and books that I’ve been reading, there isn’t a whole lot of time spent on actual node manipulation (at least, I haven’t gotten to them yet?). As someone who learns best by seeing the results of what he codes, a lot of the JS that I was learning were more behind the scenes, math functions and variable checks. These are obviously important concepts to master. But returning all values via console.log was doing nothing to help me bridge the gap between what I was learning, and how I would want to implement it.

So, instead of being discouraged from not getting what I needed to really understand what I was learning vs what I wanted to do, I left the tutorials and moved over to Stack Overflow to search for the missing pieces.

And what is it I wanted to do? Well, I wanted to muck around with the idea of creating editable content outside of standard form controls.

Making an Editable HTML Element

Making a editable element in HTML isn’t all that difficult. You can add the contenteditable="true" HTML attribute to the element (a <div> for example) that you want to be editable.

On the topic of editable elements...

If you're anticipating a user to only update a word or two within a paragraph, then you could make a <p> itself editable. However, if you're providing an area for a user to add long-form content, a div may be more appropriate, as browsers handle character returns differently.

For instance, Safari injects divs for each line break, where as Firefox injects br elements. If editing a p, divs are invalid child elements and could lead to some unexpected and undesired rendering quirks.

Beyond just editing content, I wanted to be able to temporarily save changes and revert state (or undo) the modified content.

Here’s where I wanted to apply the JavaScript I had been learning. So, I started with what I knew well, the HTML and CSS…

The HTML and CSS

To start, here’s the basic HTML I wanted to work with for this exercise:

<div id="container" class="container">
  <p>
    Edit the following content as you see fit...
  </p>
  <div contenteditable="true" id="myContent">
    This is the editable content.
  </div>
  <button class="btn" id="undo" disabled>Undo Changes</button>
  <button class="btn" id="save" disabled>Save Content</button>
</div>

The content I want to edit is contained within the div with contenteditable="true". Without any JavaScript, browsers that support this attribute will allow you to modify the content as is. Pretty neat!

Next I have two buttons which are initially disabled. The reason for this is that I want people to be aware that these controls will exist, but due to there being no changes, they should not be currently actionable or accessible.

Next, here’s some quick and dirty CSS to give it the UI a semblance of styling:

html, body {
  font-family: arial;
  font-size: 110%;
  margin: 0;
  padding: 0;
}

.container {
  margin: auto;
  padding: 20px;
  position: relative;
  width: 50%;
}

[contenteditable] {
  font-size: 26px;
  padding: .25em 0em;
  margin: 1em 0;
  transition: padding .3s ease-in-out;
}

[contenteditable]:hover,
[contenteditable]:focus {
  padding: .25em;
}

[contenteditable]:hover {
  background: #fafafa;
  outline: 2px solid #eee;
}

[contenteditable]:focus {
  background: #efefef;
  outline: 2px solid blue;
}

.btn {
  background: #fff;
  color: darkgreen;
  border: 1px solid darkgreen;
  box-shadow: inset 0 0 0 1px;
  font-size: 1em;
  padding: .75em;
  margin-right: .5em;
}

.btn[disabled] {
  color: #666;
  border-color: #ddd;
  opacity: .5;
}

.btn:not([disabled]):hover,
.btn:not([disabled]):focus {
  outline: 3px solid;
  outline-offset: 1px;
}

.btn:not([disabled]):active {
  background: darkgreen;
  color: #fff;
}

Pretty straight forward. I use attribute selectors to help me style the UI, instead of creating additional classes that would, in some cases, need to be toggled by JavaScript depending on the element’s state.

I also make sure to provide :hover, :focus, and :active styles to ensure that users get the appropriate visual feedback as they interact with the UI.

But now that I have the markup and presentational layer out of the way, it’s time to dive into the JavaScript…

(function ( doc ) {
  'use strict';
  // Use a more terse method for getting by id
  function getById ( id_string ) {
    return doc.getElementById(id_string);
  }

  function insertAfter( newEl, refEl ) {
    refEl.parentNode.insertBefore(newEl, refEl.nextSibling);
  }

  var editElement = getById('myContent');
  var undoBtn = getById('undo');
  var saveBtn = getById('save');
  var originalContent = editElement.innerHTML;
  var updatedContent = "";

  // if a user has refreshed the page, these declarations
  // will make sure everything is back to square one.
  undoBtn.disabled = true;
  saveBtn.disabled = true;

  // create a redo button
  var redoBtn = doc.createElement('button');
  var redoLabel = doc.createTextNode('Redo');
  redoBtn.id = 'redo';
  redoBtn.className = 'btn';
  redoBtn.hidden = true;
  redoBtn.appendChild(redoLabel);
  insertAfter( redoBtn, undo );

  // if the content has been changed, enable the save button
  editElement.addEventListener('keypress', function () {
    if ( editElement.innerHTML !== originalContent ) {
      saveBtn.disabled = false;
    }
  });

  // on button click, save the updated content
  // to the updatedContent var
  saveBtn.addEventListener('click', function () {
    // updates the myContent block to 'save'
    // the new content to updatedContent var
    updatedContent = getById('myContent').innerHTML;

    if ( updatedContent !== originalContent ) {
      // Show the undo button in the case that you
      // didn't like what you wrote and you want to
      // go back to square one
      undoBtn.disabled = false;
    }
  });

  // If you click the undo button,
  // revert the innerHTML of the contenteditable area to
  // the original statement that was there.
  //
  // Then add in a 'redo' button, to bring back the edited content
  undoBtn.addEventListener('click', function() {
    editElement.innerHTML = originalContent;
    undoBtn.disabled = true;
    redoBtn.hidden = false;
  });

  redoBtn.addEventListener('click', function() {
    editElement.innerHTML = updatedContent;
    this.hidden = true;
    undoBtn.disabled = false;
    undoBtn.focus();
  });

})( document );

Breaking down the JavaScript

Now, let’s look at each piece of the script and talk about what’s going on.

The first thing of note is that I’ve wrapped my code in a Immediately Invoked Function Expression (IIFE).

<script>
(function( doc ){
  // Pass in 'document' as 'doc' to trim down on instances of having
  // to write the word "document" over and over again...
  ...
})( document );
</script>

In JavaScript, all variables are scoped at the function level. Without scoping variables to a particular function (wrapping them within an IIFE) the variables instead get scoped to the global level.

This is not necessarily a problem if your goal is to create global variables. However, depending on the scale of your project, you may end up with many different functions that could potentially have variables with the same name.

Reusing variable names is fine. It happens a lot.

The problem though is that without scoping variables to a particular function, they will be overwritten if you use that same variable name within a different function.

For Example:

<button id="bananafication">Click me</button>

<script>
  // Here's a global variable of 'b'.
  // Globally, I do not want b to be a banana
  var b = "this is not a banana";

  console.log(b);
  // if you look at the console, you'll see that b is set to
  // "this is not a banana"

  // but in this instance, I am setting var 'b' to be a banana
  // my motivation for this does not need to be explained here
  // perhaps it'd be best to read the product spec?
  document.getElementById('bananafication').addEventListener('click', function() {
    var b = "banana!"
    console.log(b);
  });

  // Now the global variable b has been overwritten to be 'banana!'
  // This may not be what we wanted to have happen?
</script>

Enough about that…

Helper functions

To reduce the need for writing redundant functions where everything is the same except for a value or two, small helper functions can be created.

The function getByID accepts the id of an element as a string, and the function locates and returns the DOM element. So instead of having to write document.getElementByID('my_id'), it’s just getById('my_id').

Next, the function insertAfter() will be used to insert the redo button into the DOM. The insertAfter() function accepts a string for the new element that needs to be inserted, and the refEl (reference element) that the new element will be inserted after.

Helper functions like these are great to reduce the redundancy of common tasks, but for someone that is still learning JavaScript, it might be better to just continue to write things out the long way. You know, so it sticks and you aren’t abstracting away what it is you’re trying to learn.

Declaring variables

Using the getById function, I can find elements in the DOM to manipulate. However, instead of using processing power to re-find elements over and over again, I can use variables to store those elements for repeated reference, along with other information that I want to keep track of.

var editElement = getById('myContent');
var undoBtn = getById('undo');
var saveBtn = getById('save');
var originalContent = editElement.innerHTML;
var updatedContent = "";

And here is what each variable is for:

  • editElement is the element that is editable.
  • originalContent stores the original content of the editable region.
  • redoBtn and redoLabel are used in creating the redo button after undo is pressed.
  • saveBtn the button to save the most recent changes.
  • undoBtn keeps track of the Undo button.
  • updatedContent is a placeholder variable that will contain updated content on save.

Disabling the buttons

In the unchanged state, there is nothing to save or undo, so I use the following lines to add the disabled attribute to the save and undo buttons.

undoBtn.disabled = true;
saveBtn.disabled = true;

The disabled buttons will indicate that these controls are currently not functional. Ideally, there would be other text on the page to indicate to people that they must perform a specific action to enable these controls, as some people might find it confusing to have elements on the page that they can’t interact with.

Creating an element

While I could have manually added the redo button to the DOM, I had wanted to take a stab at generating elements with JavaScript. The following lines create my redo button and its text label.

var redoBtn = doc.createElement('button');
var redoLabel = doc.createTextNode('Redo');

Next, I add the necessary attributes to the button. Giving it an id, class, and setting it to be hidden by default.

redoBtn.id = 'redo';
redoBtn.className = 'btn';
redoBtn.hidden = true;

Finally, I add the redoLabel as a child of redoBtn and use my insertAfter helper function to place it after the undo button in the DOM.

redoBtn.appendChild(redoLabel);
insertAfter( redoBtn, undoBtn );

Adding Event Listeners and ifs

Now that all the elements have been setup, it’s time to make these buttons do things.

First, these buttons will continue to be useless if they remain disabled, so the script listens for if someone performs a keypress while focused within the contenteditable element.

editElement.addEventListener('keypress', function () {
  if ( editElement.innerHTML !== originalContent ) {
    saveBtn.disabled = false;
  }
});

Using an if statement, the script looks to see if the innerHTML of the editable element is still the same the originalContent. If so, nothing happens, but if the values no longer match, the disabled attribute is removed from the save button.

In the event there’s something to save, the save button needs to function as well!

saveBtn.addEventListener('click', function () {
  // updates the myContent block to 'save'
  // the new content to updatedContent var
  updatedContent = editElement.innerHTML;

  if ( updatedContent !== originalContent ) {
    // Enable the undo button in the case that you
    // didn't like what you wrote and you want to
    // go back to square one
    undoBtn.disabled = false;
  }
});

An event listener is being added to the button with the id of save. When that button is clicked, or is activated by use of the Space or Enter keys, for keyboard users, the anonymous function is run which saves the inner HTML of myContent (referenced as the editElment variable) to the placeholder variable updatedContent.

The second part of the code, checks if the updated content is actually different than the original. If not, there’s nothing to undo so the undo button is kept disabled. But if the content has changed, then it looks for the element with the ID of undo and removes the disabled attribute.

Finally, in the event the undo button was pressed but then the changed content was needed again, the following removes the hidden attribute from the redo button, and disables the undo button again…cause you can’t undo things twice.

undoBtn.addEventListener('click', function() {
  editElement.innerHTML = originalContent;
  undoBtn.disabled = true;
  redoBtn.hidden = false;
});

Activating the undoBtn changes the innerHTML of the editable element to the content that was stored in originalContent. Since it can’t be undone again, the undo button is reset to disabled, and the redo button is made visible by removing the hidden attribute.

redoBtn.addEventListener('click', function() {
  editElement.innerHTML = updatedContent;
  this.hidden = true;
  undoBtn.disabled = false;
  undoBtn.focus();
});

If the redo button is activated, the editable element is reset to the innerHTML that was stored in updatedContent. The redo button gets the hidden attribute again and the undo button is re-enabled. Since the redo button is hidden to all users (both visually hidden, and hidden to the accessibility API), the line undoBtn.focus() moves keyboard focus to the undo button, ensuring that a user’s keyboard focus isn’t “lost” when the redo button no longer appears in the DOM.

So that’s it!

Check out a working demo here.

Now obviously there is no real “saving” here beyond the single browser session. That would require going into more detail than I am presently ready to talk about. Maybe a topic for another day.

Thanks and updates

A quick shout out to @spmurrayzzz, @wwnjp and @kevincennis who all helped me out while learning / writing about this exercise. Thank you.

This post was originally written back in 2014, but I’ve learned quite a bit since then, about both JavaScript and accessibility. I’ve revised quite a bit of this post to provide some more accurate descriptions of what I was doing here, as well as to modify the code a bit to make it a tad more accessible of a end-user experience. I have more thoughts on the accessibility of contenteditable elements, but that will also have to be a topic for another day…