Reputation: 461
function getElementsByClassName(cls) {
const result = [];
const checkClass = (element) => {
const children = element.children;
for (let i = 0; i < children.length; i++) {
if()
if (children[i].contains(cls)) {
result.push(children[i]);
}
if (children[i].hasChildNodes()) {
checkClass(children[i]); }
}
};
checkClass(document.body);
return result;
}
Hello, all.
From what I understand childNodes has undefined nodes like "text" that can't go through .contain method.
So I switched from childNodes to children and then I am still getting the same ERROR message as below.
getElementsByClassName("targetClassName")
VM1349:8 Uncaught TypeError: Failed to execute 'contains' on 'Node': parameter 1 is not of type 'Node'.
at checkClass (<anonymous>:8:23)
at getElementsByClassName (<anonymous>:18:3)
at <anonymous>:1:1
I understand issue has to do with while iterating through the node list and then trying to apply contains, but I just don't get why it won't execute it after switching it to elements rather than nodes.
Please advise.
Upvotes: 1
Views: 302
Reputation: 13417
// ... non live collection approach ...
//
// - does not "know" `String.prototype.trim`,
// - is unaware of `Array.prototype.includes` ...
// - ... as well as of `Element.prototype.classList`
//
function getElementsByClassName(node, className) {
const regXSplitWS = (/\s+/);
const regXTrimLeft = (/^\s+/);
const regXTrimRight = (/\s+$/);
function trim(str) {
return str.replace(regXTrimLeft, '').replace(regXTrimRight, '');
}
function getClassList(str) {
str = trim(String(str || ''));
return (
(!str && []) ||
str.split(regXSplitWS)
);
}
const classList = getClassList(className);
function doesMatchClassNameQuery(elmClassList) {
return (
!!elmClassList.length &&
!!classList.length &&
classList.every(function (queryName) {
return (elmClassList.indexOf(queryName) >= 0);
})
);
}
function query(collector, elm/*, idx, collection*/) {
if (doesMatchClassNameQuery(getClassList(elm.className))) {
collector.push(elm);
}
// query recursively.
return collector.concat(Array.from(elm.children).reduce(query, []));
}
// start querying.
return Array.from((node && node.children) || []).reduce(query, []);
}
console.log(
document.body.getElementsByClassName('baz bizz')
);
console.log(
getElementsByClassName(document.body, 'baz bizz')
);
console.log(
document.body.getElementsByClassName('foo')
);
console.log(
getElementsByClassName(document.body, 'foo')
);
console.log(getElementsByClassName());
console.log(getElementsByClassName(null, 'bar'));
console.log(getElementsByClassName(document.body));
console.log(document.body.getElementsByClassName(''));
.as-console-wrapper { min-height: 100%!important; top: 0; }
<div class="level-1 foo">
<div class="level-2a bar">
<div class="level-3a baz bizz" />
<div class="level-3b buzz " />
<div>
<div class="level-2b baz">
<div class="level-3c bizz buzz" />
<div class="level-3d foo bar" />
<div>
<div class="level-2c bizz buzz">
<div class="level-3e foo bar" />
<div class="level-3f baz bizz" />
<div>
<div>
Upvotes: 0
Reputation: 1075875
From what I understand childNodes has undefined nodes like "text" that can't go through .contain method.
I don't know what you mean by "undefined nodes," but Text nodes are perfectly valid arguments for contains
:
const div = document.getElementById("x");
const text = div.firstChild;
console.log(text.nodeName); // #text
console.log(div.contains(text)); // true
<div id="x">foo</div>
That said, using children
in your function is reasonable, since only Elements can have classes and your code doesn't need to work on document fragments (e.g., checking their contents), so you only need to look at Elements, not other kinds of nodes.
I assume it's this code:
if (children[i].contains(cls)) {
that gives you
parameter 1 is not of type 'Node'
cls
in your code is a string, not a Node. As the error says, contains
accepts Nodes. It doesn't accept strings.
A couple of side notes:
Properly recreating getElementsByClassName
in your own code is fairly complicated, because it returns a live HTMLCollection
, not a snapshot NodeList
like querySelectorAll
does. That means properly recreating it would require using a MutationObserver
to track changes over time and update the list you return as things change. But to recreate it without that "live" feature, you'll probably want a recursive function.
getElementsByClassName
accepts multiple class names, not just one, in a space-delimited string.
It works on the entire document, not just body
(elements in head
can have classes).
FWIW, a non-live solution's general form might look something like this:
function getElementsByClassName(cls) {
const classes = cls.split(" ");
return worker(document.documentElement, classes, []);
}
function worker(element, classes, result) {
if (/*element has all the classes*/) {
result.push(element);
}
for /*...loop through `children`...*/ {
worker(child, classes, result);
}
return result;
}
Or, as Peter Seliger points out, you could start with the HTMLCollection
from getElementsByTagName
and just filter it. I assume this is a learning exercise, so it depends on the purpose of the learning exercise.
Upvotes: 3