-
Notifications
You must be signed in to change notification settings - Fork 166
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
Changes from all commits
e8441da
7ed74e4
0432af9
cf21398
d112b07
ed4caf3
0a04a1c
3697e08
3695eeb
b569f0b
c4edbf4
bc11939
b23f0db
028d03e
569d6d3
bf1897e
c0d92e9
8f1ebda
5b9a11f
603e00f
0b5467a
9fa7976
f8706be
8feea24
b4cd360
bf48a44
0cc92da
712d116
8ff99b7
7ccad0f
919ade7
419b87c
25a79af
080210a
27cbd49
f7f316a
682a537
9ba25d3
e7f73f5
07020d3
2a1c2ba
f4a5a73
5123f06
bc93800
3e47496
94fe63e
3b7bac0
f3d21bc
b0577ad
e68c62f
e1cfed9
a2b116a
fc375f0
9a745de
648c217
afc7406
91ec51d
aed3dcd
d60bbbb
565ce0c
f8c4faf
658f53a
a402450
32075fb
59e6ce9
2f36c06
199a201
0fc78a1
faa3fc1
86eb168
87028ba
386c48c
b5548b2
ab9dda1
52103e5
f0c953b
39868fe
67076d5
78d1f65
15c66c1
8621eeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, each ancestor matching That particular case will look something like this:
During this step,
During this step,
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or, maybe I should have said "Eventually, some of the cursors with 'algorithmic ancestor cursors' that pointed at the shallower |
||
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}'` | ||
); | ||
} | ||
}, | ||
|
||
/** | ||
|
@@ -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); | ||
}) | ||
); | ||
}, | ||
}); | ||
|
||
|
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added