Skip to content

Commit d462e44

Browse files
authored
feat: Added HTMLElement#getElementsByTagName
1 parent 773cae3 commit d462e44

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ Note: Full css3 selector supported since v3.0.0.
112112

113113
Query CSS Selector to find matching node.
114114

115+
### HTMLElement#getElementsByTagName(tagName)
116+
117+
Get all elements with the specified tagName.
118+
119+
Note: * for all elements.
120+
115121
### HTMLElement#closest(selector)
116122

117123
Query closest element by css selector.

src/nodes/html.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,51 @@ export default class HTMLElement extends Node {
567567
// return null;
568568
}
569569

570+
/**
571+
* find elements by their tagName
572+
* @param {string} tagName the tagName of the elements to select
573+
*/
574+
public getElementsByTagName(tagName: string): Array<HTMLElement> {
575+
const upperCasedTagName = tagName.toUpperCase();
576+
const re: Array<Node> = [];
577+
const stack: Array<number> = [];
578+
579+
let currentNodeReference = this as Node;
580+
let index: number | undefined = 0;
581+
582+
// index turns to undefined once the stack is empty and the first condition occurs
583+
// which happens once all relevant children are searched through
584+
while (index !== undefined) {
585+
let child: HTMLElement | undefined;
586+
// make it work with sparse arrays
587+
do {
588+
child = currentNodeReference.childNodes[index++] as HTMLElement | undefined;
589+
} while (index < currentNodeReference.childNodes.length && child === undefined);
590+
591+
// if the child does not exist we move on with the last provided index (which belongs to the parentNode)
592+
if (child === undefined) {
593+
currentNodeReference = currentNodeReference.parentNode;
594+
index = stack.pop();
595+
596+
continue;
597+
}
598+
599+
if (child.nodeType === NodeType.ELEMENT_NODE) {
600+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagName#syntax
601+
if (tagName === '*' || child.tagName === upperCasedTagName) re.push(child);
602+
603+
// if children are existing push the current status to the stack and keep searching for elements in the level below
604+
if (child.childNodes.length > 0) {
605+
stack.push(index);
606+
currentNodeReference = child;
607+
index = 0;
608+
}
609+
}
610+
}
611+
612+
return re as Array<HTMLElement>;
613+
}
614+
570615
/**
571616
* traverses the Element and its parents (heading toward the document root) until it finds a node that matches the provided selector string. Will return itself or the matching ancestor. If no such element exists, it returns null.
572617
* @param selector a DOMString containing a selector list

test/html.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ describe('HTML Parser', function () {
194194
});
195195

196196
describe('HTMLElement', function () {
197-
198197
describe('#removeWhitespace()', function () {
199198
it('should remove whitespaces while preserving nodes with content', function () {
200199
const root = parseHTML('<p> \r \n \t <h5> 123&nbsp; </h5></p>');
@@ -510,7 +509,91 @@ describe('HTML Parser', function () {
510509
a.removeChild(c);
511510
a.childNodes.length.should.eql(1);
512511
});
512+
});
513513

514+
describe('#getElementsByTagName', function () {
515+
it('find the divs in proper order', function () {
516+
const root = parseHTML(`
517+
<section>
518+
<div data-test="1.0">
519+
<div data-test="1.1">
520+
<div data-test="1.1.1"></div>
521+
</div>
522+
</div>
523+
<div data-test="2.0"></div>
524+
</section>
525+
`);
526+
const divs = root.getElementsByTagName('div');
527+
528+
for (const div of divs) {
529+
div.tagName.should.eql('DIV');
530+
}
531+
532+
// the literal appearance order
533+
divs[0].attributes['data-test'].should.eql('1.0');
534+
divs[1].attributes['data-test'].should.eql('1.1');
535+
divs[2].attributes['data-test'].should.eql('1.1.1');
536+
divs[3].attributes['data-test'].should.eql('2.0');
537+
});
538+
539+
// check that really only children are found, no parents or anything
540+
it('only return relevant items', function () {
541+
const root = parseHTML(`
542+
<section>
543+
<div data-ignore="true"></div>
544+
545+
<div id="suit" data-ignore="true">
546+
<div data-ignore="false"></div>
547+
<div data-ignore="false"></div>
548+
</div>
549+
550+
<div data-ignore="true"></div>
551+
</section>
552+
`);
553+
const divs = root.querySelector('#suit').getElementsByTagName('div');
554+
555+
divs.length.should.eql(2);
556+
557+
for (const div of divs) {
558+
div.attributes['data-ignore'].should.eql('false');
559+
}
560+
});
561+
562+
it('return all elements if tagName is *', function () {
563+
const root = parseHTML(`
564+
<section>
565+
<div></div>
566+
<span></span>
567+
<p></p>
568+
</section>
569+
`);
570+
const items = root.getElementsByTagName('*');
571+
572+
items.length.should.eql(4);
573+
items[0].tagName.should.eql('SECTION');
574+
items[1].tagName.should.eql('DIV');
575+
items[2].tagName.should.eql('SPAN');
576+
items[3].tagName.should.eql('P');
577+
});
578+
579+
it('return an empty array if nothing is found', function () {
580+
const root = parseHTML('<section></section>');
581+
582+
root.getElementsByTagName('div').length.should.eql(0);
583+
});
584+
585+
it('allow sparse arrays', function () {
586+
const root = parseHTML(`
587+
<section>
588+
<div></div>
589+
<div></div>
590+
<div></div>
591+
</section>
592+
`);
593+
delete root.querySelector('section').childNodes[1];
594+
595+
root.getElementsByTagName('div').length.should.eql(2);
596+
});
514597
});
515598
});
516599

0 commit comments

Comments
 (0)