Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make multiple contenteditables behave like one document

I have a bunch of multiline contenteditable divs arranged vertically, and I want to allow for natural navigation between them by arrow keys (as if it was one document). For that, on keydown event I need to:

  • Know current row of caret and number of rows to determine if we need to move up (first line and ↑ key pressed) or down (last line and ↓ key)
  • Know current character (position in a shown string) to determine if we need to move up (position==0 and ← key pressed) or down (position==text.length and → pressed)
  • The process should not stop between switching elements when the key is being held and not released (hence keydown event, not keyup)
  • Preferrably: The event should stop propagating (for example, if I'm on the first column on last row and I press ↓ key, it should not jump to the last character on the line and then go down)
  • Preferrably (would be really awesome): After we jump to the next element, we would not just .focus() the element, but emulate a click in the same vertical position as where we were before, so that it would feel natural, like in text editors.

All scripts/libraries I had found to date are either not doing all things I need or buggy. Please include demos in your suggestions, so that I can test without incorporating in my code first. Thanks!

Update: Visual explanation - note that there are more than 2 divs and 'arrow down key on the last line' is just one of the four triggers

enter image description here

like image 584
Serge Uvarov Avatar asked Jul 16 '16 14:07

Serge Uvarov


Video Answer


1 Answers

I already wrote some code but it's not finished at all... Maybe you can start with that and try to complete what I've done if you want ;) I'll continue working on it this week in order to provide you with a solution... Here is what I've done so far :

var ajaxResult = [
  "Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates  exitialis certamina cogebatur",
  "Inter has ruinarum varietates a Nisibi quam tuebatur accitus",
  "Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates a Nisibi quamos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur. Inter has ruinarum varietates  exitialis certamina cogebatur",
];
/*************************************************************
* 
*	LIST OF CONTENT EDITABLE DIVS MANAGEMENT
*
**************************************************************/
// Create the editable divs
window.onload = function(){
  var contentEditables = createContentEditables();
  document.body.appendChild(contentEditables);
}

// Remember all the content editable elements in the order they appear in the dom
var _currentEdit,
	_edits = [];
	
function createContentEditables(){
  var div;
  var result = document.createDocumentFragment();
  for (var i = 0, n = ajaxResult.length ; i < n ; i++){
    div = createContentEditable(ajaxResult[i]);
    _edits.push(div);
    result.appendChild(div);
  }

  return result;
}

function getPreviousEdit(edit){
  // Search for the edit index
  var index = _edits.indexOf(edit);

  if(index == 0)
    return;

  // Return the previous one
  return _edits[index - 1];
}

function getNextEdit(edit){
  // Search for the edit index
  var index = _edits.indexOf(edit);

  if(index == _edits.length - 1)
    return;

  // Return the previous one
  return _edits[index + 1];
}

/*************************************************************
* 
*	CONTENT EDITABLE MANAGEMENT
*
**************************************************************/
// We need to define the line height of the div to be able to retrieve the number of lines
var LINE_HEIGHT = 16;

// variables to keep trace of relevant information about the div
var _lines, _caretPosition;

/*
* Create a div with contenteditable set to true with the text
* received from the server
*/
function createContentEditable(text){
  var element =  document.createElement('div');
  element.className = 'contenteditable';
  element.innerHTML = text;
  element.style.lineHeight = LINE_HEIGHT + 'px';
  element.setAttribute('contenteditable', true);

  // Set listeners
  element.addEventListener('mouseup', onEdit_mouseup);
  element.addEventListener('keydown', onEdit_keydown);
  element.addEventListener('focus', onEdit_focus);

  return element;
}

function onEdit_keydown(domEvent){
  // Update caret position
  _caretPosition = getCaretPosition(domEvent.target);
  switch(domEvent.keyCode){
    case 37: // left arrow
      if (_caretPosition.index == 0){
        var previousEdit = getPreviousEdit(domEvent.target);
        if(previousEdit){
          console.log("go to end of previous edit");
          console.log(previousEdit);
          previousEdit.focus();
        }
      }
      break;
    case 38: // up arrow
      if (_caretPosition.line == 1){
        var previousEdit = getPreviousEdit(domEvent.target);
        if(previousEdit){
          console.log("go to previous edit keeping the caret offset");
          console.log(previousEdit);
          previousEdit.focus();
        }
      }
      break;
    case 39: // right arrow
      if (_caretPosition.index == domEvent.target.innerHTML.length){
        var nextEdit = getNextEdit(domEvent.target);
        if(nextEdit){
          console.log("go to beginning of next edit");
          console.log(nextEdit);
          nextEdit.focus();
        }
      }
      break;
    case 40: // down arrow
      if (_caretPosition.line == getLines(domEvent.target)){
        var nextEdit = getNextEdit(domEvent.target);
        if(nextEdit){
          console.log("go to next edit keeping the caret offset");
          console.log(nextEdit);
          nextEdit.focus();
        }
      }
      break;
  }
}

function onEdit_mouseup(domEvent){
  // Update caret position
  _caretPosition = getCaretPosition(domEvent.target);
}

function onEdit_focus(domEvent){
  // Add listeners
  _currentEdit = domEvent.target;
  _currentEdit.addEventListener('blur', onEdit_blur);
  window.addEventListener('resize', onWindow_resize);
}

function onEdit_blur(domEvent){
  // Remove listeners
  domEvent.target.removeEventListener('blur', onEdit_blur);
  window.removeEventListener('resize', onWindow_resize);
}

function onWindow_resize(domEvent){
  // Update caret position
  _caretPosition = getCaretPosition(_currentEdit);
}

/*************************************************************
* 
*	HELPERS
*
**************************************************************/
//http://stackoverflow.com/questions/4811822/get-a-ranges-start-and-end-offsets-relative-to-its-parent-container/4812022#4812022
//http://stackoverflow.com/questions/5528004/how-to-get-number-of-rows-in-contenteditable-area-and-current-caret-line-positio
function getCaretPosition(element){
  var caretPosition = {index: 0, line: 0};
  var doc = element.ownerDocument || element.document;
  var win = doc.defaultView || doc.parentWindow;
  var elemOffsetTop = element.offsetTop;
  var sel;
  // Get the x position of the caret
  if (typeof win.getSelection != "undefined") {
    sel = win.getSelection();
    if (sel.rangeCount > 0) {
      var range = win.getSelection().getRangeAt(0);
      // Retrieve the current line
      var rects = range.getClientRects();
      var caretOffsetTop;
      if (typeof rects[1] != "undefined"){
        caretOffsetTop = rects[1].top;
      }
      else if (typeof rects[0] != "undefined"){
        caretOffsetTop = rects[0].top;
      }
      else{
        // Create dummy element to get y position of the caret
        var dummy = document.createElement('CANVAS');
        dummy.id = 'findCaretHelper';
        range.insertNode(dummy);
        caretOffsetTop = dummy.offsetTop;
        element.removeChild(dummy);
      }

      var preCaretRange = range.cloneRange();
      preCaretRange.selectNodeContents(element);
      preCaretRange.setEnd(range.endContainer, range.endOffset);

      // Remember caret position
      caretPosition.index = preCaretRange.toString().length;
      caretPosition.line = Math.ceil((caretOffsetTop - elemOffsetTop)/LINE_HEIGHT) + 1;
    }
  } 
  // support ie
  //else if ( (sel = doc.selection) && sel.type != "Control") {
  //var textRange = sel.createRange();
  //var preCaretTextRange = doc.body.createTextRange();
  //preCaretTextRange.moveToElementText(element);
  //preCaretTextRange.setEndPoint("EndToEnd", textRange);
  //caretPosition.x = preCaretTextRange.text.length;
  //}

  return caretPosition;
}

function getLines(element){	
  return element.clientHeight/LINE_HEIGHT;;
}
.contenteditable{
  border: solid 1px #aaa;
  margin: 10px 0;
}

I managed getting information about the current line, the current character index in the content editable div and some other stuff... I still have to work on focusing an other content editable div in order to put the caret at the right place... I hope this beginning of a solution will help you!

like image 94
R. Foubert Avatar answered Oct 14 '22 07:10

R. Foubert