Как обернуть с помощью HTML-тегов диапазон выбора границ DOM?
В настоящий момент я фиксирую выбор текста пользователей через s = window.getSelection()
и range = s.getRangeAt(0)
(браузер в стороне). Всякий раз, когда делается выбор внутри a <p>
, я могу легко вызвать range.surroundContents(document.createElement("em"))
, чтобы выделенный текст был обернут тегом <em>
.
В этом примере, однако,
<p>This is the Foo paragraph.</p>
<p>This is the Bar paragraph.</p>
<p>This is the Baz paragraph.</p>
когда пользователь делает выбор текста от Foo
до Baz
, я не могу вызывать range.surroundContents
: сбой Firefox с The boundary-points of a range does not meet specific requirements." code: "1
, потому что выбор недействителен HTML.
В этом случае я хотел бы как-то получить следующее состояние в 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>
Любые идеи?
FYI: Я пытался с API Range
, но я не вижу прямого способа достижения этого результата. С
var r = document.createRange();
r.setStart(range.startContainer, range.startOffset);
r.setEnd(range.endContainer, range.endOffset+40);
selection.addRange(r);
В конечном итоге я могу что-то взломать, переставив смещения, но только для контейнеров 'start' и 'end'! (т.е. в этом случае абзац Bar
, как его обернуть?)
Ответы
Ответ 1
Вы пробовали следующий подход (который на самом деле является описанием в спецификации W3C того, что должны делать surroundContents):
var wrappingNode = document.createElement("div");
wrappingNode.appendChild(range.extractContents());
range.insertNode(wrappingNode);
Ответ 2
В настоящее время я работаю над встроенным редактором, и я написал функцию, которая может корректно обернуть диапазон кросс-элементов любым типом элемента, например execCommand.
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;
}
};
Здесь JSFiddle с некоторым сложным HTML как демо: http://jsfiddle.net/mjf9K/1/. Обратите внимание, что я взял это прямо из своей заявки. Я использую несколько помощников, чтобы правильно восстановить диапазон до исходного выбора и т.д. Они не включены.
Ответ 3
То есть, когда вы добавляете атрибут contentEditable = true родительскому элементу этих абзацев, выберите любой текст, даже через абзацы, затем выполните вызов
document.execCommand('italic', false, null);
и, наконец, при желании вернуть атрибут contentEditable обратно в значение false.
Btw, это работает и в IE, за исключением того, что для входа в редактируемый режим я думаю, что он называется designMode или что-то, google для него.