I'm trying to parse a page with javascript to replace links belonging to a specific class with an iframe to open a corresponding wikipedia page [so that rather than having a link you have an embedded result]. The function detects links properly but something about the replaceChild() action causes it to skip the next instance... as if it does the first replace and then thinks the next link is the one it just worked on, probably as a result of the loop.
For example, if there's 2 links on the page, the first will parse and the second will not even be seen but if there's 3, the first two will be parsed using the attributes from the first and third.
Can anyone suggest an alternative way of looping through the links that doesn't rely on a count function? Perhaps adding them to an array?
Sample Links
wiki it
Sample Javascript
(function(){
var lnks = document.getElementsByTagName("a");
for (var i = 0; i < lnks.length; i++) {
lnk = lnks[i]; if(lnk.className == "myspeciallinks"){
newif=document.createElement("iframe");
newif.setAttribute("src",'http://www.wikipedia.com');
newif.style.width="500px";
newif.style.height="100px";
newif.style.border="none";
newif.setAttribute("allowtransparency","true");
lnk.parentNode.replaceChild(newif,lnk);
}
}
})();
The problem here is that document.getElementsByTagName returns a NodeList and not an array. A NodeList is still connected to the actual DOM, you cannot safely iterate over its entries and at the same time remove the entries from the DOM (as you do when you replace the links).
You will need to convert the NodeList into an array and use the array for iteration:
(function(){
var lnksNodeList = document.getElementsByTagName("a");
// create an array from the above NodeList and use for iteration:
var lnks = Array.prototype.slice.call(lnksNodeList);
for (var i = 0; i < lnks.length; i++) {
var lnk = lnks[i];
if (lnk.className == "myspeciallinks") {
var newif = document.createElement("iframe");
newif.setAttribute("src", 'http://www.wikipedia.com');
newif.style.width = "500px";
newif.style.height = "100px";
newif.style.border = "none";
newif.setAttribute("allowtransparency", "true");
lnk.parentNode.replaceChild(newif, lnk);
}
}
})();
According to the MDN documentation:
Returns a list of elements with the given tag name. The subtree underneath the specified element is searched, excluding the element itself. The returned list is live, meaning that it updates itself with the DOM tree automatically. Consequently, there is no need to call several times element.getElementsByTagName with the same element and arguments.
Therefore, the collection shrinks every time you replace an a. You could change your loop to decrement i whenever you do a replace.
Related
When I run this script, the page is continuously loading and eventually freezes. Is this because everytime I create an element, the main DOMContentLoaded listener is being called?
If so, how can I stop this recursive behaviour and just add one node to every pre existing node?
//Waits for page to load
document.addEventListener('DOMContentLoaded', function() {
//Get all elements
var items = document.getElementsByTagName("*");
//Loop through entire DOM
for (var i = 0; i < items.length; i++) {
//If it is not a text node
if (!(items[i].nodeType == 3)){
//Create a div
var newDiv = document.createElement("div");
//Add div to current object
items[i].appendChild(newDiv);
}
}
});
It's because items is referencing a "live list". This means that any updates to the DOM are going to be reflected in your list if they match the original selector.
Because you're appending a div, and your selector selects all elements, it gets added to the list, pushing any subsequent members up an index, and so the iteration continues.
To avoid this, make a non-live copy of the collection before iterating.
var items = Array.from(document.getElementsByTagName("*"));
And FYI, the if (!(items[i].nodeType == 3)){ can be removed because getElementsByTagName will never return text nodes.
If you're supporting very old versions of IE, you may want to check that the .nodeType === 1, since some of those old versions included comment nodes when using "*".
Lastly, you can use modern features to clean this up a bit.
document.addEventListener('DOMContentLoaded', () => {
for (const el of [...document.getElementsByTagName("*")]) {
var newDiv = el.appendChild(document.createElement("div"));
// Work with newDiv
}
});
You get a continuously loading page because you have this:
var items = document.getElementsByTagName("*");
Which returns a "live" node list - meaning a list that can/will change as the matched elements change. Since your selector is for everything and then you create new elements, the set of matched elements changes (it gets bigger). Which, in turn causes the length of the node list to change, which keeps your loop running.
You should not try to match all the elements or you should use another method for getting the elements that doesn't return a live node list, like querySelectoAll()
I want to get all the <a> tags from an Html page with this JavaScript method:
function() {
var links = document.getElementsByTagName('a');
var i=0;
for (var link in links){i++;}
return i;
}
And i noticed it's won't return the correct number of a tags.
Any idea what can by the problem?
Any idea if there is any other way to get all the href in an Html ?
Edit
I tried this method on this html : http://mp3skull.com/mp3/nirvana.html .
And i get this result:"1". but there are more results in the page.
You don't need a loop here. You can read length property.
function getACount() {
return document.getElementsByTagName('a').length;
}
You don't have to loop over all of them just to count them. HTMLCollections (the type of Object that is returned by getElementsByTagName has a .length property:
$countAnchors = function () {
return document.getElementsByTagName('a').length;
}
Using getElementsByTagName("a") will return all anchor tags, not only the anchor tags that are links. An anchor tags needs a value for the href property to be a link.
You might be better off with the links property, that returns the links in the page:
var linkCount = document.links.length;
Note that this also includes area tags that has a href attribute, but I assume that you don't have any of those.
UPDATE Also gets href
You could do this
var linkCount = document.body.querySelectorAll('a').length,
hrefs= document.body.querySelectorAll('a[href]');
EDIT See the comment below, thanks to ttepasse
I would cast them to an array which you then slice up, etc.
var array = [];
var links = document.getElementsByTagName("a");
for(var i=0; i<links.length; i++) {
array.push(links[i].href);
}
var hrefs = array.length;
The JavaScript code in the question works as such or, rather, could be used to create a working solution (it’s now just an anonymous function declaration). It could be replaced by simpler code that just uses document.getElementsByTagName('a').length as others have remarked.
The problem however is how you use it: where it is placed, and when it is executed. If you run the code at a point where only one a element has been parsed, the result is 1. It needs to be executed when all a elements have been parsed. A simple way to ensure this is to put the code at the end of the document body. I tested by taking a local copy of the page mentioned and added the following right before the other script elements at the end of document body:
<script>
var f = function() {
var links = document.getElementsByTagName('a');
var i=0;
for (var link in links){i++;}
return i;
};
alert('a elements: ' + f());
</script>
The results are not consistent, even on repeated load of the page on the same browser, but this is probably caused by some dynamics on the page, making the number of a elements actually vary.
What you forget here was the length property. I think that code would be:
var count = 0;
for (var i = 0; i < links.length; i++) {
count++;
}
return count;
Or it would be:
for each (var link in links) {
i++;
}
length is used to determine or count the total number of the element which are the result.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for (For Loop)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for_each...in (Foreach Loop)
my question is while vague, specific. I would like to know the simplest means of changing each and every (10+) anchor tags href value to a corresponding array item. the page in question being constructed from the bottom up meaning that each new post is above the last new post, so I have an array of links starting with the link that corresponds to the bottom most and thereby first placed and last in order post. so far (in theory) I think that a variable that returns each array value a reverse traversal of that array that is used in a function that selects and traverses each anchor tag would be the solution.
var standard_anchor = new Array();
standard_anchor[0] = "http://whatever.com/";
standard_anchor[1] = "http://www.egs.edu/faculty/jean-baudrillard/articles/simulacra-and-simulations-viii-the-implosion-of-meaning-in-the-media/";
var standard = $(function(){
//should return reversed iterated standard anchor array
});
////
$('a [href]').each(function(){return (standard)});
that's as much as I can imagine.
You can use document.links, it's a live collection so you only need one reference, it will update as the DOM is updated.
You can iterate over it backwards using a while loop:
var links = document.links;
var i = links.length;
while (i--) {
// do stuff with links[i]
}
I've searched quite a bit on both google and stackoverflow, but a lack of knowledge on how to ask the question (or even if I'm asking the right question at all) is making it hard to find pertinent information.
I have a simple block of code that I am experimenting with to teach myself javascript.
var studio = document.getElementById('studio');
var contact = document.getElementById('contact');
var nav = document.getElementById('nav');
var navLinks = nav.getElementsByTagName('a');
var title = navLinks.getAttribute('title');
I want to grab the title attribute from the links in the element with the ID 'nav'.
Whenever I look at the debugger, it tells me that Object #<NodeList> has no method 'getAttribute'
I have no idea where I'm going wrong.
The nodetype and nodevalue for navLinks comes back as undefined, which I believe may be part of the problem, but I'm so new to this that I honestly have no idea.
The getElementsByTagName method returns an array of objects. So you need to loop through this array in order to get individual elements and their attributes:
var navLinks = nav.getElementsByTagName('a');
for (var i = 0; i < navLinks.length; i++) {
var link = navLinks[i];
var title = link.title;
}
Calling nav.getElementsByTagName('a') returns list of objects. And that list doesn't have getAttribute() method. You must call it on ONE object.
When you do:
navLinks[0].getAttribute('title')
then it should work - you will get title of the first matched element.
var navLinks = nav.getElementsByTagName('a');
getElementsByTagName returns multiple elements (hence Elements), because there can be multiple elements on one page with the same tag name. A NodeList (which is a collection of nodes as returned by getElementsByTagName) does not have a getAttribute method.
You need to access the property of the element that you actually need. My guess is that this will be the first element you find.
var title = navLinks[0].getAttribute('title');
I'm writing an extension to jQuery that adds data to DOM elements using
el.data('lalala', my_data);
and then uses that data to upload elements dynamically.
Each time I get new data from the server I need to update all elements having
el.data('lalala') != null;
To get all needed elements I use an extension by James Padolsey:
$(':data(lalala)').each(...);
Everything was great until I came to the situation where I need to run that code 50 times - it is very slow! It takes about 8 seconds to execute on my page with 3640 DOM elements
var x, t = (new Date).getTime();
for (n=0; n < 50; n++) {
jQuery(':data(lalala)').each(function() {
x++;
});
};
console.log(((new Date).getTime()-t)/1000);
Since I don't need RegExp as parameter of :data selector I've tried to replace this by
var x, t = (new Date).getTime();
for (n=0; n < 50; n++) {
jQuery('*').each(function() {
if ($(this).data('lalala'))
x++;
});
};
console.log(((new Date).getTime()-t)/1000);
This code is faster (5 sec), but I want get more.
Q Are there any faster way to get all elements with this data key?
In fact, I can keep an array with all elements I need, since I execute .data('key') in my module. Checking 100 elements having the desired .data('lalala') is better then checking 3640 :)
So the solution would be like
for (i in elements) {
el = elements[i];
....
But sometimes elements are removed from the page (using jQuery .remove()). Both solutions described above [$(':data(lalala)') solution and if ($(this).data('lalala'))] will skip removed items (as I need), while the solution with array will still point to removed element (in fact, the element would not be really deleted - it will only be deleted from the DOM tree - because my array will still have a reference).
I found that .remove() also removes data from the node, so my solution will change into
var toRemove = [];
for (vari in elements) {
var el = elements[i];
if ($(el).data('lalala'))
....
else
toRemove.push(i);
};
for (var ii in toRemove)
elements.splice(toRemove[ii], 1); // remove element from array
This solution is 100 times faster!
Q Will the garbage collector release memory taken by DOM elements when deleted from that array?
Remember, elements have been referenced by DOM tree, we made a new reference in our array, then removed with .remove() and then removed from the array.
Is there a better way to do this?
Are there any faster way to get all elements with this data key?
Sure!, loop over the data store directly instead of via the element, for example if you wanted a count:
var x=0;
for(var key in $.cache) {
if(typeof $.cache[key]["lalala"] != "undefined") x++;
}
This will be nearly instant, since elements only have an entry in $.cache if they have data and/or events, and there's no DOM traversal happening.
For the other piece, yes this will skip removed elements, since their cache is cleaned up as well, provided you don't remove them via .innerHTML directly.
Since V1.4.3 jQuery supports the HTML5-data-attribute.
This means: if you set an HTML-attribute data-lalala you can also access these attribute using element.data('lalala') . This should be much faster, because you an use the
native attribute-selector $('*[data-lalala]') instead of some workarounds.
So you have to use:
el.attr('data-lalala', my_data);
instead of
el.data('lalala', my_data);
Note: As those data-attributes only allow strings, you'll need to store objects as a stringified JSON there, if you need to work with objects.