Thanks to harsha and Tim Down I finally got this to work and also fixed the issue where Firefox doesn't allow spaces, and it works quite well. The only non-expected behavior I could find was that it messes up the browser's undo history but other than that it works very well.
I've tested in every browser but IE, but I don't see why it wouldn't work in newer IE versions also. It requires use of the input event though, so it wouldn't work in older browsers. I also don't know how it handles line breaks because I strip them in my app.
First, you need this code written by Tim Down somewhere in your file.
var saveSelection, restoreSelection;
var endSpaceIndex = -1;
if (window.getSelection && document.createRange) {
saveSelection = function(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
restoreSelection = function(containerEl, savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [containerEl], node, foundStart = false, stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
} else if (document.selection) {
saveSelection = function(containerEl) {
var selectedTextRange = document.selection.createRange();
var preSelectionTextRange = document.body.createTextRange();
preSelectionTextRange.moveToElementText(containerEl);
preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
var start = preSelectionTextRange.text.length;
return {
start: start,
end: start + selectedTextRange.text.length
}
};
restoreSelection = function(containerEl, savedSel) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(containerEl);
textRange.collapse(true);
textRange.moveEnd("character", savedSel.end);
textRange.moveStart("character", savedSel.start);
textRange.select();
};
}
Somewhere you also need the following code. It uses jQuery, so if you don't have that you'll have to get rid of the jQuery in the example below. This needs to be bound to the input
event, as Firefox is very inconsistent with when it calls keyup
and keypress
. This is what fixes the issue with Firefox not allowing spaces. It's pretty messy. But then again, apparently so is browser support for contenteditable
.
// Provided `element` is the element that you want to modify while the user changes it.
$(element).bind('input', function() {
var savedSel = saveSelection(element);
if (endSpaceIndex > -1 && $(element).text().substr(endSpaceIndex) != " "
&& savedSel.end == $(element).text().length && savedSel.end == savedSel.start) {
$(element).html($(element).text().substr(0, endSpaceIndex) + " " + $(element).text().substr(endSpaceIndex));
endSpaceIndex = -1;
savedSel.end = savedSel.start = $(element).text().length;
}
// Here, change the element however you want to.
// For example, I add a 'red' class around the text that will be chopped off
restoreSelection(element, savedSel);
var fullText = $(element).text();
if (fullText.substr(fullText.length - 1) == " ") {
endSpaceIndex = fullText.length - 1;
}
}
Also I originally marked this as a duplicate, but I no longer think it's a duplicate because it fixes this issue with Firefox not allowing spaces. Hope this helps someone else!