TheBlindDeveloper TheBlindDeveloper - 22 days ago 6
HTML Question

Manipulating a webpage's DOM in a Google Chrome extension using a background script

I'm working on a Google Chrome extension that takes all the paragraphs (p tag content) and places each words in them up into a button. It's a part of a larger program I'm working on. I have a working copy of that part of the app on JSFiddle.

Now, I'm trying to port that code into a Chrome Extension. But, I'm having trouble accessing the DOM from my background script, so that I can manipulate it with my code (in my function

FormatText()
). I haven't even called the function yet, because I can't figure out how I'm supposed to edit the DOM in the first place within background.js.

Here is my code:

manifest.json

{
"manifest_version": 2,
"name": "Test Extension",
"version": "1",
"background": {
"persistent": false,
"scripts": ["background.js","jquery-3.0.0.min.js","TextSplitter.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}],
"browser_action": {
"default_title": "Test Extension"
},
"permissions": ["activeTab","tabs"]
}


content.js

// Listen for messages
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
// If the received message has the expected format...
if (msg.text === 'report_back') {
// Call the specified callback, passing
// the web-page's DOM content as argument
sendResponse(document);
}
});


background.js

// A function to use as callback
function doStuffWithDom(domContent) {
console.log('I received the following DOM content:\n');
console.log(JSON.stringify(domContent));
var domAccess = $(domContent);
var myText = $(domAccess).find("p").text();
console.log("THIS IS MY TEXT: " + myText);
}

chrome.tabs.onUpdated.addListener(function (tabID, info, tab) {
console.log("Status: " + info.status);
if (info.status == "complete") {
chrome.tabs.sendMessage(tab.id, { text: 'report_back' }, doStuffWithDom);
}
});


TextSplitter.js

function FormatText(domObject) {
var pElements = []; // Holds the split paragraphs for each p tag
var pElementIndex = 0;

//Loop through each p tag in web page
$("p").each(function (webPElementIndex, webPElement) {
var jQObjWebPElement = $(webPElement);// Convert p element to jQuery Obj
// split current paragraph element text into an array of seperate words
pElements[webPElementIndex] = jQObjWebPElement.text().split(" ");
});

//Clear text out of all p elements
$("p").empty();

//Loop through p elements in the webpage to add back text with spans around each word
$("p").each(function (webPElementIndex, webPElement) {
// convert current web page element to jQuery Obj
var jQObjWebPElement = $(webPElement);
// Loop through each word stored in each stored paragraph element
$.each(pElements[pElementIndex], function (pElementWordIndex, pElementWord) {
var jQObjPElementWord = $(pElementWord); // convert element to jQuery object
jQObjWebPElement.append($("<button>")
.text(pElements[pElementIndex][pElementWordIndex]));
});
pElementIndex = pElementIndex + 1;
});
}


Please pardon my ignorance as I am very new to working with the DOM in general, especially in a Chrome extension.

Answer

Your code appears overly complex in a few areas. In particular, the DOM can only be manipulated from a content script. As wOxxOm mentioned in a comment, it would be a good idea for you to read the Chrome extension architecture overview. It has overall architecture information which should help your understanding of how things are generally done/organized.

The following complete extension (tested on Chrome and Firefox) changes all non-whitespace characters surrounded by a whitespace character (or the start or end of a line/paragraph) to be within a <button>. It does this when an actionButton is clicked in the browser user interface.

When the actionButton is clicked, the contentScript.js file is injected. The content script makes the changes and exits. For this functionality, there is no need for a content script that is sitting in the page waiting to get a message to perform a simple function. You may actually be doing more than you have described/shown with code, but for the functionality mentioned in the Question, injecting the script with tabs.executeScript() is a better, less complicated, and more efficient choice.

I chose not to use jQuery. jQuery is good for many things. In this instance, I did not like the trade-off of loading 90 KiB of code to save just a few characters instead of accomplishing the same thing with stock JavaScript.

I did not take a good look at the code you are using to perform your button-ization. I already had code in another answer which could be easily adapted to perform this task. Given that your question was about how to manipulate the DOM, not about the functionality of your button-ization code, I chose to use the code I was already familiar with.

The extension in action:

button-izing example.com

manifest.json

{
    "description": "Inject content script to make all words in <p> elements buttons",
    "manifest_version": 2,
    "name": "Button all words in <p>",
    "version": "0.1",

    "permissions": [
        "activeTab"
    ],

    "background": {
        "scripts": [
            "background.js"
        ]
    },

    "browser_action": {
        "default_icon": {
            "32": "myIcon.png"
        },
        "default_title": "Make Buttons"
    }
}

background.js:

chrome.browserAction.onClicked.addListener(function(tab) {
    //Inject the script to change the text in <p> to buttons
    chrome.tabs.executeScript(tab.id,{file: 'contentScript.js'});
});

contentScript.js:

(function(){
    function handleTextNode(textNode) {
        if(textNode.nodeName !== '#text') {
            //Don't do anything except on text nodes.
            return;
        }
        let origText = textNode.textContent;
        let newHtml=origText.replace(/(^|\s)(\S+)(?=\s|$)/mg, '$1<button>$2</button>');
        //Only change the DOM if we actually made a replacement in the text.
        //Compare the strings, as it should be faster than a second RegExp operation and
        //  lets us use the RegExp in only one place for maintainability.
        if( newHtml !== origText) {
            let newSpan = document.createElement('span');
            newSpan.innerHTML = newHtml;
            textNode.parentNode.replaceChild(newSpan,textNode);
        }
    }

    //Find all text node descendants of <p> elements:
    let allP = document.querySelectorAll('p');  // Get all <p>
    let textNodes = [];
    for (let p of allP) {
        //Create aNodeIterator to get the text nodes descendants of each <p>
        let nodeIter = document.createNodeIterator(p,NodeFilter.SHOW_TEXT);
        let currentNode;
        //Add text nodes found to list of text nodes to process below.
        while(currentNode = nodeIter.nextNode()) {
            textNodes.push(currentNode);
        }
    }
    //Process each text node
    textNodes.forEach(function(el){
        handleTextNode(el);
    });
})();

myIcon.png:

Icojam-Weathy-24-tornado.png

The code in handleTextNode to make changes to text nodes was modified from code in another answer of mine.

Comments