Skip to content

Commit

Permalink
Merge pull request #517 from webcomponents/shadydom-querySelector-sco…
Browse files Browse the repository at this point in the history
…pe-parse

[ShadyDOM] Add `ShadyDOM.querySelectorImplementation` setting.
  • Loading branch information
bicknellr authored Sep 15, 2022
2 parents 1d4f740 + 8621eeb commit 4756d22
Show file tree
Hide file tree
Showing 10 changed files with 1,473 additions and 52 deletions.
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;
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)) {
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

0 comments on commit 4756d22

Please sign in to comment.