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);
Related
I am trying to use Javascript to modify an existing HTML document so that I can surround every word of text in the web page with a span tag that would have a counter. This is a very specific problem so I am going to provide an example case:
<body><p>hello, <br>
change this</p>
<img src="lorempixel.com/200/200> <br></body></html>
This should change to:
<body><p><span id="1">hello,</span>
<br> <span id="2"> change</span><span id="3"> this</span> </p>
<br> <img src="lorempixel.com/200/200> <br></body></html>
I am thinking or regex solutions but they get truly complicated and I am not sure of how to ignore tags and change text without completely breaking the page.
Any thoughts appreciated!
Don't use regex on raw HTML. Use it only on text. This is because regex is a context free parser but HTML is a recursive language. You need a recursive descent parser to properly handle HTML.
First a few useful features of the DOM:
document.body is the root of the DOM
Every node of the DOM has a childNodes array (even comments, text, and attributes)
Element nodes such as <span> or <h> don't contain text, instead they contain text nodes that contain text.
All nodes have a nodeType property and text node is type 3.
All nodes have a nodeValue property that holds different things depending on what kind of node it is. For text nodes nodeValue contains the actual text.
So, using the information above we can surround all words with a span.
First a simple utility function that allows us to process the DOM:
// First a simple implementation of recursive descent,
// visit all nodes in the DOM and process it with a callback:
function walkDOM (node,callback) {
if (node.nodeName != 'SCRIPT') { // ignore javascript
callback(node);
for (var i=0; i<node.childNodes.length; i++) {
walkDOM(node.childNodes[i],callback);
}
}
}
Now we can walk the DOM and find text nodes:
var textNodes = [];
walkDOM(document.body,function(n){
if (n.nodeType == 3) {
textNodes.push(n);
}
});
Note that I'm doing this in two steps to avoid wrapping words twice.
Now we can process the text nodes:
// simple utility functions to avoid a lot of typing:
function insertBefore (new_element, element) {
element.parentNode.insertBefore(new_element,element);
}
function removeElement (element) {
element.parentNode.removeChild(element);
}
function makeSpan (txt, attrs) {
var s = document.createElement('span');
for (var i in attrs) {
if (attrs.hasOwnProperty(i)) s[i] = attrs[i];
}
s.appendChild(makeText(txt));
return s;
}
function makeText (txt) {return document.createTextNode(txt)}
var id_count = 1;
for (var i=0; i<textNodes.length; i++) {
var n = textNodes[i];
var txt = n.nodeValue;
var words = txt.split(' ');
// Insert span surrounded words:
insertBefore(makeSpan(words[0],{id:id_count++}),n);
for (var j=1; j<words.length; j++) {
insertBefore(makeText(' '),n); // join the words with spaces
insertBefore(makeSpan(words[j],{id:id_count++}),n);
}
// Now remove the original text node:
removeElement(n);
}
There you have it. It's cumbersome but is 100% safe - it will never corrupt other tags of javascript in your page. A lot of the utility functions I have above can be replaced with the library of your choice. But don't take the shortcut of treating the entire document as a giant innerHTML string. Not unless you're willing to write an HTML parser in pure javascript.
This sort of processing is always a lot more complex than you think. The following will wrap sequences of characters that match \S+ (sequence of non–whitespace) and not wrap sequences that match \s+ (whitespace).
It also allows the content of certain elements to be skipped, such as script, input, button, select and so on. Note that the live collection returned by childNodes must be converted to a static array, otherwise it is affected by the new nodes being added. An alternative is to use element.querySelectorAll() but childNodes has wider support.
// Copy numeric properties of Obj from 0 to length
// to an array
function toArray(obj) {
var arr = [];
for (var i=0, iLen=obj.length; i<iLen; i++) {
arr.push(obj[i]);
}
return arr;
}
// Wrap the words of an element and child elements in a span
// Recurs over child elements, add an ID and class to the wrapping span
// Does not affect elements with no content, or those to be excluded
var wrapContent = (function() {
var count = 0;
return function(el) {
// If element provided, start there, otherwise use the body
el = el && el.parentNode? el : document.body;
// Get all child nodes as a static array
var node, nodes = toArray(el.childNodes);
var frag, parent, text;
var re = /\S+/;
var sp, span = document.createElement('span');
// Tag names of elements to skip, there are more to add
var skip = {'script':'', 'button':'', 'input':'', 'select':'',
'textarea':'', 'option':''};
// For each child node...
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
// If it's an element, call wrapContent
if (node.nodeType == 1 && !(node.tagName.toLowerCase() in skip)) {
wrapContent(node);
// If it's a text node, wrap words
} else if (node.nodeType == 3) {
// Match sequences of whitespace and non-whitespace
text = node.data.match(/\s+|\S+/g);
if (text) {
// Create a fragment, handy suckers these
frag = document.createDocumentFragment();
for (var j=0, jLen=text.length; j<jLen; j++) {
// If not whitespace, wrap it and append to the fragment
if (re.test(text[j])) {
sp = span.cloneNode(false);
sp.id = count++;
sp.className = 'foo';
sp.appendChild(document.createTextNode(text[j]));
frag.appendChild(sp);
// Otherwise, just append it to the fragment
} else {
frag.appendChild(document.createTextNode(text[j]));
}
}
}
// Replace the original node with the fragment
node.parentNode.replaceChild(frag, node);
}
}
}
}());
window.onload = wrapContent;
The above addresses only the most common cases, it will need more work and thorough testing.
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);
}
If I had something like the following:
$400/week
$800/week
With jQuery being available, what would be an ideal method of wrapping the "/week" part in an html element like <span class='small'>/week</span>?
You could do something like this:
var data = $("#text").html();
$("#text").html(data.replace(/\/\s*week/g, '<span class="small">/week</span>'));
Working example: http://jsfiddle.net/jfriend00/TKa8D/
Note: Replacing HTML will undo any event handlers assigned within that HTML so you should generally replace the HTML at the lowest level possible so you aren't replacing elements that already have event handlers on them. If you share your specific HTML, we could make more concrete recommendations for the easiest way.
It's not pretty, but the only safe way to do it (preserving event handlers and such) is to walk the tree, splitting text nodes where necessary. Unfortunately, jQuery does not like working with text nodes; it is very element-centric.
You can start by defining a helper function to do the recursion:
function recurse(node, fn) {
if(node.nodeType === Node.TEXT_NODE) {
fn(node);
}else if(node.nodeType === Node.ELEMENT_NODE) {
var children = Array.prototype.slice.call(node.childNodes);
for(var i = 0, l = children.length; i < l; i++) {
recurse(children[i], fn);
}
}
}
It walks every node in the tree, calling a given function for every text node. Then we can use that function to split the text nodes and insert a span in the middle:
recurse(document.documentElement, function(node) {
var re = /(\$\d+)(\/week)/;
var match;
while(match = re.exec(node.nodeValue)) {
var before = document.createTextNode(node.nodeValue.substring(0, match.index + match[1].length));
node.parentNode.insertBefore(before, node);
var span = document.createElement('span');
span.className = 'week';
span.textContent = node.nodeValue.substring(match.index + match[1].length, match.index + match[0].length);
node.parentNode.insertBefore(span, node);
node.nodeValue = node.nodeValue.substring(match.index + match[0].length);
}
});
Try it.
Hi I am using jquery to get the html content of a div, remove the strong tags and then update the div with the new contents though. It isn't working for some reason though. Here is my code:
var content = $('#mydivid').html();
content = content.replace('<strong>', '').replace('</strong>', '');
$('#mydivid').html(content);
Anyone know why this isnt working?
First put in an alert to check content...
alert(content);
if that works then I would def not use replaceWith, try...
$('#mydivid').html(content);
first:
you don't need to replace twice, the first argument of replace() is regex, so you can :
content.replace(/<\/?strong>/g, "");
to remove all the <strong> and </strong> label.
second:
replaceWith() is not what you want, html() is your target.
and this is all you want:
$(function() {
$("#mydivid").html(function() {
return $(this).html().replace(/<\/?strong>/g, "");
});
});
jsfiddle: http://jsfiddle.net/pS5Xp/
the only issue I can see with the code is that you are replacing the div iteself. You probably want to do this instead:-
var content = $('#mydivid').html();
content = content.replace('<strong>', '').replace('</strong>', '');
$('#mydivid').html(content);
Also make sure you have lower case strong tags.
Sample: http://jsfiddle.net/8pVdw/
var content = $('#mydivid').html();
$('#mydivid').html(content.replace('<strong>', '').replace('</strong>', ''));
Should work.
If it doesn't, just alert the contents and see if it works.
You could try:
var div = $('#mydivid');
var strong = div.find("strong");
strong.replaceWith(strong.text());
What this does is just finds the <strong> tag and replaces it with the text contents.
A fiddle here: http://jsfiddle.net/cWpUK/1/
/**
* Syntax:
* Node replaceNode(Node newNode, Node oldNode)
* Node replaceNode(String newNode, Node oldNode)
* Node replaceNode(Object newNode, Node oldNode)
* - newNode: { String localName [, String namespaceURI ] }
* null replaceNode(null newNode, Node oldNode)
* - will delete the old node and move all childNodes to the parentNode
**/
// nodes from another document has to be imported first
// should throw an Error if newNode is descendant-or-self
// type of newNode is tested by 'firstChild' (DOM 1)
// returns new Node
var replaceNode = (function() {
var replaceNode = function(newNode, oldNode) {
var document = oldNode.ownerDocument;
if(newNode === null)
newNode = document.createDocumentFragment();
else if(typeof newNode === 'string' || newNode instanceof String)
newNode = {localName: newNode};
if(!('firstChild' in newNode)) {
var namespaceURI = 'namespaceURI' in newNode ?
newNode.namespaceURI : replaceNode.namespaceURI;
newNode = namespaceURI === null ?
document.createElement(newNode.localName) :
document.createElementNS(namespaceURI, newNode.localName);
}
var parentNode = oldNode.parentNode,
nextSibling = oldNode.nextSibling;
parentNode.removeChild(oldNode);
if(newNode.nodeType === 1 || newNode.nodeType === 11)
while(oldNode.firstChild)
newNode.appendChild(oldNode.firstChild);
parentNode.insertBefore(newNode, nextSibling);
return newNode.nodeType === 11 ? null : newNode;
};
replaceNode.namespaceURI = null;
return replaceNode;
})();
This will replace a Node with another one. This has the advantage that no EventListeners on subsequent Nodes are destroyed.
var nodeList = document.getElementById('mydivid').getElementsByTagName('strong');
while(nodeList.length)
replaceNode(null, nodeList[ nodeList.length - 1 ]);
Testcase
You probably don't want to use replaceWith. You should update the div with the new content by calling .html() again:
var content = $('#mydivid').html();
content = content.replace('<strong>', '').replace('</strong>', '');
$('#mydivid').html(content);
Works fine when I test it - http://jsbin.com/ihokav/edit#preview
A nicer alternative is to use replaceWith on the strong tags themselves:
$('#mydivid strong').replaceWith(function() { return $(this).html(); });
http://jsbin.com/ihokav/2/edit
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()
);