contenteditable

your new best friend

 

 

 

Laurent Perrin @l_perrin

Front — frontapp.com

Before contenteditable

<textarea>

your old pal

  • no formatting

  • resize sucks

Not <textarea>

  • Not maintained
  • Hard to customize
  • Lots of code

How does it work?

  • Add contenteditable="true" to any element
  • Keyboard shortcuts for free

This support only refers to basic editing capability, implementations vary significantly on how certain elements can be edited.

placeholder

[contenteditable=true]:empty::before {
  content: attr(placeholder);
  display: inline;

  color: ui-dimmed-color;
  font-weight: 300;
}

We need a toolbar

document.execCommand

  • bold, italic, underline
  • insertHTML, insertImage
  • createLink
  • fontName, fontSize
  • insertUnorderedList, insertOrderedList

https://developer.mozilla.org/en-US/docs/Web/API/document.execCommand#Commands

execCommand('isBold')?

Nope… we need to track the current selection and inspect the DOM to see what style lies under the cursor. 

http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div

        function getSelectedNodes() {
          var nodes = getAllSelectedNodes(),
              editorNode = getEditor();

          // ignore if the user's selection is not in the editor
          var allInEditor = nodes.every(function (node) {
            return editorNode.find(node).length > 0;
          });

          return allInEditor ? nodes : [];
        }

Make sure everything is in the editor

        // styles can nested
        function nodeHasParent(node, style, root) {
          if (!node || node === root)
            return false;

          if (node.nodeName === style)
            return true;

          return nodeHasParent(node.parentNode, style, root);
        }

Styles can be nested

        function selectionChanged() {
          var nodes = getSelectedNodes(),
              editorNode = getEditor();

          var state = {bold: false, italic: false, underline: false, fontSize: 12};

          if (nodes.length === 0)
            return;

          state.bold = nodes.every(function (node) {
            return nodeHasParent(node, 'B', editorNode);
          });

          state.italic = nodes.every(function (node) {
            return nodeHasParent(node, 'I', editorNode);
          });

          state.underline = nodes.every(function (node) {
            return nodeHasParent(node, 'U', editorNode);
          });

          // now do something with state…
        }
        $document.on('selectionchange', selectionChanged);

Selection woes

  • contenteditable does not save the cursor position.
  • We need to save and restore it whenever we interact with another element.
  function saveSelection() {
    var sel = window.getSelection();

    if (sel.getRangeAt && sel.rangeCount)
      savedSelection = sel.getRangeAt(0);
  }

  function restoreSelection() {
    if (!savedSelection)
      return;

    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(savedSelection);
    savedSelection = null;
  }


  // capture on mousedown so it happens before "blur"
  $('.toolbar button').on('mousedown', saveSelection);
  $('#prompt-url button.btn-primary').click(function () {
    var url = $('#prompt-url input').val();

    if (url) {
      var link = '<a href="' + encodeURI(url) + '">' + url + '</a>';

      // restore the position of the cursor
      $('[contenteditable=true').focus();
      restoreSelection();
      document.execCommand('insertHTML', false, link);
    }
  });

execCommand('fontSize')?

yes!

But…

The actual size depends on the browser

var fontSizes = {1: 10, 2: 12, 3: 14, 4: 18, 5: 24, 6: 32, 7: 48};

function forceFontSize(editor, size) {
  // execCommand inserts <font size="4"> which appears way too small in Safari
  // http://stackoverflow.com/questions/5868295/document-execcommand-fontsize-in-pixels
  var fontElements = document.getElementsByTagName('font'),
      targetSize = fontSizes[size] + 'px';

  size = size.toString();

  for (var i = 0, len = fontElements.length; i < len; ++i) {
    if (fontElements[i].size === size) {
      fontElements[i].removeAttribute('size');
      fontElements[i].style.fontSize = targetSize;
    }
  }
}

Solution: crawl the DOM to use explicity CSS instead.

Closing thoughts

  • contenteditable: powerful but lots of caveats.
  • better than it used to be (apple-style-span)
  • use it if you want deep control on your UX
  • I spared you: copy/paste, detecting current font size

And sanitize your inputs!

Laurent Perrin

CTO Front — frontapp.com

@l_perrin

https://github.com/lperrin/talk-contenteditable

contenteditable

By Laurent Perrin

contenteditable

  • 2,233