Contenteditable
Rich text editing in the browser!
a short story by Eric Wood
@eric_b_wood
eric@ericwood.org
So...What is it?
Simplest example:
<div contenteditable="true">
<h1>Edit me!</h1>
</div>
Neat!Creating a WYSIWYG for SPiceworks
The journey begins...
Let's not reinvent the wheel!
People have tackled this before. Let's use their stuff.
Neat, wysihtml5 looks like a cool library.
Roadblock #1
From our todo list:
"Admins should be able to edit the post's raw HTML"
wysihtml5:
"Stop that. Let ME decide for you. I'll remove those tags, kthx."
(lots of Github threads about it, maintainer is hardheaded and won't budge)
Solution #1
Modify wysihtml5 to make it optional
if(clean) {
returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), this.config.cleanUp);
} else {
returnValue = htmlOrElement;
}
Copy and Pasting
Users will copy and paste things from EVERYWHERE.
contenteditable will always grab the rich text version from the clipboard.
tl;dr things are going to get messy very quickly
Ideal solution: paste event
There's a paste event we can take over!
elem.onpaste = function(event) {
var html = event.clipboardData.getData('html');
var text = event.clipboardData.getData('text');
};
Caveats:
-
ZERO support in IE
-
Chrome removes formatting in text version
Hack #1
The "phantom textarea" hack
Hack #1 Caveats
- VERY timing-dependent (frail)
- Works great in the lab...not so much in real life
- It's just...gross...
Hack #2
The "nuclear war" approach
- paste event:
- preserve editor contents / caret info
- NUKE IT
- (paste event occurs)
- use wysihtml5 parser to reformat contents
- grab contents
- restore previous contents, caret position
- insert HTML of the cleaned pasted content
- cry
Hack #2 caveats
- Dealing with the caret is flaky (at best)
- Overriding caret behavior can ruin UX
(this one never ended up working well cross-browser)
Let's cut our losses
Sure, clipboardData doesn't work in IE, but we can still use it
if(event.clipboardData) {
event.preventDefault();
var text;
var html = event.clipboardData.getData('text/html');
if(html === '') {
text = event.clipboardData.getData('text/plain');
} else {
text = wysihtml5.dom.htmlToText(html);
}
var result = _.map(text.split('\n'), function(line) {
return '<p>' + line + '</p>';
});
result = result.join('\n');
that.commands.exec('insertHTML', result);
}
HTML to plaintext conversion
Preserving the important stuff
Functions like text() have no regard for whitespace :(
This is a deal breaker!
Implementation
(let the browser do the work for you)
wysihtml5.dom.htmlToText = function(html) {
var wrapper = $('<div style="display: none;" />');
wrapper.html(html);
wrapper.appendTo('body');
var contents = wrapper.find('*');
if(!contents.length) {
return wrapper.text();
}
contents.parent().find('br').replaceWith('\n');
var text = [];
contents.each(function() {
var $el = $(this);
var display = $el.css('display');
if(display === 'block') {
text.push($el.text());
}
});
if(!text.length && contents.length) {
text.push(wrapper.text());
}
wrapper.remove();
text = _.filter(text, function(t) { return /\S/.test(t); });
return text.join('\n');
};