Ngelus
Ngelus

Reputation: 35

How do I write a chrome extension that applies a content script to a Single Page Application?

I'm currently in the process of writing a extension that'll turn every string that matches a RegEx pattern, specifically the RegEx pattern for german telephone numbers, into clickable links by prepending tel: in the href of the element and surrounding it with an anchor tag if needed.

For the last three days I've been trying to make it work for this one Single Page App our company uses for their contacts. But whenever I load my extension into the browser and then restart the browser it doesn't apply my content script at first. I first need to refresh the page after visiting it.

I'm using the following files:

manifest.json:

{
  "name": "testExtension",
  "short_name": "testExtension",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "Replace telephone numbers with clickable links.",
  "author": "Ngelus",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "browser_action": {
    "default_icon": "icons/icon48.png",
    "default_title": "testExtension"
  },
  "default_locale": "en",
  "permissions": [
    "tabs",
    "activeTab",
    "<all_urls>",
    "*://*.examplecontactsapp.de/*",
    "storage",
    "webRequest",
    "webNavigation"
  ],
  "content_scripts": [
    {
      "matches": [
        "*://web.examplecontactsapp.de/contacts",
        "*://web.examplecontactsapp.de/contacts*"
      ],
      "js": ["src/inject/contentscript.js"]
    }
  ],
  "background": {
    "scripts": ["src/bg/background.js"],
    "persistent": true
  }
}

background.js:

let currentUrl = '';
let tabId;

chrome.webRequest.onCompleted.addListener(
  function (details) {
    const parsedUrl = new URL(details.url);

    if (currentUrl && currentUrl.indexOf(parsedUrl.pathname) > -1 && tabId) {
      chrome.tabs.sendMessage(tabId, { type: "pageRendered" });
    }
  },
  { urls: ['*://web.examplecontactsapp.de/contacts*'] }
);

chrome.webNavigation.onHistoryStateUpdated.addListener(
  (details) => {
    tabId = details.tabId;
    currentUrl = details.url;
  },
  {
    url: [
      {
        hostSuffix: 'examplecontactsapp.de',
      },
    ],
  }
);

contentscript.js:

var readyStateCheckInterval = setInterval(function () {
  if (!location.href.startsWith('https://web.examplecontactsapp.de/contacts')) return;
  if (document.readyState === 'complete') {
    clearInterval(readyStateCheckInterval);
    console.log(
      `[---testExtension---]: replacing all telephone numbers with clickable links...`
    );
    replaceNumbersWithLinks();
  }
}, 10);

chrome.runtime.onMessage.addListener(function (request) {
  if (request && request.type === 'pageRendered') {
    const readyStateCheckInterval = setInterval(function () {
      if (document.readyState === 'complete') {
        clearInterval(readyStateCheckInterval);
        console.log(
          `[---testExtension---]: replacing all telephone numbers with clickable links...`
        );
        replaceNumbersWithLinks();
      }
    }, 10);
  }
});

function replaceNumbersWithLinks() {
  document
    .querySelectorAll(
      'body > main > div.page-content > div > table > tbody > tr > td > p > a'
    )
    .forEach((a) => {
      var b = a.innerText.replaceAll(' ', '').trim();
      a.removeAttribute('data-phonenumber');
      if (b != '') a.href = 'tel:' + b;
    });
}

What's the proper way to do this? How do I make it work whenever I visit that single page app?

Thank you all in advance.

Upvotes: 0

Views: 2091

Answers (1)

woxxom
woxxom

Reputation: 73686

Make matches match the entire site in manifest.json:

{
  "content_scripts": [
    {
      "matches": [
        "*://web.examplecontactsapp.de/*"
      ],
      "js": ["src/inject/contentscript.js"]
    }
  ],

Use MutationObserver and check for the selector on each mutation in contentscript.js using querySelector, which is moderately fast, then proceed with the slow querySelectorAll on success:

const SEL = 'a[data-phonenumber]';
const mo = new MutationObserver(onMutation);
observe();

function onMutation() {
  if (document.querySelector(SEL)) {
    mo.disconnect();
    replaceNumbersWithLinks();
    observe();
  }
}

function observe() {
  mo.observe(document, {
    subtree: true,
    childList: true,
  });
}

function replaceNumbersWithLinks() {
  document.querySelectorAll(SEL).forEach((a) => {
    a.removeAttribute('data-phonenumber');
    const b = a.textContent.replaceAll(' ', '').trim();
    if (b) a.href = 'tel:' + b;
  });
}

Upvotes: 8

Related Questions