mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat(selectors): implement builtin selectors in new evaluator (#4579)
This commit is contained in:
		
							parent
							
								
									3121de403b
								
							
						
					
					
						commit
						016925cd16
					
				@ -17,8 +17,9 @@
 | 
				
			|||||||
import { CSSComplexSelector, CSSSimpleSelector, CSSComplexSelectorList, CSSFunctionArgument } from '../common/cssParser';
 | 
					import { CSSComplexSelector, CSSSimpleSelector, CSSComplexSelectorList, CSSFunctionArgument } from '../common/cssParser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type QueryContext = {
 | 
					export type QueryContext = {
 | 
				
			||||||
  scope: Element | ShadowRoot | Document;
 | 
					  scope: Element | Document;
 | 
				
			||||||
  // Place for more options, e.g. normalizing whitespace or piercing shadow.
 | 
					  pierceShadow: boolean;
 | 
				
			||||||
 | 
					  // Place for more options, e.g. normalizing whitespace.
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export type Selector = any; // Opaque selector type.
 | 
					export type Selector = any; // Opaque selector type.
 | 
				
			||||||
export interface SelectorEvaluator {
 | 
					export interface SelectorEvaluator {
 | 
				
			||||||
@ -42,6 +43,11 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
    this._engines.set('where', isEngine);
 | 
					    this._engines.set('where', isEngine);
 | 
				
			||||||
    this._engines.set('has', hasEngine);
 | 
					    this._engines.set('has', hasEngine);
 | 
				
			||||||
    this._engines.set('scope', scopeEngine);
 | 
					    this._engines.set('scope', scopeEngine);
 | 
				
			||||||
 | 
					    this._engines.set('text', textEngine);
 | 
				
			||||||
 | 
					    this._engines.set('matches-text', matchesTextEngine);
 | 
				
			||||||
 | 
					    this._engines.set('xpath', xpathEngine);
 | 
				
			||||||
 | 
					    for (const attr of ['id', 'data-testid', 'data-test-id', 'data-test'])
 | 
				
			||||||
 | 
					      this._engines.set(attr, createAttributeEngine(attr));
 | 
				
			||||||
    // TODO: host
 | 
					    // TODO: host
 | 
				
			||||||
    // TODO: host-context?
 | 
					    // TODO: host-context?
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -154,19 +160,19 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      const { selector: simple, combinator } = complex.simples[index];
 | 
					      const { selector: simple, combinator } = complex.simples[index];
 | 
				
			||||||
      if (combinator === '>') {
 | 
					      if (combinator === '>') {
 | 
				
			||||||
        const parent = parentElementOrShadowHostInScope(element, context.scope);
 | 
					        const parent = parentElementOrShadowHostInContext(element, context);
 | 
				
			||||||
        if (!parent || !this._matchesSimple(parent, simple, context))
 | 
					        if (!parent || !this._matchesSimple(parent, simple, context))
 | 
				
			||||||
          return false;
 | 
					          return false;
 | 
				
			||||||
        return this._matchesParents(parent, complex, index - 1, context);
 | 
					        return this._matchesParents(parent, complex, index - 1, context);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (combinator === '+') {
 | 
					      if (combinator === '+') {
 | 
				
			||||||
        const previousSibling = element === context.scope ? null : element.previousElementSibling;
 | 
					        const previousSibling = previousSiblingInContext(element, context);
 | 
				
			||||||
        if (!previousSibling || !this._matchesSimple(previousSibling, simple, context))
 | 
					        if (!previousSibling || !this._matchesSimple(previousSibling, simple, context))
 | 
				
			||||||
          return false;
 | 
					          return false;
 | 
				
			||||||
        return this._matchesParents(previousSibling, complex, index - 1, context);
 | 
					        return this._matchesParents(previousSibling, complex, index - 1, context);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (combinator === '') {
 | 
					      if (combinator === '') {
 | 
				
			||||||
        let parent = parentElementOrShadowHostInScope(element, context.scope);
 | 
					        let parent = parentElementOrShadowHostInContext(element, context);
 | 
				
			||||||
        while (parent) {
 | 
					        while (parent) {
 | 
				
			||||||
          if (this._matchesSimple(parent, simple, context)) {
 | 
					          if (this._matchesSimple(parent, simple, context)) {
 | 
				
			||||||
            if (this._matchesParents(parent, complex, index - 1, context))
 | 
					            if (this._matchesParents(parent, complex, index - 1, context))
 | 
				
			||||||
@ -174,12 +180,12 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
            if (complex.simples[index - 1].combinator === '')
 | 
					            if (complex.simples[index - 1].combinator === '')
 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          parent = parentElementOrShadowHostInScope(parent, context.scope);
 | 
					          parent = parentElementOrShadowHostInContext(parent, context);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (combinator === '~') {
 | 
					      if (combinator === '~') {
 | 
				
			||||||
        let previousSibling = element === context.scope ? null : element.previousElementSibling;
 | 
					        let previousSibling = previousSiblingInContext(element, context);
 | 
				
			||||||
        while (previousSibling) {
 | 
					        while (previousSibling) {
 | 
				
			||||||
          if (this._matchesSimple(previousSibling, simple, context)) {
 | 
					          if (this._matchesSimple(previousSibling, simple, context)) {
 | 
				
			||||||
            if (this._matchesParents(previousSibling, complex, index - 1, context))
 | 
					            if (this._matchesParents(previousSibling, complex, index - 1, context))
 | 
				
			||||||
@ -187,7 +193,7 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
            if (complex.simples[index - 1].combinator === '~')
 | 
					            if (complex.simples[index - 1].combinator === '~')
 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          previousSibling = previousSibling === context.scope ? null : previousSibling.previousElementSibling;
 | 
					          previousSibling = previousSiblingInContext(previousSibling, context);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -212,13 +218,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _callMatches(engine: SelectorEngine, element: Element, args: CSSFunctionArgument[], context: QueryContext): boolean {
 | 
					  private _callMatches(engine: SelectorEngine, element: Element, args: CSSFunctionArgument[], context: QueryContext): boolean {
 | 
				
			||||||
    return this._cached<boolean>(element, ['_callMatches', engine, args, context.scope], () => {
 | 
					    return this._cached<boolean>(element, ['_callMatches', engine, args, context.scope, context.pierceShadow], () => {
 | 
				
			||||||
      return engine.matches!(element, args, context, this);
 | 
					      return engine.matches!(element, args, context, this);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _callQuery(engine: SelectorEngine, args: CSSFunctionArgument[], context: QueryContext): Element[] {
 | 
					  private _callQuery(engine: SelectorEngine, args: CSSFunctionArgument[], context: QueryContext): Element[] {
 | 
				
			||||||
    return this._cached<Element[]>(args, ['_callQuery', engine, context.scope], () => {
 | 
					    return this._cached<Element[]>(args, ['_callQuery', engine, context.scope, context.pierceShadow], () => {
 | 
				
			||||||
      return engine.query!(context, args, this);
 | 
					      return engine.query!(context, args, this);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -229,11 +235,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _queryCSS(context: QueryContext, css: string): Element[] {
 | 
					  _queryCSS(context: QueryContext, css: string): Element[] {
 | 
				
			||||||
    return this._cached<Element[]>(css, ['_queryCSS', context], () => {
 | 
					    return this._cached<Element[]>(css, ['_queryCSS', context], () => {
 | 
				
			||||||
      const result: Element[] = [];
 | 
					      const result: Element[] = [];
 | 
				
			||||||
      function query(root: Element | ShadowRoot | Document) {
 | 
					      function query(root: Element | ShadowRoot | Document) {
 | 
				
			||||||
        result.push(...root.querySelectorAll(css));
 | 
					        result.push(...root.querySelectorAll(css));
 | 
				
			||||||
 | 
					        if (!context.pierceShadow)
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
        if ((root as Element).shadowRoot)
 | 
					        if ((root as Element).shadowRoot)
 | 
				
			||||||
          query((root as Element).shadowRoot!);
 | 
					          query((root as Element).shadowRoot!);
 | 
				
			||||||
        for (const element of root.querySelectorAll('*')) {
 | 
					        for (const element of root.querySelectorAll('*')) {
 | 
				
			||||||
@ -255,13 +263,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isEngine: SelectorEngine = {
 | 
					const isEngine: SelectorEngine = {
 | 
				
			||||||
  matches(element: Element, args: any[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
    if (args.length === 0)
 | 
					    if (args.length === 0)
 | 
				
			||||||
      throw new Error(`"is" engine expects non-empty selector list`);
 | 
					      throw new Error(`"is" engine expects non-empty selector list`);
 | 
				
			||||||
    return args.some(selector => evaluator.matches(element, selector, context));
 | 
					    return args.some(selector => evaluator.matches(element, selector, context));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  query(context: QueryContext, args: any[], evaluator: SelectorEvaluator): Element[] {
 | 
					  query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
 | 
				
			||||||
    if (args.length === 0)
 | 
					    if (args.length === 0)
 | 
				
			||||||
      throw new Error(`"is" engine expects non-empty selector list`);
 | 
					      throw new Error(`"is" engine expects non-empty selector list`);
 | 
				
			||||||
    const elements: Element[] = [];
 | 
					    const elements: Element[] = [];
 | 
				
			||||||
@ -273,7 +281,7 @@ const isEngine: SelectorEngine = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const hasEngine: SelectorEngine = {
 | 
					const hasEngine: SelectorEngine = {
 | 
				
			||||||
  matches(element: Element, args: any[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
    if (args.length === 0)
 | 
					    if (args.length === 0)
 | 
				
			||||||
      throw new Error(`"has" engine expects non-empty selector list`);
 | 
					      throw new Error(`"has" engine expects non-empty selector list`);
 | 
				
			||||||
    return evaluator.query({ ...context, scope: element }, args).length > 0;
 | 
					    return evaluator.query({ ...context, scope: element }, args).length > 0;
 | 
				
			||||||
@ -284,7 +292,7 @@ const hasEngine: SelectorEngine = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const scopeEngine: SelectorEngine = {
 | 
					const scopeEngine: SelectorEngine = {
 | 
				
			||||||
  matches(element: Element, args: any[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
    if (args.length !== 0)
 | 
					    if (args.length !== 0)
 | 
				
			||||||
      throw new Error(`"scope" engine expects no arguments`);
 | 
					      throw new Error(`"scope" engine expects no arguments`);
 | 
				
			||||||
    if (context.scope.nodeType === 9 /* Node.DOCUMENT_NODE */)
 | 
					    if (context.scope.nodeType === 9 /* Node.DOCUMENT_NODE */)
 | 
				
			||||||
@ -292,7 +300,7 @@ const scopeEngine: SelectorEngine = {
 | 
				
			|||||||
    return element === context.scope;
 | 
					    return element === context.scope;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  query(context: QueryContext, args: any[], evaluator: SelectorEvaluator): Element[] {
 | 
					  query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
 | 
				
			||||||
    if (args.length !== 0)
 | 
					    if (args.length !== 0)
 | 
				
			||||||
      throw new Error(`"scope" engine expects no arguments`);
 | 
					      throw new Error(`"scope" engine expects no arguments`);
 | 
				
			||||||
    if (context.scope.nodeType === 9 /* Node.DOCUMENT_NODE */) {
 | 
					    if (context.scope.nodeType === 9 /* Node.DOCUMENT_NODE */) {
 | 
				
			||||||
@ -306,13 +314,102 @@ const scopeEngine: SelectorEngine = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const notEngine: SelectorEngine = {
 | 
					const notEngine: SelectorEngine = {
 | 
				
			||||||
  matches(element: Element, args: any[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
    if (args.length === 0)
 | 
					    if (args.length === 0)
 | 
				
			||||||
      throw new Error(`"not" engine expects non-empty selector list`);
 | 
					      throw new Error(`"not" engine expects non-empty selector list`);
 | 
				
			||||||
    return !evaluator.matches(element, args, context);
 | 
					    return !evaluator.matches(element, args, context);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const textEngine: SelectorEngine = {
 | 
				
			||||||
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
 | 
					    if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
 | 
				
			||||||
 | 
					      throw new Error(`"text" engine expects a string and an optional flags string`);
 | 
				
			||||||
 | 
					    const text = args[0];
 | 
				
			||||||
 | 
					    const flags = args.length === 2 ? args[1] : '';
 | 
				
			||||||
 | 
					    const matcher = textMatcher(text, flags);
 | 
				
			||||||
 | 
					    return elementMatchesText(element, context, matcher);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const matchesTextEngine: SelectorEngine = {
 | 
				
			||||||
 | 
					  matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
 | 
					    if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
 | 
				
			||||||
 | 
					      throw new Error(`"matches-text" engine expects a regexp body and optional regexp flags`);
 | 
				
			||||||
 | 
					    const re = new RegExp(args[0], args.length === 2 ? args[1] : undefined);
 | 
				
			||||||
 | 
					    return elementMatchesText(element, context, s => re.test(s));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function textMatcher(text: string, flags: string): (s: string) => boolean {
 | 
				
			||||||
 | 
					  const normalizeSpace = flags.includes('s');
 | 
				
			||||||
 | 
					  const lowerCase = flags.includes('i');
 | 
				
			||||||
 | 
					  const substring = flags.includes('g');
 | 
				
			||||||
 | 
					  if (normalizeSpace)
 | 
				
			||||||
 | 
					    text = text.trim().replace(/\s+/g, ' ');
 | 
				
			||||||
 | 
					  if (lowerCase)
 | 
				
			||||||
 | 
					    text = text.toLowerCase();
 | 
				
			||||||
 | 
					  return (s: string) => {
 | 
				
			||||||
 | 
					    if (normalizeSpace)
 | 
				
			||||||
 | 
					      s = s.trim().replace(/\s+/g, ' ');
 | 
				
			||||||
 | 
					    if (lowerCase)
 | 
				
			||||||
 | 
					      s = s.toLowerCase();
 | 
				
			||||||
 | 
					    return substring ? s.includes(text) : s === text;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function elementMatchesText(element: Element, context: QueryContext, matcher: (s: string) => boolean) {
 | 
				
			||||||
 | 
					  if (element.nodeName === 'SCRIPT' || element.nodeName === 'STYLE' || document.head && document.head.contains(element))
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  if ((element instanceof HTMLInputElement) && (element.type === 'submit' || element.type === 'button') && matcher(element.value))
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  let lastText = '';
 | 
				
			||||||
 | 
					  for (let child = element.firstChild; child; child = child.nextSibling) {
 | 
				
			||||||
 | 
					    if (child.nodeType === 3 /* Node.TEXT_NODE */) {
 | 
				
			||||||
 | 
					      lastText += child.nodeValue;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (lastText && matcher(lastText))
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      lastText = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return !!lastText && matcher(lastText);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const xpathEngine: SelectorEngine = {
 | 
				
			||||||
 | 
					  query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
 | 
				
			||||||
 | 
					    if (args.length !== 1 || typeof args[0] !== 'string')
 | 
				
			||||||
 | 
					      throw new Error(`"xpath" engine expects a single string`);
 | 
				
			||||||
 | 
					    const document = context.scope.nodeType === 9 /* Node.DOCUMENT_NODE */ ? context.scope as Document : context.scope.ownerDocument;
 | 
				
			||||||
 | 
					    if (!document)
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    const result: Element[] = [];
 | 
				
			||||||
 | 
					    const it = document.evaluate(args[0], context.scope, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
 | 
				
			||||||
 | 
					    for (let node = it.iterateNext(); node; node = it.iterateNext()) {
 | 
				
			||||||
 | 
					      if (node.nodeType === 1 /* Node.ELEMENT_NODE */)
 | 
				
			||||||
 | 
					        result.push(node as Element);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createAttributeEngine(attr: string): SelectorEngine {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
 | 
				
			||||||
 | 
					      if (args.length === 0 || typeof args[0] !== 'string')
 | 
				
			||||||
 | 
					        throw new Error(`"${attr}" engine expects a single string`);
 | 
				
			||||||
 | 
					      return element.getAttribute(attr) === args[0];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
 | 
				
			||||||
 | 
					      if (args.length !== 1 || typeof args[0] !== 'string')
 | 
				
			||||||
 | 
					        throw new Error(`"${attr}" engine expects a single string`);
 | 
				
			||||||
 | 
					      const css = `[${attr}=${CSS.escape(args[0])}]`;
 | 
				
			||||||
 | 
					      return (evaluator as SelectorEvaluatorImpl)._queryCSS(context, css);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function parentElementOrShadowHost(element: Element): Element | undefined {
 | 
					function parentElementOrShadowHost(element: Element): Element | undefined {
 | 
				
			||||||
  if (element.parentElement)
 | 
					  if (element.parentElement)
 | 
				
			||||||
    return element.parentElement;
 | 
					    return element.parentElement;
 | 
				
			||||||
@ -322,8 +419,18 @@ function parentElementOrShadowHost(element: Element): Element | undefined {
 | 
				
			|||||||
    return (element.parentNode as ShadowRoot).host;
 | 
					    return (element.parentNode as ShadowRoot).host;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function parentElementOrShadowHostInScope(element: Element, scope: Element | ShadowRoot | Document): Element | undefined {
 | 
					function parentElementOrShadowHostInContext(element: Element, context: QueryContext): Element | undefined {
 | 
				
			||||||
  return element === scope ? undefined : parentElementOrShadowHost(element);
 | 
					  if (element === context.scope)
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  if (!context.pierceShadow)
 | 
				
			||||||
 | 
					    return element.parentElement || undefined;
 | 
				
			||||||
 | 
					  return parentElementOrShadowHost(element);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function previousSiblingInContext(element: Element, context: QueryContext): Element | undefined {
 | 
				
			||||||
 | 
					  if (element === context.scope)
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  return element.previousElementSibling || undefined;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sortInDOMOrder(elements: Element[]): Element[] {
 | 
					function sortInDOMOrder(elements: Element[]): Element[] {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user