Skip to content

Commit

Permalink
feat: Added HTMLElement#getElementsByTagName
Browse files Browse the repository at this point in the history
  • Loading branch information
steve-py96 authored Oct 6, 2021
1 parent 773cae3 commit d462e44
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ Note: Full css3 selector supported since v3.0.0.

Query CSS Selector to find matching node.

### HTMLElement#getElementsByTagName(tagName)

Get all elements with the specified tagName.

Note: * for all elements.

### HTMLElement#closest(selector)

Query closest element by css selector.
Expand Down
45 changes: 45 additions & 0 deletions src/nodes/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,51 @@ export default class HTMLElement extends Node {
// return null;
}

/**
* find elements by their tagName
* @param {string} tagName the tagName of the elements to select
*/
public getElementsByTagName(tagName: string): Array<HTMLElement> {
const upperCasedTagName = tagName.toUpperCase();
const re: Array<Node> = [];
const stack: Array<number> = [];

let currentNodeReference = this as Node;
let index: number | undefined = 0;

// index turns to undefined once the stack is empty and the first condition occurs
// which happens once all relevant children are searched through
while (index !== undefined) {
let child: HTMLElement | undefined;
// make it work with sparse arrays
do {
child = currentNodeReference.childNodes[index++] as HTMLElement | undefined;
} while (index < currentNodeReference.childNodes.length && child === undefined);

// if the child does not exist we move on with the last provided index (which belongs to the parentNode)
if (child === undefined) {
currentNodeReference = currentNodeReference.parentNode;
index = stack.pop();

continue;
}

if (child.nodeType === NodeType.ELEMENT_NODE) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagName#syntax
if (tagName === '*' || child.tagName === upperCasedTagName) re.push(child);

// if children are existing push the current status to the stack and keep searching for elements in the level below
if (child.childNodes.length > 0) {
stack.push(index);
currentNodeReference = child;
index = 0;
}
}
}

return re as Array<HTMLElement>;
}

/**
* 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.
* @param selector a DOMString containing a selector list
Expand Down
85 changes: 84 additions & 1 deletion test/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ describe('HTML Parser', function () {
});

describe('HTMLElement', function () {

describe('#removeWhitespace()', function () {
it('should remove whitespaces while preserving nodes with content', function () {
const root = parseHTML('<p> \r \n \t <h5> 123&nbsp; </h5></p>');
Expand Down Expand Up @@ -510,7 +509,91 @@ describe('HTML Parser', function () {
a.removeChild(c);
a.childNodes.length.should.eql(1);
});
});

describe('#getElementsByTagName', function () {
it('find the divs in proper order', function () {
const root = parseHTML(`
<section>
<div data-test="1.0">
<div data-test="1.1">
<div data-test="1.1.1"></div>
</div>
</div>
<div data-test="2.0"></div>
</section>
`);
const divs = root.getElementsByTagName('div');

for (const div of divs) {
div.tagName.should.eql('DIV');
}

// the literal appearance order
divs[0].attributes['data-test'].should.eql('1.0');
divs[1].attributes['data-test'].should.eql('1.1');
divs[2].attributes['data-test'].should.eql('1.1.1');
divs[3].attributes['data-test'].should.eql('2.0');
});

// check that really only children are found, no parents or anything
it('only return relevant items', function () {
const root = parseHTML(`
<section>
<div data-ignore="true"></div>
<div id="suit" data-ignore="true">
<div data-ignore="false"></div>
<div data-ignore="false"></div>
</div>
<div data-ignore="true"></div>
</section>
`);
const divs = root.querySelector('#suit').getElementsByTagName('div');

divs.length.should.eql(2);

for (const div of divs) {
div.attributes['data-ignore'].should.eql('false');
}
});

it('return all elements if tagName is *', function () {
const root = parseHTML(`
<section>
<div></div>
<span></span>
<p></p>
</section>
`);
const items = root.getElementsByTagName('*');

items.length.should.eql(4);
items[0].tagName.should.eql('SECTION');
items[1].tagName.should.eql('DIV');
items[2].tagName.should.eql('SPAN');
items[3].tagName.should.eql('P');
});

it('return an empty array if nothing is found', function () {
const root = parseHTML('<section></section>');

root.getElementsByTagName('div').length.should.eql(0);
});

it('allow sparse arrays', function () {
const root = parseHTML(`
<section>
<div></div>
<div></div>
<div></div>
</section>
`);
delete root.querySelector('section').childNodes[1];

root.getElementsByTagName('div').length.should.eql(2);
});
});
});

Expand Down

0 comments on commit d462e44

Please sign in to comment.