Browse Source

fix: can't query element inside closed shadow dom

Ahmad Kholid 2 years ago
parent
commit
e58d425938

+ 0 - 1
package.json

@@ -81,7 +81,6 @@
     "prosemirror-history": "^1.3.0",
     "prosemirror-keymap": "^1.2.0",
     "prosemirror-schema-list": "^1.2.2",
-    "query-selector-shadow-dom": "^1.0.1",
     "read-excel-file": "^5.5.3",
     "rxjs": "^7.8.0",
     "sizzle": "^2.3.8",

+ 226 - 0
src/lib/query-selector-shadow-dom/index.js

@@ -0,0 +1,226 @@
+/* eslint-disable */
+/* 
+Forked from: https://github.com/webdriverio/query-selector-shadow-dom/
+*/
+
+import { normalizeSelector } from './normalize';
+
+/**
+* Finds first matching elements on the page that may be in a shadow root using a complex selector of n-depth
+*
+* Don't have to specify all shadow roots to button, tree is travered to find the correct element
+*
+* Example querySelectorAllDeep('downloads-item:nth-child(4) #remove');
+*
+* Example should work on chrome://downloads outputting the remove button inside of a download card component
+*
+* Example find first active download link element querySelectorDeep('#downloads-list .is-active a[href^="https://"]');
+*
+* Another example querySelectorAllDeep('#downloads-list div#title-area + a');
+e.g.
+*/
+export function querySelectorAllDeep(
+  selector,
+  root = document,
+  allElements = null
+) {
+  return _querySelectorDeep(selector, true, root, allElements);
+}
+
+export function querySelectorDeep(
+  selector,
+  root = document,
+  allElements = null
+) {
+  return _querySelectorDeep(selector, false, root, allElements);
+}
+
+function _querySelectorDeep(selector, findMany, root, allElements = null) {
+  selector = normalizeSelector(selector);
+  const lightElement = root.querySelector(selector);
+
+  if (document.head.createShadowRoot || document.head.attachShadow) {
+    // no need to do any special if selector matches something specific in light-dom
+    if (!findMany && lightElement) {
+      return lightElement;
+    }
+
+    // split on commas because those are a logical divide in the operation
+    const selectionsToMake = splitByCharacterUnlessQuoted(selector, ',');
+
+    return selectionsToMake.reduce(
+      (acc, minimalSelector) => {
+        // if not finding many just reduce the first match
+        if (!findMany && acc) {
+          return acc;
+        }
+        // do best to support complex selectors and split the query
+        const splitSelector = splitByCharacterUnlessQuoted(
+          minimalSelector
+            // remove white space at start of selector
+            .replace(/^\s+/g, '')
+            .replace(/\s*([>+~]+)\s*/g, '$1'),
+          ' '
+        )
+          // filter out entry white selectors
+          .filter((entry) => !!entry)
+          // convert "a > b" to ["a", "b"]
+          .map((entry) => splitByCharacterUnlessQuoted(entry, '>'));
+
+        const possibleElementsIndex = splitSelector.length - 1;
+        const lastSplitPart =
+          splitSelector[possibleElementsIndex][
+            splitSelector[possibleElementsIndex].length - 1
+          ];
+        const possibleElements = collectAllElementsDeep(
+          lastSplitPart,
+          root,
+          allElements
+        );
+        const findElements = findMatchingElement(
+          splitSelector,
+          possibleElementsIndex,
+          root
+        );
+        if (findMany) {
+          acc = acc.concat(possibleElements.filter(findElements));
+          return acc;
+        }
+        acc = possibleElements.find(findElements);
+        return acc || null;
+      },
+      findMany ? [] : null
+    );
+  }
+  if (!findMany) {
+    return lightElement;
+  }
+  return root.querySelectorAll(selector);
+}
+
+function findMatchingElement(splitSelector, possibleElementsIndex, root) {
+  return (element) => {
+    let position = possibleElementsIndex;
+    let parent = element;
+    let foundElement = false;
+    while (parent && !isDocumentNode(parent)) {
+      let foundMatch = true;
+      if (splitSelector[position].length === 1) {
+        foundMatch = parent.matches(splitSelector[position]);
+      } else {
+        // selector is in the format "a > b"
+        // make sure a few parents match in order
+        const reversedParts = [].concat(splitSelector[position]).reverse();
+        let newParent = parent;
+        for (const part of reversedParts) {
+          if (!newParent || !newParent.matches(part)) {
+            foundMatch = false;
+            break;
+          }
+          newParent = findParentOrHost(newParent, root);
+        }
+      }
+
+      if (foundMatch && position === 0) {
+        foundElement = true;
+        break;
+      }
+      if (foundMatch) {
+        position--;
+      }
+      parent = findParentOrHost(parent, root);
+    }
+    return foundElement;
+  };
+}
+
+function splitByCharacterUnlessQuoted(selector, character) {
+  return selector.match(/\\?.|^$/g).reduce(
+    (p, c) => {
+      if (c === '"' && !p.sQuote) {
+        p.quote ^= 1;
+        p.a[p.a.length - 1] += c;
+      } else if (c === "'" && !p.quote) {
+        p.sQuote ^= 1;
+        p.a[p.a.length - 1] += c;
+      } else if (!p.quote && !p.sQuote && c === character) {
+        p.a.push('');
+      } else {
+        p.a[p.a.length - 1] += c;
+      }
+      return p;
+    },
+    { a: [''] }
+  ).a;
+}
+
+/**
+ * Checks if the node is a document node or not.
+ * @param {Node} node
+ * @returns {node is Document | DocumentFragment}
+ */
+function isDocumentNode(node) {
+  return (
+    node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
+    node.nodeType === Node.DOCUMENT_NODE
+  );
+}
+
+function findParentOrHost(element, root) {
+  const { parentNode } = element;
+  return parentNode && parentNode.host && parentNode.nodeType === 11
+    ? parentNode.host
+    : parentNode === root
+    ? null
+    : parentNode;
+}
+
+const getShadowRoot = (element) => {
+  if (element === document) return element;
+
+  return BROWSER_TYPE === 'firefox'
+    ? element.openOrClosedShadowRoot
+    : chrome.dom.openOrClosedShadowRoot(element);
+}
+
+/**
+ * Finds all elements on the page, inclusive of those within shadow roots.
+ * @param {string=} selector Simple selector to filter the elements by. e.g. 'a', 'div.main'
+ * @return {!Array<string>} List of anchor hrefs.
+ * @author ebidel@ (Eric Bidelman)
+ * License Apache-2.0
+ */
+export function collectAllElementsDeep(
+  selector = null,
+  root,
+  cachedElements = null
+) {
+  let allElements = [];
+
+  if (cachedElements) {
+    allElements = cachedElements;
+  } else {
+    const findAllElements = function (nodes) {
+      for (let i = 0; i < nodes.length; i++) {
+        const el = nodes[i];
+        allElements.push(el);
+
+        const shadowRoot = getShadowRoot(el);
+        // If the element has a shadow root, dig deeper.
+        if (shadowRoot) {
+          findAllElements(shadowRoot.querySelectorAll('*'));
+        }
+      }
+    };
+
+    const rootShadowRoot = getShadowRoot(root);
+    if (rootShadowRoot) {
+      findAllElements(rootShadowRoot.querySelectorAll('*'));
+    }
+    findAllElements(root.querySelectorAll('*'));
+  }
+
+  return selector
+    ? allElements.filter((el) => el.matches(selector))
+    : allElements;
+}

+ 166 - 0
src/lib/query-selector-shadow-dom/normalize.js

@@ -0,0 +1,166 @@
+/* istanbul ignore file */
+/* eslint-disable */
+
+// normalize-selector-rev-02.js
+/*
+  author: kyle simpson (@getify)
+  original source: https://gist.github.com/getify/9679380
+
+  modified for tests by david kaye (@dfkaye)
+  21 march 2014
+
+  rev-02 incorporate kyle's changes 3/2/42014
+*/
+
+export function normalizeSelector(sel) {
+  // save unmatched text, if any
+  function saveUnmatched() {
+    if (unmatched) {
+      // whitespace needed after combinator?
+      if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) {
+        tokens.push(' ');
+      }
+
+      // save unmatched text
+      tokens.push(unmatched);
+    }
+  }
+
+  var tokens = [];
+  let match;
+  let unmatched;
+  let regex;
+  const state = [0];
+  let next_match_idx = 0;
+  let prev_match_idx;
+  const not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/;
+  const whitespace_pattern = /^\s+$/;
+  const state_patterns = [
+    /\s+|\/\*|["'>~+[(]/g, // general
+    /\s+|\/\*|["'[\]()]/g, // [..] set
+    /\s+|\/\*|["'[\]()]/g, // (..) set
+    null, // string literal (placeholder)
+    /\*\//g, // comment
+  ];
+  sel = sel.trim();
+
+  // eslint-disable-next-line no-constant-condition
+  while (true) {
+    unmatched = '';
+
+    regex = state_patterns[state[state.length - 1]];
+
+    regex.lastIndex = next_match_idx;
+    match = regex.exec(sel);
+
+    // matched text to process?
+    if (match) {
+      prev_match_idx = next_match_idx;
+      next_match_idx = regex.lastIndex;
+
+      // collect the previous string chunk not matched before this token
+      if (prev_match_idx < next_match_idx - match[0].length) {
+        unmatched = sel.substring(
+          prev_match_idx,
+          next_match_idx - match[0].length
+        );
+      }
+
+      // general, [ ] pair, ( ) pair?
+      if (state[state.length - 1] < 3) {
+        saveUnmatched();
+
+        // starting a [ ] pair?
+        if (match[0] === '[') {
+          state.push(1);
+        }
+        // starting a ( ) pair?
+        else if (match[0] === '(') {
+          state.push(2);
+        }
+        // starting a string literal?
+        else if (/^["']$/.test(match[0])) {
+          state.push(3);
+          state_patterns[3] = new RegExp(match[0], 'g');
+        }
+        // starting a comment?
+        else if (match[0] === '/*') {
+          state.push(4);
+        }
+        // ending a [ ] or ( ) pair?
+        else if (/^[\])]$/.test(match[0]) && state.length > 0) {
+          state.pop();
+        }
+        // handling whitespace or a combinator?
+        else if (/^(?:\s+|[~+>])$/.test(match[0])) {
+          // need to insert whitespace before?
+          if (
+            tokens.length > 0 &&
+            !whitespace_pattern.test(tokens[tokens.length - 1]) &&
+            state[state.length - 1] === 0
+          ) {
+            // add normalized whitespace
+            tokens.push(' ');
+          }
+
+          // case-insensitive attribute selector CSS L4
+          if (
+            state[state.length - 1] === 1 &&
+            tokens.length === 5 &&
+            tokens[2].charAt(tokens[2].length - 1) === '='
+          ) {
+            tokens[4] = ` ${tokens[4]}`;
+          }
+
+          // whitespace token we can skip?
+          if (whitespace_pattern.test(match[0])) {
+            continue;
+          }
+        }
+
+        // save matched text
+        tokens.push(match[0]);
+      }
+      // otherwise, string literal or comment
+      else {
+        // save unmatched text
+        tokens[tokens.length - 1] += unmatched;
+
+        // unescaped terminator to string literal or comment?
+        if (not_escaped_pattern.test(tokens[tokens.length - 1])) {
+          // comment terminator?
+          if (state[state.length - 1] === 4) {
+            // ok to drop comment?
+            if (
+              tokens.length < 2 ||
+              whitespace_pattern.test(tokens[tokens.length - 2])
+            ) {
+              tokens.pop();
+            }
+            // otherwise, turn comment into whitespace
+            else {
+              tokens[tokens.length - 1] = ' ';
+            }
+
+            // handled already
+            match[0] = '';
+          }
+
+          state.pop();
+        }
+
+        // append matched text to existing token
+        tokens[tokens.length - 1] += match[0];
+      }
+    }
+    // otherwise, end of processing (no more matches)
+    else {
+      unmatched = sel.substr(next_match_idx);
+      saveUnmatched();
+
+      break;
+    }
+  }
+
+  return tokens.join('').trim();
+}

+ 1 - 1
src/utils/FindElement.js

@@ -2,7 +2,7 @@ import Sizzle from 'sizzle';
 import {
   querySelectorAllDeep,
   querySelectorDeep,
-} from 'query-selector-shadow-dom';
+} from '@/lib/query-selector-shadow-dom';
 
 const specialSelectors = [':contains', ':header', ':parent'];
 const specialSelectorsRegex = new RegExp(specialSelectors.join('|'));

+ 0 - 5
yarn.lock

@@ -5773,11 +5773,6 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
-query-selector-shadow-dom@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349"
-  integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==
-
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"