Get an element's nth-child number in pure JavaScript - javascript

I am making a function for my site where I set a data attribute which contains the nth-child number of that element.
My HTML markup:
<html>
<body>
<section class="hardware">some text, nth-child is one</section>
<section class="hardware">some text, nth-child is two</section>
<section class="hardware">some text, nth-child is three</section>
<section class="hardware">some text, nth-child is four</section>
<section class="hardware">some text, nth-child is five</section>
</body>
</html>
My JavaScript so far:
var selector = document.getElementsByClassName('hardware');
for(var i = 0; i <= selector.length; i++) {
var index = selector[i] //get the nth-child number here
selector[i].dataset.number = index;
}
How can I get the nth-child number of an element with pure JavaScript (not jQuery), is this even possible in JavaScript?

Check out this previous answer HERE.
It uses
var i = 0;
while( (child = child.previousSibling) != null )
i++;
//at the end i will contain the index.

When you say "number", do you mean 1, 2, etc or "one", "two", etc?
If 1, 2, etc, then the number is simply i+1...
If "one", "two", etc, then you need to get the text inside the element, then probably use a Regexp to parse it and get the value you want.

Simply incrementing the index linearly will only work if all the elements matching that class name are the only element children of the same parent, with no other elements that could interfere with :nth-child(), as shown exactly in the given markup. See this answer for an explanation on how other elements might interfere. Also review the Selectors spec on :nth-child().
One way to achieve this that is more foolproof is to loop through the child nodes of each element's parent node, incrementing a counter for each child node that is an element node (since :nth-child() only counts element nodes):
var selector = document.getElementsByClassName('hardware');
for (var i = 0; i < selector.length; i++) {
var element = selector[i];
var child = element.parentNode.firstChild;
var index = 0;
while (true) {
if (child.nodeType === Node.ELEMENT_NODE) {
index++;
}
if (child === element || !child.nextSibling) {
break;
}
child = child.nextSibling;
}
element.dataset.number = index;
}
JSFiddle demo
Note that this will apply the correct index regardless of where the given element is in the DOM:
If a particular section.hardware element is the first and only child of a different section, it will be assigned the correct index of 1.
If a .hardware element is the second child of its parent, even if it is the only one with that class (i.e. it follows some other element without the class), it will be assigned the correct index of 2.

I'm going to answer the questions with the following assumptions:
Your hardware classed elements are all siblings
You are interested in nth child not nth child + 1
They can be mixed with other elements:
<body>
<section class="hardware">some text, nth-child is zero</section>
<section class="software"></section>
<section class="hardware">some text, nth-child is two</section>
</body>
(I'm making these assumptions, because this is the problem I'm facing, thought it could be useful)
So the main difference is that instead of querying the elements that belong to a given class, I'm going to get the (direct) children of the body, and filter them.
Array.from(document.body.children)
.map((element, index) => ({element, index}))
.filter(({element}) => element.classList.contains('hardware'))
The resulting array will look like this:
[
{element: section.hardware, index: 0}
{element: section.hardware, index: 2}
]

You can split the text at the spaces at get the last word from each split-array:
var hards = document.getElementsByClassName('hardware');
for (var i=0; i < hards.length; i++) {
var hardText = hards[i].innerText || hard[i].textContent;
var hardList = hardText.split(' ');
var hardLast = hardList[hardList.length - 1];
alert(hardLast);
}
I am using || here because Firefox does not support innerText, while IE does not support textContent.
If the elements only contain text then innerHTML can be used instead of innerText/textContent.

[].slice.call(elem.parentElement.childNodes).indexOf(elem)

Related

Vanilla JS: Find all the DOM elements that just contain text

I want to get all the DOM elements in an HTML that doesn't contain any node, but text only.
I've got this code right now:
var elements = document.querySelectorAll("body *");
for(var i = 0; i < elements.length; i++) {
if(!elements[i].hasChildNodes()) {
console.log(elements[i])
}
}
This prints of course elements that have absolutely no content (and curiously enough, iframes).
Texts are accounted as a child node, so the .childNodes.length equals 1, but I don't know how to distinguish the nodes from the text. typeof the first node is always object, sadly.
How to distinguish the texts from the nodes?
Basically you are looking for leaf nodes of DOM with something inside the textContent property of the leaf node.
Let's traverse DOM and work out our little logic on leaf nodes.
const nodeQueue = [ document.querySelector('html') ];
const textOnlyNodes = [];
const textRegEx = /\w+/gi;
function traverseDOM () {
let currentNode = nodeQueue.shift();
// Our Leaf node
if (!currentNode.childElementCount && textRegEx.test(currentNode.textContent)) {
textOnlyNodes.push(currentNode);
return;
}
// Nodes with child nodes
nodeQueue.push(...currentNode.children);
traverseDOM();
}
childElementCount property make sure that the node is the leaf node and the RegEx test on textContent property is just my understanding of what a text implies in general. You can anytime tune the expression to make it a btter fit for your use case.
You can check for elements that have no .firstElementChild, which means it will only have text (or other invisible stuff).
var elements = document.querySelectorAll("body *");
for (var i = 0; i < elements.length; i++) {
if (!elements[i].firstElementChild) {
console.log(elements[i].nodeName)
}
}
<p>
text and elements <span>text only</span>
</p>
<div>text only</div>
The script that the stack snippet is included because it also only has text. You can filter out scripts if needed. This will also include elements that can not have content, like <input>.

Get all the descendant nodes (also the leaves) of a certain node

I have an html document consists of a <div id="main">. Inside this div may be several levels of nodes, without a precise structure because is the user who creates the document content.
I want to use a JavaScript function that returns all nodes within div id="main". Any tag is, taking into account that there may be different levels of children.
For example, if I has this document:
...
<div id="main">
<h1>bla bla</h1>
<p>
<b>fruits</b> apple<i>text</i>.
<img src="..">image</img>
</p>
<div>
<p></p>
<p></p>
</div>
<p>..</p>
</div>
...
The function getNodes would return an array of object nodes (I don't know how to represent it, so I list them):
[h1, #text (= bla bla), p, b, #text (= fruits), #text (= _apple), i, #text (= text), img, #text (= image), div, p, p, p, #text (= ..)]
As we see from the example, you must return all nodes, even the leaf nodes (ie #text node).
For now I have this function that returns all nodes except leaf:
function getNodes() {
var all = document.querySelectorAll("#main *");
for (var elem = 0; elem < all.length; elem++) {
//do something..
}
}
In fact, this feature applied in the above example returns:
[H1, P, B, I, IMG, DIV, P, P, P]
There aren't #text nodes.
Also, if text elements returned by that method in this way:
all[elem].children.length
I obtain that (I tested on <p>fruits</p>) <p> is a leaf node.
But if I build the DOM tree it is clear that is not a leaf node, and that in this example the leaf nodes are the #text...
Thank you
Classic case for recursion into the DOM.
function getDescendants(node, accum) {
var i;
accum = accum || [];
for (i = 0; i < node.childNodes.length; i++) {
accum.push(node.childNodes[i])
getDescendants(node.childNodes[i], accum);
}
return accum;
}
and
getDescendants( document.querySelector("#main") );
Aside from the already existing and perfectly functional answer, I find it worth mentioning that one can do away with the recursion and the many resulting function calls by simply navigating via the firstChild, nextSibling, and parentNode properties:
function getDescendants(node) {
var list = [], desc = node, checked = false, i = 0;
do {
checked || (list[i++] = desc);
desc =
(!checked && desc.firstChild) ||
(checked = false, desc.nextSibling) ||
(checked = true, desc.parentNode);
} while (desc !== node);
return list;
}
(Whenever we encounter a new node, we add it to the list, then try going to its first child node. If such does not exist, get the next sibling instead. Whenever no child node or following sibling is found, we go back up to the parent, while setting the checked flag to avoid adding that to the list again or reentering its descendant tree.)
This will, in virtually every case, improve performance greatly. Not that there is nothing left to optimize here, e.g. one could cache the nodes where we descend further into the hierarchy so as to later get rid of the parentNode when coming back up. I leave implementing this as an exercise for the reader.
Keep in mind though that iterating through the DOM like this will rarely be the bottleneck in a script. Unless you are going through a large DOM tree many tens/hundreds of times a second, that is — in which case you probably ought to think about avoiding that if at all possible, rather than simply optimizing it.
the children property only returns element nodes. If you want all children, I would suggest using the childNodes property. Then you can loop through this nodeList, and eliminate nodes that have nodeType of Node.ELEMENT_NODE or pick which other node types you would be interested in
so try something like:
var i, j, nodes
var result=[]
var all = document.querySelectorAll("#main *");
for (var elem = 0; elem < all.length; elem++) {
result.push(all[elem].nodeName)
nodes = all[elem].childNodes;
for (i=0, j=nodes.length; i<j; i++) {
if (nodes[i].nodeType == Node.TEXT_NODE) {
result.push(nodes[i].nodeValue)
}
}
}
If you only need the html tags and not the #text, you can just simply use this:<elem>.querySelectorAll("*");

Combining getElementsByTagName and getElementsByClassName

I'm trying to combine getElementsByTagName and getElementsByClassName to narrow a search down and count the resulting nodes, but the second search always results in a length of 0, and I've no idea why.
<!DOCTYPE html>
<html>
<head></head>
<body>
<p>Stuff</p>
<p class="content">Stuff2</p>
<p>Stuff</p>
<p class="content">Stuff2</p>
<script type="text/javascript">
pElements = document.getElementsByTagName("p");
console.log(pElements);
for(i = 0; i < pElements.length; i++) {
console.log(pElements[i].getElementsByClassName("content").length);
}
//console.log(document.querySelectorAll('p.content').length);
</script>
</body>
</html>
I know I can use querySelectorAll for this like the line I have commented out, but I'd like to understand why the first solution isn't working, and what I can do to fix it.
Thanks!
The problem with the first example is that:
pElements[i].getElementsByClassName("content")
searches for children of the p element. But since it is actually on the same element, they are not found.
W3C reference:
"The getElementsByClassName() method returns a collection of an element's child elements with the specified class name"
EDIT: To find if a p element has the content class, instead of getElementsByClassName(), you could use
pElements[i].classList.contains("content")
which will return true if the element has the class. Reference
EDIT2: A more backwards-compatible way would be to get the className property, split it on spaces and iterate the array to see if the class is there.
var names = pElements[i].className.split(" ");
var found = false;
for(var i = 0; i < names.length; i++){
if(names[i] === "content"){
found = true;
break;
}
}
if(found){
//Your code here
}
You can NOT combine getElementsByTagName and getElementsByClassName. Because as mentioned by #juunas, pElements now consists of the result consisting of an array of all the <p> elements.
And when you apply the getElementsByClassName to this result-set, using pElements[i].getElementsByClassName("content"), it searches in the child elements of pElements.
Suggestive Result :
Use the getAttribute() function to check the class of each element in pElements, like,
pElements = document.getElementsByTagName("p");
console.log(pElements);
for(var i = 0, j = 0; i < pElements.length; i++) {
if (pElements[i].getAttribute("class") === "content") {
j++;
}
}
console.log("Length of the resulting nodes: ", j);
It's because your p-elements dont have any elements with the class "content". The p-elements itself have this class, so you cant find it and 0 is correct.
Change
<p class="content">Stuff2</p>
To
<p><span class="content">Stuff2</span></p>
And you will get 1 as the result.

Loop through textNodes within selection with unknown number of descendants

I'm required to basically Find and replace a list of words retrieved as an array of objects (which have comma separated terms) from a webservice. The find and replace only occurs on particular elements in the DOM, but they can have an unknown and varying number of children (of which can be nested an unknown amount of times).
The main part I'm struggling with is figuring out how to select all nodes down to textNode level, with an unknown amount of nested elements.
Here is a very stripped-down example:
Retrieved from the webservice:
[{
terms: 'first term, second term',
youtubeid: '123qwerty789'
},{
terms: 'match, all, of these',
youtubeid: '123qwerty789'
},{
terms: 'only one term',
youtubeid: '123qwerty789'
},
etc]
HTML could be something like:
<div id="my-wrapper">
<ol>
<li>This is some text here without a term</li>
<li>This is some text here with only one term</li>
<li>This is some text here that has <strong>the first term</strong> nested!</li>
</ol>
</div>
Javascript:
$('#my-wrapper').contents().each(function(){
// Unfortunately only provides the <ol> -
// How would I modify this to give me all nested elements in a loopable format?
});
The following function is very similar to cbayram's but should be a bit more efficient and it skips script elements. You may want to skip other elements too.
It's based on a getText function I have used for some time, your requirements are similar. The only difference is what to do with the value of the text nodes.
function processTextNodes(element) {
element = element || document.body;
var self = arguments.callee; // or processTextNodes
var el, els = element.childNodes;
for (var i=0, iLen=els.length; i<iLen; i++) {
el = els[i];
// Exclude script element content
// May need to add other node types here
if (el.nodeType == 1 && el.tagName && el.tagName.toLowerCase() != 'script') {
// Have an element node, so process it
self(el);
// Othewise see if it's a text node
// If working with XML, add nodeType 4 if you want to process
// text in CDATA nodes
} else if (el.nodeType == 3) {
/* do something with el.data */
}
}
/* return a value? */
}
The function should be completely browser agnostic and should work with any conforming DOM (e.g. XML and HTML). Incidentally, it's also very similar to jQuery's text function.
One issue you may want to consider is words split over two or more nodes. It should be rare, but difficult to find when it happens.
I think you want
$('#my-wrapper *').each
This should select all the descendants of #my-wrapper no matter what they are.
See this fiddle for an example
I'm not sure if you are looking strictly for a jQuery answer, but here is one solution in JavaScript:
var recurse = function(el) {
// if text node or comment node
if(el.nodeType == 3 || el.nodeType == 8) {
// do your work here
console.log("Text: " + el.nodeValue);
}else {
for(var i = 0, children = el.childNodes, len = children.length; i < len; i++) {
recurse(children[i]);
}
}
}
recurse(document.getElementById("my-wrapper"));
Try the below:
$('#my-wrapper li')

Javascript getElementById based on a partial string

I need to get the ID of an element but the value is dynamic with only the beginning of it is the same always.
Heres a snippet of the code.
<form class="form-poll" id="poll-1225962377536" action="/cs/Satellite">
The ID always starts with poll- then the numbers are dynamic.
How can I get the ID using just JavaScript and not jQuery?
You can use the querySelector for that:
document.querySelector('[id^="poll-"]').id;
The selector means: get an element where the attribute [id] begins with the string "poll-".
^ matches the start
* matches any position
$ matches the end
jsfiddle
Try this.
function getElementsByIdStartsWith(container, selectorTag, prefix) {
var items = [];
var myPosts = document.getElementById(container).getElementsByTagName(selectorTag);
for (var i = 0; i < myPosts.length; i++) {
//omitting undefined null check for brevity
if (myPosts[i].id.lastIndexOf(prefix, 0) === 0) {
items.push(myPosts[i]);
}
}
return items;
}
Sample HTML Markup.
<div id="posts">
<div id="post-1">post 1</div>
<div id="post-12">post 12</div>
<div id="post-123">post 123</div>
<div id="pst-123">post 123</div>
</div>
Call it like
var postedOnes = getElementsByIdStartsWith("posts", "div", "post-");
Demo here: http://jsfiddle.net/naveen/P4cFu/
querySelectorAll with modern enumeration
polls = document.querySelectorAll('[id ^= "poll-"]');
Array.prototype.forEach.call(polls, callback);
function callback(element, iterator) {
console.log(iterator, element.id);
}
The first line selects all elements in which id starts ^= with the string poll-.
The second line evokes the enumeration and a callback function.
Given that what you want is to determine the full id of the element based upon just the prefix, you're going to have to do a search of the entire DOM (or at least, a search of an entire subtree if you know of some element that is always guaranteed to contain your target element). You can do this with something like:
function findChildWithIdLike(node, prefix) {
if (node && node.id && node.id.indexOf(prefix) == 0) {
//match found
return node;
}
//no match, check child nodes
for (var index = 0; index < node.childNodes.length; index++) {
var child = node.childNodes[index];
var childResult = findChildWithIdLike(child, prefix);
if (childResult) {
return childResult;
}
}
};
Here is an example: http://jsfiddle.net/xwqKh/
Be aware that dynamic element ids like the ones you are working with are typically used to guarantee uniqueness of element ids on a single page. Meaning that it is likely that there are multiple elements that share the same prefix. Probably you want to find them all.
If you want to find all of the elements that have a given prefix, instead of just the first one, you can use something like what is demonstrated here: http://jsfiddle.net/xwqKh/1/
I'm not entirely sure I know what you're asking about, but you can use string functions to create the actual ID that you're looking for.
var base = "common";
var num = 3;
var o = document.getElementById(base + num); // will find id="common3"
If you don't know the actual ID, then you can't look up the object with getElementById, you'd have to find it some other way (by class name, by tag type, by attribute, by parent, by child, etc...).
Now that you've finally given us some of the HTML, you could use this plain JS to find all form elements that have an ID that starts with "poll-":
// get a list of all form objects that have the right type of ID
function findPollForms() {
var list = getElementsByTagName("form");
var results = [];
for (var i = 0; i < list.length; i++) {
var id = list[i].id;
if (id && id.search(/^poll-/) != -1) {
results.push(list[i]);
}
}
return(results);
}
// return the ID of the first form object that has the right type of ID
function findFirstPollFormID() {
var list = getElementsByTagName("form");
var results = [];
for (var i = 0; i < list.length; i++) {
var id = list[i].id;
if (id && id.search(/^poll-/) != -1) {
return(id);
}
}
return(null);
}
You'll probably have to either give it a constant class and call getElementsByClassName, or maybe just use getElementsByTagName, and loop through your results, checking the name.
I'd suggest looking at your underlying problem and figure out a way where you can know the ID in advance.
Maybe if you posted a little more about why you're getting this, we could find a better alternative.
You use the id property to the get the id, then the substr method to remove the first part of it, then optionally parseInt to turn it into a number:
var id = theElement.id.substr(5);
or:
var id = parseInt(theElement.id.substr(5));
<form class="form-poll" id="poll-1225962377536" action="/cs/Satellite" target="_blank">
The ID always starts with 'post-' then the numbers are dynamic.
Please check your id names, "poll" and "post" are very different.
As already answered, you can use querySelector:
var selectors = '[id^="poll-"]';
element = document.querySelector(selectors).id;
but querySelector will not find "poll" if you keep querying for "post": '[id^="post-"]'
If you need last id, you can do that:
var id_list = document.querySelectorAll('[id^="image-"]')
var last_id = id_list.length
alert(last_id)

Categories

Resources