Skip to content

Commit

Permalink
fix(filter): Preserve order of filtered elements
Browse files Browse the repository at this point in the history
Fixes #7
  • Loading branch information
fb55 committed Feb 11, 2021
1 parent cea6d8d commit 229b944
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@typescript-eslint/prefer-for-of": 0,
"@typescript-eslint/member-ordering": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-use-before-define": [
2,
{ "functions": false }
Expand Down
4 changes: 4 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,9 @@ describe("index", () => {
expect(filter("p:first", ps)).toHaveLength(1);
expect(filter("div p:first", ps)).toHaveLength(1);
expect(filter("div:first p:first", ps)).toHaveLength(1);
expect(filter("p:nth-child(1), :last", ps)).toHaveLength(2);
expect(
filter("div p:not(:scope)", ps, { context: [ps[1]] })
).toHaveLength(1);
});
});
74 changes: 51 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,13 @@ export function filter(
elements: Element[],
options: Options = {}
): Element[] {
return DomUtils.uniqueSort(
filterParsed(parse(selector, options), elements, options)
) as Element[];
return filterParsed(parse(selector, options), elements, options);
}

/**
* Filter a set of elements by a selector.
*
* If there are multiple selectors, this can
* return elements multiple times; use `uniqueSort`
* to eliminate duplicates afterwards.
* Will return elements in the original order.
*
* @param selector Selector to filter by.
* @param elements Elements to filter.
Expand All @@ -86,42 +82,74 @@ function filterParsed(
if (elements.length === 0) return [];

const [plainSelectors, filteredSelectors] = groupSelectors(selector);
const results = [];
let found: undefined | Set<Element>;

if (plainSelectors.length) {
results.push(filterElements(elements, plainSelectors, options));
const filtered = filterElements(elements, plainSelectors, options);

// If there are no filters, just return
if (filteredSelectors.length === 0) {
return filtered;
}

// Otherwise, we have to do some filtering
if (filtered.length) {
found = new Set(filtered);
}
}

for (const filteredSelector of filteredSelectors) {
for (let i = 0; i < filteredSelectors.length; i++) {
const filteredSelector = filteredSelectors[i];
const missing = found
? elements.filter((e) => !found!.has(e))
: elements;

if (missing.length === 0) break;
let filtered: Element[];

if (filteredSelector.some(isTraversal)) {
/*
* Get one root node, run selector with the scope
* set to all of our nodes.
*/
const root = getDocumentRoot(elements[0]);
const sel = [...filteredSelector, CUSTOM_SCOPE_PSEUDO];
results.push(
findFilterElements(
root as Element,
sel,
options,
true,
elements
)
filtered = findFilterElements(
root as Element,
sel,
options,
true,
elements
);
} else {
// Performance optimization: If we don't have to traverse, just filter set.
results.push(
findFilterElements(elements, filteredSelector, options, false)
filtered = findFilterElements(
elements,
filteredSelector,
options,
false
);
}
}

if (results.length === 1) {
return results[0];
if (!found) {
/*
* If we haven't found anything before the last selector,
* just return what we found now.
*/
if (i === filteredSelectors.length - 1) {
return filtered;
}
if (filtered.length) {
found = new Set(filtered);
}
} else if (filtered.length) {
filtered.forEach((el) => found!.add(el));
}
}

return results.reduce((arr, rest) => [...arr, ...rest], []);
return typeof found !== "undefined"
? elements.filter((el) => found!.has(el))
: [];
}

export function select(
Expand Down

0 comments on commit 229b944

Please sign in to comment.