-
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
Conversation
…lectorAll` wrappers.
… context element.
…te is the context element.
:scope
in querySelector{,All}
by traversing the logical tree.:scope
in querySelector{,All}
by traversing the logical tree manually to match selectors.
…s for the descendant combinator.
…doc examples. (...) The formatter wants to format HTML snippets, but it keeps putting the comments in the examples on their own line, which makes it harder to tell what element they're referring to. To avoid this, I've stopped marking these snippets as HTML.
e703702
to
603e00f
Compare
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.
Looking really good. We need:
- 3 modes: (a) existing impl, (b) native QSA + filtering, (c) this impl.
- tests for each mode highlighting what works/fails in each.
* @return {!Array<!Element>} | ||
*/ | ||
const logicalQuerySelectorList = (contextElement, selectorList) => { | ||
return deduplicateAndFilterToDescendants( |
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.
What about just doing Array.from(new Set(arrayWithDups))
?
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.
I think this comment was on an older commit, but yeah this is now how the final deduplication is handled.
@@ -0,0 +1,204 @@ | |||
`logicalQuerySelectorAll` emulates `querySelectorAll` within Shady DOM's logical |
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.
This is awesome. Great write up.
combinators, | ||
}; | ||
}) | ||
.filter(({compoundSelectors}) => compoundSelectors.length > 0); |
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.
For what case is this filter needed?
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.
This probably isn't necessary. I just haven't tested how the library responds to all whitespace / the empty string yet.
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.
Removed.
const {compoundSelectors} = complexSelectorParts; | ||
const index = compoundSelectors.length - 1; | ||
if (matchesCompoundSelector(element, compoundSelectors[index])) { | ||
return [ |
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.
Why does this need to be an array?
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.
This probably doesn't need to be.
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.
Removed this one and a few others.
ancestor && ancestor instanceof Element; | ||
ancestor = ancestor[utils.SHADY_PREFIX + 'parentNode'] | ||
) { | ||
if (matchesCompoundSelector(ancestor, compoundSelector)) { |
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.
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?
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.
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>
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.
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. :/
@@ -0,0 +1,140 @@ | |||
/** |
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.
This package doesn't handle escape sequences :
console.log('a\\,b,c', splitSelector('a\\,b,c'))
// a\,b,c { selectors: [ 'a\\', 'b', 'c' ], joiners: [ ',', ',' ] }
console.log('a\\",b,c', splitSelector('a\\",b,c'))
// a\",b,c { selectors: [ 'a\\",b,c' ], joiners: [] }
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.
bf502b5
to
f8706be
Compare
…ings `ShadyDOM` global object.
0c81de3
to
0fc78a1
Compare
const matchesCompoundSelector = (element, compoundSelector) => { | ||
return ( | ||
utils.matchesSelector(element, compoundSelector) && | ||
(compoundSelector.indexOf(':scope') === -1 || element === contextElement) |
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.
This check is likely cheaper, do it first?
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.
Updated
* - `complexSelectorParts` is the decomposed version of the selector that | ||
* this cursor is attempting to match. | ||
* | ||
* - `position` is an element that matches a compound selector in |
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.
Nit: position
referring to an element is a little odd. Some suggestions: current
or cursor
or matchedElement
?
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.
SGTM, switched to matchedElement
.
* Deduplicates items in an array. | ||
* | ||
* This function could normally be implemented as merely `Array.from(new | ||
* Set(...))`. However, in IE 11, `Set` does not support being constructed with |
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.
This is likely more expensive than doing new Set([...])
. What about doing this the good way if it's supported?
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.
There's also the Array.from
issue. Do you think this is worth the complexity of checking if we're in the good case?
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.
Think it's fine.
return utils.flat( | ||
complexSelectors.map((complexSelectorParts) => { | ||
const {compoundSelectors} = complexSelectorParts; | ||
const index = compoundSelectors.length - 1; |
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
// `position`. | ||
for ( | ||
let ancestor = position[utils.SHADY_PREFIX + 'parentNode']; | ||
ancestor && ancestor instanceof Element; |
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.
Can we check nodeType
instead of using instanceof
? Should be better perf.
And/or don't we have parentElement
? Can we use that?
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.
Yeah, that should work - updated.
On another note, this question just made me realize that :host
and :host-context
won't work in this implementation. I don't think this is supported by the default implementation either since it also uses matches
, but I've added this to the list of unsupported stuff in the docs for this flag.
this[utils.NATIVE_PREFIX + 'querySelector'](selector) | ||
); | ||
const root = this[utils.SHADY_PREFIX + 'getRootNode'](); | ||
return utils.createPolyfilledHTMLCollection( |
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.
Shouldn't this just be the matched element?
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.
(reply in other comment)
packages/shadydom/src/shadydom.js
Outdated
@@ -118,6 +118,81 @@ if (utils.settings.inUse) { | |||
'nativeMethods': nativeMethods, | |||
'nativeTree': nativeTree, | |||
'patchElementProto': patchElementProto, | |||
// Use this setting to choose the implementation of `querySelector` and | |||
// `querySelectorAll`. The logical tree exposed by Shady DOM via the DOM |
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.
Maybe add: "The options provide different trade offs between accuracy and performance, as explained below."
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.
I added that sentence and reworded this paragraph a bit.
packages/shadydom/src/shadydom.js
Outdated
// correct. When given a selector that would not parse under a full | ||
// implementation, this implementation may fail silently. | ||
// | ||
// - Only top-level combinators and compound selectors are taken into |
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.
Maybe change this to "contextual combinators are not supported"?
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.
Yeah, this was a bit hard to describe since there's not really a standard term for this. 'Contextual combinators' is a bit confusing to me also, so I reworded it using 'nested selectors'. Do you think it's clearer after this update?
@@ -2906,6 +3105,352 @@ | |||
document.body.removeChild(d); | |||
}); | |||
}); | |||
|
|||
suite('querySelectorImplementation', function () { |
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.
- Make sure to also add asserts for
querySelector
in addition toquerySelectorAll
.
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.
Ok, I added them for the tests for the tree-walking parts, but I left them out of the parseSelectorList
tests since those are only to check if they parsed properly.
@@ -26,6 +26,12 @@ | |||
'shady-dynamic.html?forceCustomElements=true', | |||
'shady-dynamic.html?noPatch=true&forceCustomElements=true', | |||
'shady-dynamic.html?noPatch=on-demand&forceCustomElements=true', | |||
'shady-dynamic.html?querySelectorImplementation=selectorEngine', |
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.
Are there tests for querySelectorImplementation=native
?
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.
No, I've just added those though. I had to update some of them since they were doing things that aren't supported with that option.
…rySelectorAll`. (...) The `parseSelectorList` tests are not included since they're specifically for determining if the certain selectors are parsed as expected, rather than finding elements.
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.
LGTM
I've been looking into the Sauce tests since they were passing before and nothing meaningful happened between then and now that should have caused them to start completely refusing to run on a handful of older browsers. I see the same thing when running the full list of browsers on sauce locally but running those same browsers individually seems to work. I tried debugging a run with the full list and found that the older browsers were being served code that wasn't correctly compiled for their feature sets, so my best guess as of now is that Sauce Connect Proxy might be caching responses between different browser sessions that the single tunnel is used for, causing the older browsers later in the list to see code generated for the newer browsers earlier in the list. Sauce Connect Proxy used to have a |
I think #522 will resolve the test issue. |
:scope
in querySelector{,All}
by traversing the logical tree manually to match selectors.ShadyDOM.querySelectorImplementation
setting.
The test timeout probably needs to be increased; I had to restart them a few times. |
This PR splits the selectors passed to
querySelector
andquerySelectorAll
, then traverses the logical tree manually to match them. This fixes:scope
by allowing Shady DOM to decide if the candidate elements being tested against simple selectors containing:scope
are the context element and should therefore match.Only walk ancestors and inclusive descendants of the context element.querySelectorAll
result ordering, parsing)Fixes #480.