How to wrap with HTML tags a cross-boundary DOM selection range?
Right now I'm capturing users' text selections through s = window.getSelection()
and range = s.getRangeAt(0)
(browser's impls aside). Whenever a selection within a <p>
is made, I can easily call range.surroundContents(document.createElement("em"))
to have the selected text wrapped with an <em>
tag.
In this example, however,
<p>This is the Foo paragraph.</p>
<p>This is the Bar paragraph.</p>
<p>This is the Baz paragraph.</p>
when a user makes a text selection from Foo
to Baz
, I cannot call range.surroundContents
: Firefox fails with The boundary-points of a range does not meet specific requirements." code: "1
because the selection is not valid HTML.
In that case, I'd like to somehow obtain the following state in the DOM:
<p>This is the <em>Foo paragraph.</em></p>
<p><em>This is the Bar paragraph.</em></p>
<p><em>This is the Baz</em> paragraph.</p>
Any ideas?
FYI: I've been trying with the Range
API but I can't see a straightforward way of achieving that result. With
var r = document.createRange();
r.setStart(range.startContainer, range.startOffset);
r.setEnd(range.endContainer, range.endOffset+40);
selection.addRange(r);
I can eventually hack something by repositioning the offsets, but only for the 'start' and 'end' containers! (i.e. in this case the Bar
paragraph,开发者_运维问答 how do I wrap it?)
have you tried the following approach (which is actually the description in the W3C spec of what surroundContents should do):
var wrappingNode = document.createElement("div");
wrappingNode.appendChild(range.extractContents());
range.insertNode(wrappingNode);
I'm currently working on an inline editor and I've written a function that can properly wrap a cross-element range with any type of element like the execCommand does.
function surroundSelection(elementType) {
function getAllDescendants (node, callback) {
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
getAllDescendants(child, callback);
callback(child);
}
}
function glueSplitElements (firstEl, secondEl){
var done = false,
result = [];
if(firstEl === undefined || secondEl === undefined){
return false;
}
if(firstEl.nodeName === secondEl.nodeName){
result.push([firstEl, secondEl]);
while(!done){
firstEl = firstEl.childNodes[firstEl.childNodes.length - 1];
secondEl = secondEl.childNodes[0];
if(firstEl === undefined || secondEl === undefined){
break;
}
if(firstEl.nodeName !== secondEl.nodeName){
done = true;
} else {
result.push([firstEl, secondEl]);
}
}
}
for(var i = result.length - 1; i >= 0; i--){
var elements = result[i];
while(elements[1].childNodes.length > 0){
elements[0].appendChild(elements[1].childNodes[0]);
}
elements[1].parentNode.removeChild(elements[1]);
}
}
// abort in case the given elemenType doesn't exist.
try {
document.createElement(elementType);
} catch (e){
return false;
}
var selection = getSelection();
if(selection.rangeCount > 0){
var range = selection.getRangeAt(0),
rangeContents = range.extractContents(),
nodesInRange = rangeContents.childNodes,
nodesToWrap = [];
for(var i = 0; i < nodesInRange.length; i++){
if(nodesInRange[i].nodeName.toLowerCase() === "#text"){
nodesToWrap.push(nodesInRange[i]);
} else {
getAllDescendants(nodesInRange[i], function(child){
if(child.nodeName.toLowerCase() === "#text"){
nodesToWrap.push(child);
}
});
}
};
for(var i = 0; i < nodesToWrap.length; i++){
var child = nodesToWrap[i],
wrap = document.createElement(elementType);
if(child.nodeValue.replace(/(\s|\n|\t)/g, "").length !== 0){
child.parentNode.insertBefore(wrap, child);
wrap.appendChild(child);
} else {
wrap = null;
}
}
var firstChild = rangeContents.childNodes[0];
var lastChild = rangeContents.childNodes[rangeContents.childNodes.length - 1];
range.insertNode(rangeContents);
glueSplitElements(firstChild.previousSibling, firstChild);
glueSplitElements(lastChild, lastChild.nextSibling);
rangeContents = null;
}
};
Here's a JSFiddle with some complex HTML as demo: http://jsfiddle.net/mjf9K/1/. Please note that I took this straight out of my application. I use a few helpers to correctly restore the range to the original selection etc. These are not included.
That is when you add contentEditable=true attribute to the parent of those paragraphs, select any text, even across paragraphs, then make the call
document.execCommand('italic', false, null);
and finally if desired set contentEditable attribute back to false.
Btw, this works on IE too, except that to enter editable mode I think it is called designMode or something, google for it.
精彩评论