Inserting an element at cursor position in a content editable DIV

I have recently been asked by a customer to implement a document template editor for their webapp and was faced with the problem of inserting database fields into the template editor. The challenge here was that I needed to insert a SPAN node inside a content editable DIV, at the exact location of the cursor inside the DIV.

The layout

To simplify, I basically needed a div that was editable and a list of fields that could be inserted into the editable div

<div class="row">
  <div id="editor" contenteditable="true" class="column">
     Entering some text here. First click within this area so you can set the cursor location, 
     then click on the fields to the right to add them at cursor position
  </div>
  <div class="column">
    <strong>Fields</strong>
    <ul>
      <li data-db-field="fname" class="field">First Name</li>
      <li data-db-field="lname" class="field">Last Name</li>
    </ul>
  </div>
</div>

The logic

The normal logic you would expect is that, as you type into the editor, at some point you want to insert a database field, but clicking away from the editor requires that you remember where the cursor was so that you know where to insert the new node. Therefore, the best course of action is to store the focused element (if null it means that the editor was not clicked) and the cursor location into an object.

We also create two functions, one to store the current position and one to restore the position.

let focusedElement = null;
let cursorPosition = { node: null, offset: 0 };
let editor = document.getElementById("editor")

function storeCursorPosition() {
  if (window.getSelection) {
    selection = window.getSelection();
    if (selection.rangeCount > 0) {
      let range = selection.getRangeAt(0);
      cursorPosition.node = range.startContainer;
      cursorPosition.offset = range.startOffset;
    }
    console.log(cursorPosition)
  }
}
            
            
function restoreCursorPosition(element) {
  let range = document.createRange();
  range.setStart(cursorPosition.node, cursorPosition.offset);
  range.collapse(true);
  let selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(range);
  element.focus();
}

With these in place we can now tell the browser to store the cursor location whenever any of the following events occur on the contenteditable div: focusin, click, input which should cover most use cases. NOTE: you may want to add the events for navigating with keys, in which case you need to monitor for onkeydown event for keyCode:

  • Left: 37
  • Up: 38
  • Right: 39
  • Down: 40
editor.addEventListener("focusin",function(ev){
    focusedElement = ev.target
    storeCursorPosition()
})
editor.addEventListener("input", function(ev){
    focusedElement = ev.target
    storeCursorPosition()
})
editor.addEventListener("click", function(ev){
    focusedElement = ev.target
    storeCursorPosition()
})

Further on, we need to create a function that inserts an element into the editable div, at the exact location of the cursor that we stored:

function insertElementAtCursorPosition(element,targetEl) {
  restoreCursorPosition(targetEl)
  let range = document.getSelection().getRangeAt(0);
  range.insertNode(element)
  storeCursorPosition()
}

notice that first we restore the cursor position which is done in 2 steps: focus on the editable element and set the cursor to the correct location. In order to call the insert element function we need to add onclick event listener to each of the fields

let fields = document.querySelectorAll(".field")
fields.forEach(function(field){
	field.addEventListener('click',function () {
    if (focusedElement) {
      let node = document.createElement('span')
      node.classList.add('inserted-field')
      node.contentEditable = "false"
      node.dataset.dbField = this.dataset.dbField
      node.textContent = this.textContent
      insertElementAtCursorPosition(node, focusedElement)
    } else {
       console.log('No contenteditable element was focused before clicking the button.');
     }
  })
})

notice that, if there was no contenteditable element clicked on first, there will be no action (other then a log in the console) since we don’t know where to insert the new node. Furthermore, I chose to make the span element not editable since we don’t want the user to edit it. This doesn’t mean that it cannot be deleted though.

Conclusion

You can of course adapt the above to your needs but in general this is how to approach the insertion of an element at cursor position into a div that is content editable. You can see a working version in this fiddle:


Posted

in

by