Split an element containing TextNodes and elements using jQuery - javascript

I need to split an HTML element based on a users selection using jQuery. In the following example square brackets indicate the selection:
Lor[em <a>ips]um <span>dolor</span></a>
should become
Lor [ em <a>ips</a> ] <a>um <span>dolor</span></a>
To do this I create a range, find the TextNodes containing the selection boundaries and split them using splitText(index). Next I check whether the parent element must also be split. If yes, I clone and empty them, move the second parts of the original elements into the clones and insert them after the original like so:
var tail = textNode.splitText( offset );
var $parent = $(textNode).parent();
if ($parent.is("span")) {
var $tail = $parent.clone();
$tail.contents().remove();
$tail = $tail.append(tail).insertAfter($parent);
if ($parent.parent().is("a")) {
$tail = $parent.parent().clone();
$tail.contents().remove();
$tail = $tail.append($tail).insertAfter($parent.parent());
}
return $tail[0];
}
else if ($parent.is("a")) {
var $tail = $parent.clone();
$tail.contents().remove();
$tail = $tail.append(tail).insertAfter($parent);
return $tail[0];
}
return tail;
Problem is, though, tail only contains the second part of the TextNode. The following <span /> is not moved, so the HTML is messed up like so (selection is lost underway, but not important):
Lor em <a>ips <span>dolor</span></a> <a>um</a>
I also tried $(tail).nextAll() but it seems to return an empty set. Does anybody have an idea how I can achieve this? If anything is not clear, please ask for more detail.
EDIT: Like suggested I created the following http://jsfiddle.net/7PdLd/4/.

This seems to work:
Demo
function start () {
var range = window.getSelection().getRangeAt(0);
split(
range.startContainer,
range.startOffset,
range.commonAncestorContainer,
false
);
split(
range.endContainer,
range.endOffset,
range.commonAncestorContainer,
true
);
}
function split(node, offset, ancestor, backwards) {
var clone;
if(backwards) {
clone = node;
node = node.splitText(offset);
}else{
clone = node.splitText(offset);
}
if(node == ancestor) return;
var parent;
while((parent = node.parentNode) && parent != ancestor) {
var parentClone = parent.cloneNode(false);
appendUntil(parentClone, parent, node, !backwards);
parentClone.insertBefore(clone, backwards ? null : parentClone.firstChild);
node = parent;
clone = parentClone;
}
insertAdjacent(ancestor, clone, node, backwards);
}
function appendUntil(target, parent, until, fromEnd) {
var from, to, sibling;
if(fromEnd) {
from = until.nextSibling;
to = null;
} else {
from = parent.firstChild;
to = until;
}
while(from && from != to) {
sibling = from.nextSibling;
target.appendChild(from);
from = sibling;
}
}
function insertAdjacent(parent, newEl, refEl, before) {
parent.insertBefore(newEl, before ? refEl : refEl.nextSibling);
}

Related

Splitting node content in JavaScript DOM -

This existing answer is an excellent piece of code that very nearly does what I want. Like the OP in that questions I want HTML tags to be split, but based on a tag rather than an offset, and bounded by an item that should not be split.
That is, I want to turn this:
<p>
<strong>hi there, how <em>are <span>y<!--break-->ou</span> doing</em> today?</strong>
</p>
into this:
<p>
<strong>hi there, how <em>are <span>y</span></em></strong>
<!--break-->
<strong><em><span>ou</span> doing</em> today?</strong>
</p>
I'm still getting my head around javascript so while I had a play with the jsbin provided by #Hemlock I couldn't get it to do what I intended.
The given answer was:
function splitNode(node, offset, limit) {
var parent = limit.parentNode;
var parentOffset = getNodeIndex(parent, limit);
var doc = node.ownerDocument;
var leftRange = doc.createRange();
leftRange.setStart(parent, parentOffset);
leftRange.setEnd(node, offset);
var left = leftRange.extractContents();
parent.insertBefore(left, limit);
}
function getNodeIndex(parent, node) {
var index = parent.childNodes.length;
while (index--) {
if (node === parent.childNodes[index]) {
break;
}
}
return index;
}
No ranges required, you just need to duplicate all the cut elements and move their children around:
function splitOn(bound, cutElement) {
// will divide the DOM tree rooted at bound to the left and right of cutElement
// cutElement must be a descendant of bound
for (var parent = cutElement.parentNode; bound != parent; parent = grandparent) {
var right = parent.cloneNode(false);
while (cutElement.nextSibling)
right.appendChild(cutElement.nextSibling);
var grandparent = parent.parentNode;
grandparent.insertBefore(right, parent.nextSibling);
grandparent.insertBefore(cutElement, right);
}
}
(jsfiddle demo)
You could build your own split function by thinking how to split the content into an array and later concatinate the string together.
the problem with this answer is that it does not start/nor finish any split tag, like in your situation, is the SPAN element.
<script>
var content = document.getElementById('content');
var elements = document.getElementsByTagName('strong');
var array = element.split("<!--break-->");
var string = '';
for(var i = 0; i < array.length; i++) {
string += '<strong>' + sarray[i] + "</strong>';
}
content.innerHTML = string;
</script>
<div id="content">
<strong>hi there, how <em>are <span>y<!--break-->ou</span> doing</em> today?</strong>
</div>

get SINGLE text node from DOM object

Need to get all direct nodes from DOM element and don't actually know, how it many and what kind they are.
.contents()?
Ok, let's see..
$('<div />').html('<p>p</p>').contents() ->
[<p>ā€‹pā€‹</p>ā€‹]
Ok.
$('<div />').html('textNode').contents() -> []
WTF?
$('<div />').html('textNode').append('another').contents() ->
["textNode", "another"]
Ok, so what about single text node?
I don't know if this is helpful. A while ago I built a Document Fragment generator using JSON styled input. I also wrote a (somewhat working) reverse function for it so you could turn your nodeList into a JSON string.
https://gist.github.com/2313580
var reverseFunction = function(DOM /* DOM tree or nodeList */) {
var tree = [];[].forEach.call(DOM, function(obj) {
if (obj instanceof Text) {
tree.push({
'textContent': obj.textContent
});
} else {
var tmp = {};
tmp['tagName'] = obj.nodeName;
for( var data in obj.dataset ) {
tmp['data-' + data] = obj.dataset[data];
}
for (var i = 0, l = obj.attributes.length; i < l; i++) {
var key = obj.attributes[i].name,
val;
if (key.indexOf('data-') === -1) {
switch (key) {
case ('class'):
key = 'className';
break;
case ('style'):
val = {};
obj.attributes[i].value.split(';').forEach(function(rule) {
var parts = rule.split(':');
val[parts[0]] = parts[1];
});
break;
};
tmp[key] = val || obj.attributes[i].value;
}
}
if (obj.childNodes.length > 0) {
tmp['childNodes'] = reverseFunction(obj.childNodes);
}
tree.push(tmp);
}
});
return tree;
};
This does find textNodes and separates them... You may be able to extract something from it.
Update: to answer a comment in your question above...
var div = document.createElement('div');
div.appendChild(document.createTextNode('dsf'));
console.log( div.childNodes.length, div.childNodes, div.childNodes[0].textContent);ā€‹
I hope this makes a bit more sense to you know. The array appears empty in the console but it is not. check the length and attempt to access it and you will see.
.contents() is concerned with DOM nodes. That string in the 2nd example is not a DOM element.

Get Element ID by String it Contains using Plain Javascript

How can I get an elemnts ID based on the string it contains?
<span id="th67">This the string I need to match</span>
I can't make use of JQuery or any other Javascript library to do this.
I need to do this for a selenium test.
I didn't realise how useless I am in JS without my libraries!
Thanks all for any help.
Well, if you know what kind of tag you're looking for, you can just do:
var spans = document.getElementsByTagName('span'), targetId;
for (var i = 0; i < spans.length; ++i) {
if (spans[i].innerText === stringToMatch) {
// found it ...
targetId = spans[i].id;
break;
}
}
if (targetId) {
// ... do whatever ...
}
If you want to get fancy you could construct an xpath query, I guess.
If the browsers you're targeting support XPath you can do a simple XPath query:
// Find an element by the text it contains, optionally
// starting at specified parent element.
function getElementByText( text, ctx)
{
return document.evaluate("//*[.='"+text+"']",
ctx || document, null, XPathResult.ANY_TYPE, null).iterateNext();
}
Then just run
var myElement = getElementByText( "This is the string I need to match" );
if ( myElement )
{
// do something with myElement.id
}
Here's a simple recursive function that will do it:
function findByText(node, text) {
if(node.nodeValue == text) {
return node.parentNode;
}
for (var i = 0; i < node.childNodes.length; i++) {
var returnValue = findByText(node.childNodes[i], text);
if (returnValue != null) {
return returnValue;
}
}
return null;
}
Use it as:
var target = findByText(document, "This the string I need to match");
This will end up with either target being null, or it being a DOM node whose id you can get with target.id.
See it in action.

Remove All Nodes with (nodeName = "script") from a Document Fragment *before placing it in dom*

My goal is to remove all <[script]> nodes from a document fragment (leaving the rest of the fragment intact) before inserting the fragment into the dom.
My fragment is created by and looks something like this:
range = document.createRange();
range.selectNode(document.getElementsByTagName("body").item(0));
documentFragment = range.cloneContents();
sasDom.insertBefore(documentFragment, credit);
document.body.appendChild(documentFragment);
I got good range walker suggestions in a separate post, but realized I asked the wrong question. I got an answer about ranges, but what I meant to ask about was a document fragment (or perhaps there's a way to set a range of the fragment? hrmmm). The walker provided was:
function actOnElementsInRange(range, func) {
function isContainedInRange(el, range) {
var elRange = range.cloneRange();
elRange.selectNode(el);
return range.compareBoundaryPoints(Range.START_TO_START, elRange) <= 0
&& range.compareBoundaryPoints(Range.END_TO_END, elRange) >= 0;
}
var rangeStartElement = range.startContainer;
if (rangeStartElement.nodeType == 3) {
rangeStartElement = rangeStartElement.parentNode;
}
var rangeEndElement = range.endContainer;
if (rangeEndElement.nodeType == 3) {
rangeEndElement = rangeEndElement.parentNode;
}
var isInRange = function(el) {
return (el === rangeStartElement || el === rangeEndElement ||
isContainedInRange(el, range))
? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
var container = range.commonAncestorContainer;
if (container.nodeType != 1) {
container = container.parentNode;
}
var walker = document.createTreeWalker(document,
NodeFilter.SHOW_ELEMENT, isInRange, false);
while (walker.nextNode()) {
func(walker.currentNode);
}
}
actOnElementsInRange(range, function(el) {
el.removeAttribute("id");
});
That walker code is lifted from: Remove All id Attributes from nodes in a Range of Fragment
PLEASE No libraries (ie jQuery). I want to do this the raw way. Thanks in advance for your help
The easiest way to gather all <script> nodes would be to use getElementsByTagName, but unfortunately that is not implemented on DocumentFragment.
However, you could create a temporary container and append all elements within the fragment, and then go through and remove all <script> elements, like so:
var temp = document.createElement('div');
while (documentFragment.firstChild)
temp.appendChild(documentFragment.firstChild);
var scripts = temp.getElementsByTagName('script');
var length = scripts.length;
while (length--)
scripts[length].parentNode.removeChild(scripts[length]);
// Add elements back to fragment:
while (temp.firstChild)
documentFragment.appendChild(temp.firstChild);
Correct me if I'm wrong, but if the documentFragment is a real DOM Fragment, you should be able to do something like:
var scripts = documentFragment.getElementsByTagName('script');
if (scripts.length){
for (var i=0, l = scripts.length;i<l;i++){
documentFragment.removeChild(scripts[i]);
}
}
right?
Correction: you can't apply getElementsByTagName to a documentFragment, J-P is right. You can however us a child of the fragment if it is a (cloned) node supporting getElementsByTagName. Here's some (working) code I use within a larger script a few days ago:
var fragment = d.createDocumentFragment(), f;
fragment.appendChild(document.createElement('div'));
fragment.firstChild.appendChild(zoeklijst.cloneNode(true));
f = fragment.firstChild;
return f.getElementsByTagName(getList); //<==
2022, Chrome, querySelector family works on document fragments:
frag.content.querySelectorAll('script').forEach(
(s)=>s.remove()
);

how to select all childs other than a element

I have a element which contains 3 child. let says
<div class="parent">
<div class="firstChild">firstChild</div>
SecondChild
<ul><li>thrid child</li></ul>
</div>
In the example I need to select first 2 childs and not the UL. how to do through jquery.
You can use the :lt selector. http://api.jquery.com/lt-selector/ and the * selector.
$('div.parent > *:lt(2)')
This selector should do it.
$(".parent *").not("ul")
Try this:
$(".parent").children();
If you want the text node included, .clone() it and remove what you don't want like this:
var children = $(".parent").clone();
children.find("ul").remove(); //or: children.find(":gt(1)").remove();
//children now contains everything by the <ul>
I commented some in the original post about what nodes there really are in the poster's example markup.
Here is a little something to print out the "real" structure if anyone is interested. I just added an id to the parent element to get ahold of it a little easier when about to start walking the DOM:
<body>
<div id="parent" class="parent">
<div class="firstChild">firstChild</div>
SecondChild
<ul><li>thrid child</li></ul>
</div>
<script type="text/javascript">
(function (startNode) {
// Recursively walking the structure from the supplied node
function walk(node, indent) {
indent = (typeof indent==='undefined') ? '' : indent;
var children = node.childNodes;
var child, info;
// For each child of node...
for (var idx=0, len=children.length; idx<len; ++idx) {
child = children.item(idx);
// ..print it.
printNodeInfo(child, indent);
// If it was an element (tag) we try to display any children it might have
if (child.nodeType===1) {
arguments.callee(child, indentAdd+indent);
}
}
}
function printNodeInfo(node, indent) {
indent = (typeof indent==='undefined') ? '' : indent;
console.log(indent+getNodePrintString(node));
}
function getNodePrintString(node) {
var info = '';
// Check type and extract what info to display
if (node.nodeType===1) {info = node.nodeName;} // element nodes, show name
else if (node.nodeType===3) {info = trim(node.nodeValue);} // text nodes, show value
// If it was an empty textnode, return special string
if (!info) {return '[empty '+nodeTypes[node.nodeType]+' node]';}
else {return nodeTypes[node.nodeType]+': '+info+(node.id?'#'+node.id:'');}
}
// Just a utility function to trim values of textnodes
function trim(str) {
return str.replace(/^\s+/, '').replace(/\s+$/, '');
}
// Amount of indentation to add each level
var indentAdd = ' ';
// Mappings for nodeType => name of nodetype
var nodeTypes = {
1: '#Element'
, 2: '#Attribute' // not used right now
, 3: '#Text'
};
// Print info in start node
printNodeInfo(startNode);
// Start walking
walk(startNode, indentAdd);
})(document.getElementById('parent')); // Supply a start node
</script>
</body>
And here's the output:
#Element: DIV#parent
[empty #Text node]
#Element: DIV
#Text: firstChild
#Text: SecondChild
#Element: UL
#Element: LI
#Text: thrid child
[empty #Text node]
Here's how you can grab childnodes of an element, including "pure" text nodes (text not inside tags).
// Returns child nodes (including text nodes, if not empty) of 'node',
// but no more than 'limit' nodes.
// If limit given is not a number >= 0, it harvests as many as it can find.
function npupNodes(node, limit) {
// not a number or < 0 means 'no limit'
limit = (typeof limit!=='number' || limit<0) ? -1 : limit;
var result = [];
var children = node.childNodes;
var child, nodeType, captureCount=0;
// Loop over children...
for (var idx=0, len=children.length; idx<len && (limit<0 || captureCount<limit); ++idx) {
child = children.item(idx);
nodeType = child.nodeType;
// If it is an element or a textnode...
if (nodeType===1 || nodeType===3) {
if (nodeType===3 && !child.nodeValue.replace(/^\s+/, '').replace(/\s+$/, '')) {
// ..if it is a textnode that is logically empty, ignore it
continue;
}
// ..otherwise add it to the harvest, and increase counter
result.push(child);
captureCount += 1;
}
}
return result;
}
As you can see in the code, logically blank (all whitespace) textnodes are not returned.
Calling it like this with the markup in the poster's question, it does the job asked for (except for not using jQuery - sue me :)
var someChildren = npupNodes(document.getElementsByClassName('parent')[0], 2);

Categories