0

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.

Ngelus
  • 35
  • 5
  • You can use MutationObserver in the content script that runs at `document_start`, [example](https://stackoverflow.com/a/32537455). Note that `matches` covers the entire site e.g. `site/*` without path. Also, no need for the background script. – wOxxOm Jul 12 '21 at 12:35
  • I've already played around with mutationObservers aswell, but never got it to work the way I described above @wOxxOm – Ngelus Jul 12 '21 at 12:42
  • yeah. I took a look at the example and tried modifying it to fit my purpose. This only seems to work when I refresh the page aswell though. Whenever I navigate to another page of the single page app and then navigate back to /contacts this doesn't trigger. – Ngelus Jul 12 '21 at 13:04
  • @wOxxOm having the following in the content script didn't work: https://pastebin.com/9uAgVdYs – Ngelus Jul 12 '21 at 13:06

1 Answers1

7

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;
  });
}
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • This solution works flawlessly. Thank you so much for having the patience to help me @wOxxOm. I'm not really all that good at javascript yet as you might have noticed. – Ngelus Jul 12 '21 at 13:27