Search code examples
javascriptcssshadow-domuserscripts

Userscript to style Shadow DOM elements of arbitrary website


This is my userscript to change the color of visited links. It is assumed to be able to work with any website.

// ==UserScript==
// @name        Change the color of visited links
// @description -
// @match       *://*/*
// ==/UserScript==

function style(css) {
  let head, style;
  head = document.getElementsByTagName('head')[0];
  if (!head) return;
  style = document.createElement('style');
  style.type = 'text/css';
  style.innerHTML = css.replace(/;/g, '!important;');
  head.appendChild(style);
}

function links(anylink, link, visited, hover, active) {
  style(`
    ${anylink} { outline: solid 1px; }
    ${link}    { color: blue;    }
    ${visited} { color: fuchsia; }
    ${hover}   { color: red;     }
    ${active}  { color: yellow;  }
  `);
}

links('a:any-link , a:any-link *',
      'a:link     , a:link     *',
      'a:visited  , a:visited  *',
      'a:hover    , a:hover    *',
      'a:active   , a:active   *'
     );

The problem is that on some websites some links belong to shadow DOM elements, and by this reason they are not affected by the script. How this can be fixed?

temporary screenshot, see comments

enter image description here


Solution

  • You can access the shadow DOM with document.querySelector(selector).shadowRoot

    From there all you need to find the first head using querySelectorAll or default to the element itself, and inject the CSS there.

    // setup example
    const host = document.querySelector("#host");
    const shadow = host.attachShadow({
      mode: "open"
    });
    const span = document.createElement("span");
    span.innerHTML = "I'm in the shadow DOM <a href='#'>link</a>";
    shadow.appendChild(span);
    
    
    function addToHeadOrSelf(elem, css) {
      var head
    
      if (elem.getElementsByTagName) {
        head = elem.getElementsByTagName('head')[0];
      }
    
      if (elem.querySelectorAll) {
        head = elem.querySelectorAll('head')[0]
      }
    
      if (!head) {
        head = elem
      }
    
      style = document.createElement('style');
      style.type = 'text/css';
      style.innerHTML = css.replace(/;/g, '!important;');
      head.appendChild(style);
    }
    
    function style(css) {
      addToHeadOrSelf(document, css)
    
      var elementsWithShadowRoot = findElementsWithShadowRoot()
      elementsWithShadowRoot.forEach(function(elem) {
        addToHeadOrSelf(elem.shadowRoot, css)
      })
    
    }
    
    function findElementsWithShadowRoot() {
      function hasShadowRoot(element) {
        return element.shadowRoot !== null;
      }
    
      function traverseTree(node) {
        if (hasShadowRoot(node)) {
          elementsWithShadowRoot.push(node);
        }
    
        const children = node.children || [];
        for (let i = 0; i < children.length; i++) {
          traverseTree(children[i]);
        }
      }
    
      const elementsWithShadowRoot = [];
      traverseTree(document.body);
      return elementsWithShadowRoot;
    }
    
    
    
    function links(anylink, link, visited, hover, active) {
      style(`
        ${anylink} { outline: solid 1px; }
        ${link}    { color: blue;    }
        ${visited} { color: fuchsia; }
        ${hover}   { color: red;     }
        ${active}  { color: yellow;  }
      `);
    }
    
    links('a:any-link , a:any-link *',
      'a:link     , a:link     *',
      'a:visited  , a:visited  *',
      'a:hover    , a:hover    *',
      'a:active   , a:active   *'
    );
    <div id="host"></div>
    
    <span>I'm not in the shadow DOM <a href='#'>link</a></span>
    <br />