Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ShadyDOM] Add ShadyDOM.querySelectorImplementation setting. #517

Merged
merged 81 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
e8441da
Add <https://github.com/perry-mitchell/css-selector-splitter>
bicknellr Jul 28, 2022
7ed74e4
css-selector-parser: Make the API minification safe; use real exports.
bicknellr Jul 29, 2022
0432af9
Add `logicalQuerySelectorAll` and use in `querySelector` and `querySe…
bicknellr Jul 29, 2022
cf21398
css-selector-splitter: Fix another minification error.
bicknellr Jul 29, 2022
d112b07
Deduplicate results and filter to only those within the same root.
bicknellr Jul 29, 2022
ed4caf3
Use `Set` to simplify `deduplicateArray`.
bicknellr Jul 29, 2022
0a04a1c
Fix attempts to call properties that are getters, not functions.
bicknellr Jul 29, 2022
3697e08
Fix filtering so that results are always exclusive descendants of the…
bicknellr Jul 29, 2022
3695eeb
Fix early returns in descendant and child combinators.
bicknellr Jul 29, 2022
b569f0b
Fix `contains` call to use the Shady DOM wrapper.
bicknellr Jul 29, 2022
c4edbf4
Simple selectors containing `:scope` should only match if the candida…
bicknellr Jul 29, 2022
bc11939
Add a default parameter value to satisfy Closure.
bicknellr Jul 30, 2022
b23f0db
`Array`'s `flatMap` is not compiled out by Closure, so implement it m…
bicknellr Aug 1, 2022
028d03e
Evaluate selectors in reverse to avoid walking entire descendant tree…
bicknellr Aug 1, 2022
569d6d3
Closure isn't compiling out `includes`; replace with `indexOf`.
bicknellr Aug 1, 2022
bf1897e
Remove `utils.flatMap`; use `utils.flat` directly.
bicknellr Aug 1, 2022
c0d92e9
Replace "simple selector" with "compound selector" where appropriate.
bicknellr Aug 2, 2022
8f1ebda
Add documentation.
bicknellr Aug 3, 2022
5b9a11f
Prevent the formatter from breaking the `logicalQuerySingleSelector` …
bicknellr Aug 3, 2022
603e00f
Match all selectors in a selector list simultaneously.
bicknellr Aug 4, 2022
0b5467a
Avoid `Set` iterable constructor and `Array.from` during deduplicatio…
bicknellr Aug 8, 2022
9fa7976
Add a new `querySelectorImplementation` setting.
bicknellr Aug 9, 2022
f8706be
Run all Shady DOM tests with `querySelectorImplementation === 'select…
bicknellr Aug 9, 2022
8feea24
Add docs for `querySelectorImplementation` and expose on the non-sett…
bicknellr Aug 10, 2022
b4cd360
Remove an unnecessary `filter` and some wrapper arrays.
bicknellr Aug 10, 2022
bf48a44
`Array::some` always returns `false` when the array is empty.
bicknellr Aug 10, 2022
0cc92da
Test that `>` works across shadow root boundaries.
bicknellr Aug 10, 2022
712d116
Test that the semantics of `:scope` are preserved.
bicknellr Aug 11, 2022
8ff99b7
Add tests for combinators in scenarios without shadow trees or `:scope`.
bicknellr Aug 11, 2022
7ccad0f
Merge remote-tracking branch 'origin/master' into shadydom-querySelec…
bicknellr Aug 11, 2022
919ade7
format
bicknellr Aug 11, 2022
419b87c
Use `wrapIfNeeded` and `wrap` as necessary for `noPatch` modes.
bicknellr Aug 11, 2022
25a79af
Skip `:scope` test if `:scope` isn't natively supported.
bicknellr Aug 15, 2022
080210a
Use a custom CSS selector parser.
bicknellr Aug 18, 2022
27cbd49
Remove `css-selector-splitter`.
bicknellr Aug 18, 2022
f7f316a
Fix Closure types.
bicknellr Aug 18, 2022
682a537
Remove dead code.
bicknellr Aug 18, 2022
9ba25d3
Merge remote-tracking branch 'origin/master' into shadydom-querySelec…
bicknellr Aug 23, 2022
e7f73f5
Merge branch 'shadydom-querySelector-scope-parse' into shadydom-query…
bicknellr Aug 23, 2022
07020d3
Merge branch 'shadydom-querySelector-scope-parse-custom' into shadydo…
bicknellr Aug 24, 2022
2a1c2ba
Use a single parens info map.
bicknellr Aug 24, 2022
f4a5a73
Remove outer whitespace early with `trim` instead.
bicknellr Aug 24, 2022
5123f06
Unzip compound selectors and combinators with `filter`.
bicknellr Aug 24, 2022
bc93800
`findIndex` now takes a start index.
bicknellr Aug 24, 2022
3e47496
Remove `COMBINATORS` constant.
bicknellr Aug 24, 2022
94fe63e
format
bicknellr Aug 24, 2022
3b7bac0
Remove all whitespace chunks next to other combinators.
bicknellr Aug 24, 2022
f3d21bc
Perform splitting and whitespace/combinator collapsing simultaneously.
bicknellr Aug 24, 2022
b0577ad
Fix a bug where `findNext`'s return value was being used as a length.
bicknellr Aug 24, 2022
e68c62f
Fix a bug where consecutive whitespace was not being collapsed.
bicknellr Aug 24, 2022
e1cfed9
Flatten `parseComplexSelector` into `parseSelectorList` to prevent du…
bicknellr Aug 24, 2022
a2b116a
`includes(...)` -> `indexOf(...) !== -1`
bicknellr Aug 25, 2022
fc375f0
Add comments for the selector parser.
bicknellr Aug 29, 2022
9a745de
Move the selector parser out of `patches/`.
bicknellr Aug 29, 2022
648c217
Update a comment.
bicknellr Aug 29, 2022
afc7406
Add known limitations to the docs for the `querySelectorImplementatio…
bicknellr Aug 29, 2022
91ec51d
Add a change log entry.
bicknellr Aug 30, 2022
aed3dcd
Add tests for escape sequences and nested commas.
bicknellr Aug 30, 2022
d60bbbb
Test that whitespace surrounding compound selectors and combinators i…
bicknellr Aug 30, 2022
565ce0c
Use `ShadyDOM.wrapIfNeeded(...)` on hosts before getting shadow roots.
bicknellr Aug 30, 2022
f8c4faf
Only run the `shady-dynamic.html` test suite with the selector engine.
bicknellr Aug 30, 2022
658f53a
Fix lint warnings.
bicknellr Aug 31, 2022
a402450
IE 11 doesn't support `Map`'s constructor parameter.
bicknellr Aug 31, 2022
32075fb
Test native selector support using Shady DOM's stored copy of `queryS…
bicknellr Aug 31, 2022
59e6ce9
Test that results from queries with selector lists are in document or…
bicknellr Aug 31, 2022
2f36c06
Use a template in a test instead of building the tree manually.
bicknellr Aug 31, 2022
199a201
Use `ShadyDOM.wrapIfNeeded(...)` where necessary.
bicknellr Aug 31, 2022
0fc78a1
Add `ShadyDOM.flush();` before querying in tests.
bicknellr Aug 31, 2022
faa3fc1
Reorder an expression to take advantage of short-circuiting.
bicknellr Sep 8, 2022
86eb168
`position` -> `matchedElement`
bicknellr Sep 8, 2022
87028ba
Add a comment explaining why selectors are iterated backwards while m…
bicknellr Sep 8, 2022
386c48c
Use `parentElement` instead of `parentNode` while searching for candi…
bicknellr Sep 8, 2022
b5548b2
Note in the docs that `:host` and `:host-context` are not supported.
bicknellr Sep 8, 2022
ab9dda1
Remove an outdated comment.
bicknellr Sep 8, 2022
52103e5
Fix the wrappers using the 'native' option and enable and update test…
bicknellr Sep 8, 2022
f0c953b
Update a comment for clarity.
bicknellr Sep 9, 2022
39868fe
Update a comment for clarity.
bicknellr Sep 9, 2022
67076d5
Tree-walking tests now check results of both `querySelector` and `que…
bicknellr Sep 9, 2022
78d1f65
`position` -> `matchedElement` in the `logicalQuerySelectorAll` docs.
bicknellr Sep 9, 2022
15c66c1
Merge remote-tracking branch 'origin/master' into shadydom-querySelec…
bicknellr Sep 15, 2022
8621eeb
Update change log.
bicknellr Sep 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/shadydom/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
original event as its prototype so that it can update `currentTarget` without
modifying the built-in property descriptor.
([#519](https://github.com/webcomponents/polyfills/pull/519))
- Add `ShadyDOM.querySelectorImplementation` setting.
([#517](https://github.com/webcomponents/polyfills/pull/517))

## [1.9.0] - 2021-08-02

Expand Down
1 change: 1 addition & 0 deletions packages/shadydom/externs/shadydom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ declare global {
};
noPatch: boolean | string;
patchElementProto: (node: Object) => void;
querySelectorImplementation?: 'native' | 'selectorEngine';
wrap: (node: Node) => Node;
}

Expand Down
288 changes: 268 additions & 20 deletions packages/shadydom/src/patches/ParentNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN

import * as utils from '../utils.js';
import {shadyDataForNode} from '../shady-data.js';
import {ComplexSelectorParts, parseSelectorList} from '../selector-parser.js'; // eslint-disable-line @typescript-eslint/no-unused-vars

/**
* @param {Node} node
Expand Down Expand Up @@ -128,24 +129,257 @@ export const ParentNodePatches = utils.getOwnPropertyDescriptors({
},
});

/**
* Performs the equivalent of `querySelectorAll` within Shady DOM's logical
* model of the tree for a selector list.
*
* See <./logicalQuerySelectorAll.md> for implementation details.
*
* @param {!Element} contextElement
* @param {string} selectorList
* @return {!Array<!Element>}
*/
const logicalQuerySelectorAll = (contextElement, selectorList) => {
/**
* @type {!Array<!ComplexSelectorParts>}
*/
const complexSelectors = parseSelectorList(selectorList);

if (complexSelectors.length < 1) {
return [];
}

/**
* Determines if a single compound selector matches an element. If the
* selector contains `:scope` (as a substring), then the selector only is only
* considered matching if `element` is `contextElement`.
*
* @param {!Element} element
* @param {string} compoundSelector
* @return {boolean}
*/
const matchesCompoundSelector = (element, compoundSelector) => {
return (
(element === contextElement ||
compoundSelector.indexOf(':scope') === -1) &&
utils.matchesSelector(element, compoundSelector)
);
};

/**
* An object used to track the current position of a potential selector match.
*
* - `target` is the element that would be considered matching if the cursor
* eventually results in a complete match.
*
* - `complexSelectorParts` is the parsed representation of the complex
* selector that this cursor is attempting to match.
*
* - `index` is the index into `.complexSelectorParts.compoundSelectors` of
* the last successfully matched compound selector.
*
* - `matchedElement` is the element that successfully matched the compound
* selector in `.complexSelectorParts.compoundSelectors` at `index`.
*
* @typedef {{
* target: !Element,
* complexSelectorParts: !ComplexSelectorParts,
* matchedElement: !Element,
* index: number,
* }}
*/
let SelectorMatchingCursor; // eslint-disable-line @typescript-eslint/no-unused-vars

/**
* The list of `SelectorMatchingCursor`s, initialized with cursors pointing at
* all descendants of `contextElement` that match the last compound selector
* in any complex selector in `selectorList`.
*
* @type {!Array<!SelectorMatchingCursor>}
*/
let cursors = utils.flat(
query(contextElement, (_element) => true).map((element) => {
return utils.flat(
complexSelectors.map((complexSelectorParts) => {
const {compoundSelectors} = complexSelectorParts;
// Selectors are matched by iterating their compound selectors in
// reverse order for efficiency. In particular, when finding
// candidates for the descendant combinator, iterating forwards would
// imply needing to walk all descendants of the last matched element
// for possible candidates, but iterating backwards only requires
// walking up the ancestor chain.
const index = compoundSelectors.length - 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a little comment here about why we're starting from the end (it's more efficient).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

if (matchesCompoundSelector(element, compoundSelectors[index])) {
return {
target: element,
complexSelectorParts,
matchedElement: element,
index,
};
} else {
return [];
}
})
);
})
);

// At each step, any remaining cursors that have not finished matching (i.e.
// with `cursor.index > 0`) should be replaced with new cursors for any valid
// candidates that match the next compound selector.
while (cursors.some((cursor) => cursor.index > 0)) {
cursors = utils.flat(
cursors.map((cursor) => {
// Cursors with `index` of 0 have already matched and should not be
// replaced or removed.
if (cursor.index <= 0) {
return cursor;
}

const {
target,
matchedElement,
complexSelectorParts,
index: lastIndex,
} = cursor;
const index = lastIndex - 1;
const combinator = complexSelectorParts.combinators[index];
const compoundSelector = complexSelectorParts.compoundSelectors[index];

if (combinator === ' ') {
const results = [];

// For `a b`, where existing cursors have `matchedElement`s matching
// `b`, the candidates to test against `a` are all ancestors of each
// cursor's `matchedElement`.
for (
let ancestor = matchedElement[utils.SHADY_PREFIX + 'parentElement'];
ancestor;
ancestor = ancestor[utils.SHADY_PREFIX + 'parentElement']
) {
if (matchesCompoundSelector(ancestor, compoundSelector)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't obvious to me why results would ever be an array so maybe this should be commented, but the reason is that there may be > 1 candidate in the ancestor tree...

e.g.: given c c c d and <c><c><c><d>: the first cursor will be for d and next we need a cursor for each c, but eventually the one for the top most c will survive. Is that right?

Copy link
Collaborator Author

@bicknellr bicknellr Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, each ancestor matching c needs a cursor because any of them could potentially be part of the match when it's only gotten around to testing the rightmost c in the selector. The cursors that point at the shallower <c>s will eventually be unable to find enough candidate <c> ancestors to match the rest of the selector and will be replaced with zero cursors.

That particular case will look something like this:

<c>
  <c>
    <c>
      <d> <!-- cursor[0] `c c c (d)`-->
<c> <!-- candidate for: cursor[0] `c c c( )d` -->
  <c> <!-- candidate for: cursor[0] `c c c( )d` -->
    <c> <!-- candidate for: cursor[0] `c c c( )d` -->
      <d>
<c> <!-- new cursor[2] `c c (c) d` -->
  <c> <!-- new cursor[1] `c c (c) d` -->
    <c> <!-- new cursor[0] `c c (c) d` -->
      <d>

During this step, cursor[2] doesn't find any candidates:

<c> <!-- candidate for: cursor[0] `c c( )c d` and cursor[1] `c c( )c d` -->
  <c> <!-- candidate for: cursor[0] `c c( )c d` -->
    <c>
      <d>
<c> <!-- new cursor[1] `c (c) c d`, new cursor[2] `c (c) c d` -->
  <c> <!-- new cursor[0] `c (c) c d` -->
    <c>
      <d>

During this step, cursor[1] doesn't find any candidates:

<c> <!-- candidate for: cursor[0] `c( )c c d` -->
  <c>
    <c>
      <d>
<c> <!-- new cursor[0]: `(c) c c d` -->
  <c>
    <c>
      <d>

Copy link
Collaborator Author

@bicknellr bicknellr Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursors that point at the shallower <c>s will eventually be unable to find enough candidate <c> ancestors to match the rest of the selector and will be replaced with zero cursors.

Or, maybe I should have said "Eventually, some of the cursors with 'algorithmic ancestor cursors' that pointed at the shallower <c>s will be unable..." since only cursors that have already succeeded in making a complete match match actually 'survive' between iterations - all others are replaced with zero or more new cursors. The overlapping tree / lineage terminology that's shared here between the DOM and the timeline of cursors makes describing this a bit difficult. :/

results.push({
target,
complexSelectorParts,
matchedElement: ancestor,
index,
});
}
}

return results;
} else if (combinator === '>') {
const parent = matchedElement[utils.SHADY_PREFIX + 'parentElement'];

// For `a > b`, where existing cursors have `matchedElement`s matching
// `b`, the candidates to test against `a` are the parents of each
// cursor's `matchedElement`.
if (matchesCompoundSelector(parent, compoundSelector)) {
return {
target,
complexSelectorParts,
matchedElement: parent,
index,
};
}

return [];
} else if (combinator === '+') {
const sibling =
matchedElement[utils.SHADY_PREFIX + 'previousElementSibling'];

// For `a + b`, where existing cursors have `matchedElement`s matching
// `b`, the candidates to test against `a` are the immediately
// preceding siblings of each cursor's `matchedElement`.
if (sibling && matchesCompoundSelector(sibling, compoundSelector)) {
return {
target,
complexSelectorParts,
matchedElement: sibling,
index,
};
}

return [];
} else if (combinator === '~') {
const results = [];

// For `a ~ b`, where existing cursors have `matchedElement`s matching
// `b`, the candidates to test against `a` are all preceding siblings
// of each cursor's `matchedElement`.
for (
let sibling =
matchedElement[utils.SHADY_PREFIX + 'previousElementSibling'];
sibling;
sibling = sibling[utils.SHADY_PREFIX + 'previousElementSibling']
) {
if (matchesCompoundSelector(sibling, compoundSelector)) {
results.push({
target,
complexSelectorParts,
matchedElement: sibling,
index,
});
}
}

return results;
} else {
// As of writing, there are no other combinators:
// <https://drafts.csswg.org/selectors/#combinators>
throw new Error(`Unrecognized combinator: '${combinator}'.`);
}
})
);
}

// Map remaining cursors to their `target` and deduplicate.
return utils.deduplicate(cursors.map(({target}) => target));
};

const querySelectorImplementation =
utils.settings['querySelectorImplementation'];

export const QueryPatches = utils.getOwnPropertyDescriptors({
// TODO(sorvell): consider doing native QSA and filtering results.
/**
* @this {Element}
* @param {string} selector
*/
querySelector(selector) {
// match selector and halt on first result.
let result = query(
this,
function (n) {
return utils.matchesSelector(n, selector);
},
function (n) {
return Boolean(n);
if (querySelectorImplementation === 'native') {
// Polyfilled `ShadowRoot`s don't have a native `querySelectorAll`.
const target = this instanceof ShadowRoot ? this.host : this;
const candidates = Array.prototype.slice.call(
target[utils.NATIVE_PREFIX + 'querySelectorAll'](selector)
);
const root = this[utils.SHADY_PREFIX + 'getRootNode']();
// This could use `find`, but Closure doesn't polyfill it.
for (const candidate of candidates) {
if (candidate[utils.SHADY_PREFIX + 'getRootNode']() == root) {
return candidate;
}
}
)[0];
return result || null;
return null;
} else if (querySelectorImplementation === 'selectorEngine') {
return logicalQuerySelectorAll(this, selector)[0] || null;
} else if (querySelectorImplementation === undefined) {
// match selector and halt on first result.
let result = query(
this,
function (n) {
return utils.matchesSelector(n, selector);
},
function (n) {
return Boolean(n);
}
)[0];
return result || null;
} else {
throw new Error(
'Unrecognized value of ShadyDOM.querySelectorImplementation: ' +
`'${querySelectorImplementation}'`
);
}
},

/**
Expand All @@ -157,20 +391,34 @@ export const QueryPatches = utils.getOwnPropertyDescriptors({
// misses distributed nodes, see
// https://github.com/webcomponents/shadydom/pull/210#issuecomment-361435503
querySelectorAll(selector, useNative) {
if (useNative) {
const o = Array.prototype.slice.call(
this[utils.NATIVE_PREFIX + 'querySelectorAll'](selector)
if (useNative || querySelectorImplementation === 'native') {
// Polyfilled `ShadowRoot`s don't have a native `querySelectorAll`.
const target = this instanceof ShadowRoot ? this.host : this;
const candidates = Array.prototype.slice.call(
target[utils.NATIVE_PREFIX + 'querySelectorAll'](selector)
);
const root = this[utils.SHADY_PREFIX + 'getRootNode']();
return utils.createPolyfilledHTMLCollection(
o.filter((e) => e[utils.SHADY_PREFIX + 'getRootNode']() == root)
candidates.filter(
(e) => e[utils.SHADY_PREFIX + 'getRootNode']() == root
)
);
} else if (querySelectorImplementation === 'selectorEngine') {
return utils.createPolyfilledHTMLCollection(
logicalQuerySelectorAll(this, selector)
);
} else if (querySelectorImplementation === undefined) {
return utils.createPolyfilledHTMLCollection(
query(this, function (n) {
return utils.matchesSelector(n, selector);
})
);
} else {
throw new Error(
'Unrecognized value of ShadyDOM.querySelectorImplementation: ' +
`'${querySelectorImplementation}'`
);
}
return utils.createPolyfilledHTMLCollection(
query(this, function (n) {
return utils.matchesSelector(n, selector);
})
);
},
});

Expand Down
Loading