Skip to content

Commit e703702

Browse files
committed
Match all selectors in a selector list simultaneously.
1 parent 5b9a11f commit e703702

File tree

3 files changed

+356
-253
lines changed

3 files changed

+356
-253
lines changed

packages/shadydom/src/patches/ParentNode.js

Lines changed: 152 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -131,106 +131,48 @@ export const ParentNodePatches = utils.getOwnPropertyDescriptors({
131131
},
132132
});
133133

134-
/**
135-
* Deduplicates items in an array.
136-
*
137-
* @param {!Array<T>} array
138-
* @return {!Array<T>}
139-
* @template T
140-
*/
141-
const deduplicateArray = (array) => Array.from(new Set(array));
142-
143-
/**
144-
* Deduplicates an array of elements and removes any that are not exclusive
145-
* descendants of `ancestor`.
146-
*
147-
* @param {!Element} ancestor
148-
* @param {!Array<!Element>} elements
149-
* @return {!Array<!Element>}
150-
*/
151-
const deduplicateAndFilterToDescendants = (ancestor, elements) => {
152-
return deduplicateArray(elements).filter((element) => {
153-
return (
154-
element !== ancestor && ancestor[utils.SHADY_PREFIX + 'contains'](element)
155-
);
156-
});
157-
};
158-
159134
/**
160135
* Performs the equivalent of `querySelectorAll` within Shady DOM's logical
161136
* model of the tree for a selector list.
162137
*
163-
* @param {!Element} contextElement
164-
* @param {string} selectorList
165-
* @return {!Array<!Element>}
166-
*/
167-
const logicalQuerySelectorList = (contextElement, selectorList) => {
168-
return deduplicateAndFilterToDescendants(
169-
contextElement,
170-
utils.flat(
171-
extractSelectors(selectorList).map((selector) => {
172-
return logicalQuerySingleSelector(contextElement, selector);
173-
})
174-
)
175-
);
176-
};
177-
178-
/**
179-
* Performs the equivalent of `querySelectorAll` within Shady DOM's logical
180-
* model of the tree for a single complex selector.
181-
*
182-
* See <./logicalQuerySingleSelector.md> for implementation details.
138+
* See <./logicalQuerySelectorAll.md> for implementation details.
183139
*
184140
* @param {!Element} contextElement
185-
* @param {string} complexSelector
141+
* @param {string} selectorList
186142
* @return {!Array<!Element>}
187143
*/
188-
const logicalQuerySingleSelector = (contextElement, complexSelector) => {
189-
const {
190-
'selectors': compoundSelectors,
191-
'joiners': combinators,
192-
} = splitSelectorBlocks(complexSelector);
193-
194-
if (compoundSelectors.length < 1) {
195-
return [];
196-
}
197-
144+
const logicalQuerySelectorAll = (contextElement, selectorList) => {
198145
/**
199-
* An object used to track the current position of a potential selector match.
200-
*
201-
* - `position` is the element that matches the compound selector last reached
202-
* by the selector engine.
203-
*
204-
* - `target` is the element that matches the selector if the cursor
205-
* eventually results in a complete match.
146+
* A key-renamed version of the return value of `extractSelectors`, which
147+
* describes a single complex selector.
206148
*
207149
* @typedef {{
208-
* position: !Element,
209-
* target: !Element,
150+
* compoundSelectors: !Array<string>,
151+
* combinators: !Array<string>,
210152
* }}
211153
*/
212-
let SelectorMatchingCursor;
154+
let ComplexSelectorParts;
213155

214156
/**
215-
* The list of selector matching cursors, initialized to point at all
216-
* descendants of `contextElement`'s root node that match the last compound
217-
* selector in `complexSelector`.
218-
*
219-
* For example, if `complexSelector` is `a > b + c`, then this list is
220-
* initialized to all descendants of `contextElement.getRootNode()` that match
221-
* the compound selector `c`.
222-
*
223-
* @type {!Array<!SelectorMatchingCursor>}
157+
* @type {!Array<!ComplexSelectorParts>}
224158
*/
225-
let cursors = query(
226-
contextElement[utils.SHADY_PREFIX + 'getRootNode'](),
227-
(node) => {
228-
return utils.matchesSelector(
229-
node,
230-
compoundSelectors[compoundSelectors.length - 1]
231-
);
232-
}
233-
).map((element) => ({position: element, target: element}));
159+
const complexSelectors = extractSelectors(selectorList)
160+
.map((complexSelector) => {
161+
const {
162+
'selectors': compoundSelectors,
163+
'joiners': combinators,
164+
} = splitSelectorBlocks(complexSelector);
165+
166+
return {
167+
compoundSelectors,
168+
combinators,
169+
};
170+
})
171+
.filter(({compoundSelectors}) => compoundSelectors.length > 0);
172+
173+
if (complexSelectors.length < 1) {
174+
return [];
175+
}
234176

235177
/**
236178
* Determines if a single compound selector matches an element. If the
@@ -248,39 +190,106 @@ const logicalQuerySingleSelector = (contextElement, complexSelector) => {
248190
);
249191
};
250192

251-
// Iterate backwards through the remaining combinators and compound selectors,
252-
// updating the cursors at each step.
253-
for (let i = combinators.length - 1; i >= 0; i--) {
254-
const combinator = combinators[i];
255-
const compoundSelector = compoundSelectors[i];
193+
/**
194+
* An object used to track the current position of a potential selector match.
195+
*
196+
* - `target` is the element that matches the selector if the cursor
197+
* eventually results in a complete match.
198+
*
199+
* - `complexSelectorParts` is the decomposed version of the selector that
200+
* this cursor is attempting to match.
201+
*
202+
* - `position` is an element that matches a compound selector in
203+
* `complexSelectorParts`.
204+
*
205+
* - `index` is the index of the compound selector in `complexSelectorParts`
206+
* that matched `position`.
207+
*
208+
* @typedef {{
209+
* target: !Element,
210+
* complexSelectorParts: !ComplexSelectorParts,
211+
* position: !Element,
212+
* index: number,
213+
* }}
214+
*/
215+
let SelectorMatchingCursor;
216+
217+
/**
218+
* The list of `SelectorMatchingCursor`s, initialized with cursors pointing at
219+
* all descendants of `contextElement` that match the last compound selector
220+
* in any complex selector in `selectorList`.
221+
*
222+
* @type {!Array<!SelectorMatchingCursor>}
223+
*/
224+
let cursors = utils.flat(
225+
query(contextElement, (_element) => true).map((element) => {
226+
return utils.flat(
227+
complexSelectors.map((complexSelectorParts) => {
228+
const {compoundSelectors} = complexSelectorParts;
229+
const index = compoundSelectors.length - 1;
230+
if (matchesCompoundSelector(element, compoundSelectors[index])) {
231+
return [
232+
{
233+
target: element,
234+
complexSelectorParts,
235+
position: element,
236+
index,
237+
},
238+
];
239+
} else {
240+
return [];
241+
}
242+
})
243+
);
244+
})
245+
);
256246

257-
if (combinator === ' ') {
258-
// Descendant combinator
259-
cursors = utils.flat(
260-
cursors.map((cursor) => {
247+
// At each step, any remaining cursors that have not finished matching (i.e.
248+
// with `cursor.index > 0`) should be replaced with new cursors for any valid
249+
// candidates that match the next compound selector.
250+
while (cursors.length > 0 && cursors.some((cursor) => cursor.index > 0)) {
251+
cursors = utils.flat(
252+
cursors.map((cursor) => {
253+
// Cursors with `index` of 0 have already matched and should not be
254+
// replaced or removed.
255+
if (cursor.index <= 0) {
256+
return cursor;
257+
}
258+
259+
const {
260+
target,
261+
position,
262+
complexSelectorParts,
263+
index: lastIndex,
264+
} = cursor;
265+
const index = lastIndex - 1;
266+
const combinator = complexSelectorParts.combinators[index];
267+
const compoundSelector = complexSelectorParts.compoundSelectors[index];
268+
269+
if (combinator === ' ') {
261270
const results = [];
262271

263-
// For `a b`, where existing cursors have `position`s matching `b`,
264-
// the candidates to test against `a` are all ancestors each cursor's
272+
// For `a b`, where existing cursors have `position`s matching `b`, the
273+
// candidates to test against `a` are all ancestors each cursor's
265274
// `position`.
266275
for (
267-
let ancestor = cursor.position[utils.SHADY_PREFIX + 'parentNode'];
276+
let ancestor = position[utils.SHADY_PREFIX + 'parentNode'];
268277
ancestor && ancestor instanceof Element;
269278
ancestor = ancestor[utils.SHADY_PREFIX + 'parentNode']
270279
) {
271280
if (matchesCompoundSelector(ancestor, compoundSelector)) {
272-
results.push({position: ancestor, target: cursor.target});
281+
results.push({
282+
target,
283+
complexSelectorParts,
284+
position: ancestor,
285+
index,
286+
});
273287
}
274288
}
275289

276290
return results;
277-
})
278-
);
279-
} else if (combinator === '>') {
280-
// Child combinator
281-
cursors = utils.flat(
282-
cursors.map((cursor) => {
283-
const parent = cursor.position[utils.SHADY_PREFIX + 'parentNode'];
291+
} else if (combinator === '>') {
292+
const parent = position[utils.SHADY_PREFIX + 'parentNode'];
284293

285294
// For `a > b`, where existing cursors have `position`s matching `b`,
286295
// the candidates to test against `a` are the parents of each cursor's
@@ -290,63 +299,70 @@ const logicalQuerySingleSelector = (contextElement, complexSelector) => {
290299
parent instanceof Element &&
291300
matchesCompoundSelector(parent, compoundSelector)
292301
) {
293-
return [{position: parent, target: cursor.target}];
302+
return [
303+
{
304+
target,
305+
complexSelectorParts,
306+
position: parent,
307+
index,
308+
},
309+
];
294310
}
295311

296312
return [];
297-
})
298-
);
299-
} else if (combinator === '+') {
300-
// Next-sibling combinator
301-
cursors = utils.flat(
302-
cursors.map((cursor) => {
313+
} else if (combinator === '+') {
303314
const sibling =
304-
cursor.position[utils.SHADY_PREFIX + 'previousElementSibling'];
315+
position[utils.SHADY_PREFIX + 'previousElementSibling'];
305316

306317
// For `a + b`, where existing cursors have `position`s matching `b`,
307318
// the candidates to test against `a` are the immediately preceding
308319
// siblings of each cursor's `position`.
309320
if (sibling && matchesCompoundSelector(sibling, compoundSelector)) {
310-
return [{position: sibling, target: cursor.target}];
321+
return [
322+
{
323+
target,
324+
complexSelectorParts,
325+
position: sibling,
326+
index,
327+
},
328+
];
311329
}
312330

313331
return [];
314-
})
315-
);
316-
} else if (combinator === '~') {
317-
// Subsequent-sibling combinator
318-
cursors = utils.flat(
319-
cursors.map((cursor) => {
332+
} else if (combinator === '~') {
320333
const results = [];
321334

322335
// For `a ~ b`, where existing cursors have `position`s matching `b`,
323-
// the candidates to test against `a` are all preceding siblings of
324-
// each cursor's `position`.
336+
// the candidates to test against `a` are all preceding siblings of each
337+
// cursor's `position`.
325338
for (
326339
let sibling =
327-
cursor.position[utils.SHADY_PREFIX + 'previousElementSibling'];
340+
position[utils.SHADY_PREFIX + 'previousElementSibling'];
328341
sibling;
329342
sibling = sibling[utils.SHADY_PREFIX + 'previousElementSibling']
330343
) {
331344
if (matchesCompoundSelector(sibling, compoundSelector)) {
332-
results.push({position: sibling, target: cursor.target});
345+
results.push({
346+
target,
347+
complexSelectorParts,
348+
position: sibling,
349+
index,
350+
});
333351
}
334352
}
335353

336354
return results;
337-
})
338-
);
339-
} else {
340-
// As of writing, there are no other combinators:
341-
// <https://drafts.csswg.org/selectors/#combinators>
342-
throw new Error(`Unrecognized combinator: '${combinator}'.`);
343-
}
355+
} else {
356+
// As of writing, there are no other combinators:
357+
// <https://drafts.csswg.org/selectors/#combinators>
358+
throw new Error(`Unrecognized combinator: '${combinator}'.`);
359+
}
360+
})
361+
);
344362
}
345363

346-
return deduplicateAndFilterToDescendants(
347-
contextElement,
348-
cursors.map((cursor) => cursor.target)
349-
);
364+
// Map remaining cursors to their `target` and deduplicate.
365+
return Array.from(new Set(cursors.map(({target}) => target)));
350366
};
351367

352368
export const QueryPatches = utils.getOwnPropertyDescriptors({
@@ -356,7 +372,7 @@ export const QueryPatches = utils.getOwnPropertyDescriptors({
356372
* @param {string} selector
357373
*/
358374
querySelector(selector) {
359-
return logicalQuerySelectorList(this, selector)[0] || null;
375+
return logicalQuerySelectorAll(this, selector)[0] || null;
360376
},
361377

362378
/**
@@ -378,7 +394,7 @@ export const QueryPatches = utils.getOwnPropertyDescriptors({
378394
);
379395
}
380396
return utils.createPolyfilledHTMLCollection(
381-
logicalQuerySelectorList(this, selector)
397+
logicalQuerySelectorAll(this, selector)
382398
);
383399
},
384400
});

0 commit comments

Comments
 (0)