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