cssiosiphonesafari-content-blocker

Select CSS Parent `selector` in iOS Content Blocking


I am setting up some content blockers (https://developer.apple.com/documentation/safariservices/creating_a_content_blocker)

The HTML I am testing on looks something like this:

<div class="<random>">
  <div>
    <div>
      <div>
        <a class="bad" />

Now, I am wondering if I can have a CSS selector that selects on .bad but then removes the entire .random div block.

I have tried things like:

  {
    "action": {
      "type": "css-display-none",
      "selector": "div > div > div > div >a[href*='speedtest.net']"
    },
    "trigger": {
      "url-filter": "^https?:\/+([^\/:]+\\.)?google.*[:\/]"
    }
  }

and div:has(a).

Nothing seems to work.

Anyone know if its possible? I can't even find anywhere that says what apple supports for this (CSS4?)


Solution

  • Update:: has is now supported in Safari and Safari iOS. source: caniuse

    There is currently no parent selector in css even though it is one of the most requested CSS feature.

    The simple reason for the lack of implementation yet is performance issues. https://snook.ca/archives/html_and_css/css-parent-selectors

    The closest thing we have to a parent selector in CSS for now is :focus-within which will match an element if the element or any of its descendants is focused. However it is of no use in your case.

    The proposed implementation of a parent selector as you mentioned is :has and is part of the level 4 of CSS selectors.

    https://www.w3.org/TR/selectors-4/

    Although it isn't implemented yet in any browser, it is now part of the technical preview for Safari so we might get it some day. However it is not yet part of the technical preview for iOS and that might still take a lot of time.

    https://caniuse.com/css-has

    Even though there is no CSS parent selector, what you are describing can be easily achieved through Javascript in your Safari extension.

    However you will need to give permissions to your Safari extension to inject scripts in the web page.

    The users will have to accept the permissions. If they refuse you won't be able to access the page DOM through javascript.

    You can do that by going in the manifest.json of your Xcode project : AppName => Shared (Extension) => Ressources => manifest

    In content_scripts you will need to add

    "content_scripts": [{
        "js": [ "content.js" ],
        "matches": [ "<all_urls>" ]
    }]
    

    You can also select the sites you want your extension to have access to with regular expressions like you did previously.

    https://developer.apple.com/documentation/safariservices/safari_web_extensions/managing_safari_web_extension_permissions

    Then in the content.js file you can add your javascript code to edit the web pages as you please.

    Here is a function you could use to remove the parent of the bad elements:

    const removeBadElementParent = () =>{
        const badElements = document.querySelectorAll('.bad-class-name')
        if(badElements.length === 0) return;
        
        badElements.forEach(element =>{
            if(!element instanceof HTMLElement) return;
            const parent = element.parentElement;
            
            if(!parent instanceof HTMLElement) return;
            parent.style.display = 'none'
        })
    }
    

    You don't want to use it immediately but only when the DOM has loaded so you will need to call it like this:

    if( document.readyState !== 'loading' ) {
        removeBadElementParent();
    } else {
        document.addEventListener("DOMContentLoaded", function(event) {
            removeBadElementParent();
        });
    }
    

    However most website make changes to the DOM and might add the bad elements after the DOMContentLoaded event has fired. So you will need to add a mutation observer to check when the DOM is changed:

    const onDOMMutation = (callback) =>{
        MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
        const observer = new MutationObserver(function(mutations, observer) {
            callback();
        });
    
        observer.observe(document, {
          subtree: true,
          attributes: true
        });
    }
    

    And you can call it like this instead of calling directly the removeBadElementParent function:

    if( document.readyState !== 'loading' ) {
        onDOMMutation(removeBadElementParent);
    } else {
        document.addEventListener("DOMContentLoaded", function(event) {
            onDOMMutation(removeBadElementParent);
        });
    }
    

    https://developer.apple.com/documentation/safariservices/safari_app_extensions/injecting_a_script_into_a_webpage