[ Links open in: same window | new window ]

Radical Software

LCST 2234, Fall 2024 (CRN 9430)
Rory Solomon

Project 1, Tutorial 4: Some additional techniques

Some additional code snippets and techniques:

  1. Options page formatting
  2. Modifying HTML, can be used to create "popup" effects

01. Options page formatting

Several people were planning to add more to their options page by adding additional input fields. We didn't talk about how to format those nicely so I wanted to give an example of how to do that.

At the very least, if you want your various <input> elements to appear each on their own line, you could enclose each inside a <p> tag. So building on where we left off with options.html, you might add those tags with the file looking like this:

Modifying options.html:

<body>
  <div>
    <p>
      From text: <input id="fromField" type="text" />
    </p>
    <p>
      To text: <input id="toField" type="text" />
    </p>
    <button type="button" id="button">Set text</button>
    <button type="button" id="clear">Clear text</button>
  </div>
</body>

Next, if you want to have more elements on the page and you want to control their placement and spacing, you could include groupings of HTML elements in <div> elements. Like this:

Further modifying options.html:

<body>
  <div>
    <div>
      <p>
        From text 1: <input id="fromField1" type="text" />
      </p>
      <p>
        To text 1: <input id="toField1" type="text" />
      </p>
    </div>

    <div>
      <p>
        From text 2: <input id="fromField2" type="text" />
      </p>
      <p>
        To text 2: <input id="toField2" type="text" />
      </p>
    </div>

    <button type="button" id="button">Set text</button>
    <button type="button" id="clear">Clear text</button>
  </div>
</body>

Note that I have changed the id attributes for my HTML <input> elements here. If you do this, you'll need to go through your extenions (your options.js file, and your content scripts) and change any references to these fields. So fromField should become fromField1, etc. You should then push this through so that all those fields end with ...1, and if you want to access the other fields, they'll all end with ...2 etc. After making the JS changes that correspond to the above HTML changes, options.js would look like this:

Modifying options.js:

let fromTextElement1 = document.getElementById('fromField1');
let toTextElement1 = document.getElementById('toField1');
let fromTextElement2 = document.getElementById('fromField2');
let toTextElement2 = document.getElementById('toField2');
let setButtonElement = document.getElementById('button');
let clearButton = document.getElementById('clear');

setButtonElement.addEventListener('click', function() {
  chrome.storage.sync.set({
    fromStored1: fromTextElement1.value,
    toStored1: toTextElement1.value,
    fromStored2: fromTextElement2.value,
    toStored2: toTextElement2.value
  });
});

chrome.storage.sync.get({fromStored1: "", toStored1: "", fromStored2: "", toStored2: ""}, function(result) {
    fromTextElement1.value = result.fromStored1;
    toTextElement1.value = result.toStored1;
    fromTextElement2.value = result.fromStored2;
    toTextElement2.value = result.toStored2;
});

clearButton.addEventListener('click', function() {
  chrome.storage.sync.clear();
  fromTextElement1.value = "";
  toTextElement1.value = "";
  fromTextElement2.value = "";
  toTextElement2.value = "";
});

UPDATE. In class we talked about how adding an additional search/replace field like that will not have any effect on the behavior of your extension unless you also make corresponding modifications to the content script JavaScript file.

Going in the content script that we were working on last week, you could modify it to use these new form fields like this:

Modifying content-tutorial3.js:

chrome.storage.sync.get({fromStored1: "", toStored1: "",fromStored2: "", toStored2: ""}, function(result) {

    var html = document.querySelector('html');
    var walker = document.createTreeWalker(html, NodeFilter.SHOW_TEXT);
    var node;
    while (node = walker.nextNode()) {

        var re = new RegExp(result.fromStored1,"gi")
        node.nodeValue = node.nodeValue.replace(re, result.toStored1);

        var re = new RegExp(result.fromStored2,"gi")
        node.nodeValue = node.nodeValue.replace(re, result.toStored2);``
    }
});

Now, if you want to style those <div>s in some way, you can add some CSS to control their appearance. See my comments in Tutorial 2, part 02.a for three different ways of attaching CSS rules to an HTML page. If there is a lot of styling that you want to do to your options page, you should probably use the external method of including CSS, as explained in the class notes linked above. Refresher: you put all your CSS rules in a separate CSS file, then link to that CSS file from your HTML file. So, go into your options.html file and add this:

Modifying options.html:

<html>
<head>
  <script src="options.js" defer></script>
  <link rel="stylesheet" href="css/options.css">
</head>
<body>

Then, create a new file in your css subfolder called options.css and start to add CSS rules to it. Here is some sample CSS that starts to work on controlling the appearance of this options.html code:

Creating css/options.css:

div {
  width: 250px;
  border: solid 1px #000;
  padding: 5px;
  margin: 5px;
}

If you do all that, and click in to your extenion's options page, you should see something that looks like this:

If you want to format the different <div>s differently, you can add CSS classes, like this:

Modifying options.html:

  <body>
    <div class="outter">
      <div>

Then modify your CSS file like this, adding a new rule to target the specific <div> indicated by that class:

Modifying css/options.css:

div {
  width: 250px;
  border: solid 1px #000;
  padding: 5px;
  margin: 5px;
}

div.outter {
  width: 275px;
  margin: 25px;
  padding: 10px;
}

That should space things out a little better.

Then maybe you'd like to add some additional HTML and text to offer some context, explanation, or instructions to your user:

Modifying options.html:

    <div class="outter">
      <h1>Rory's Browser Extension Options</h1>
      <p>
        Specify some options here ...
      </p>
      <div>

If you do all that, you should be able to reload your extension and see something that looks like this:

Hopefully that's enough to get you started exploring further.

Remember, the Mozilla Developer Network has some great tutorials about other CSS properties you can explore. And you can also use the Chome browser's inspect tool to experiment with different CSS properties and their values.

(jump back up to table of contents)

02. Modifying HTML & "popup" effects

Another technique people have asked about is how to create "pop-up" like effects. You do this by using Javascript code to add or modify HTML within the current page.

You can create a "pop-up" effect specifically by adding a <div> to the page, specify its position, dimensions, and appearance, and then when the user clicks somewhere, delete it.

Let's start with the technique that we looked at in Tutorial 2, part 05, modified slightly. To start, let's create a new JS file called content-tutorial4.js. Don't forget to modify manifest.json to point to this new file:

  "content_scripts": [
    {
      "matches": [
        "*://*/*"
      ],
      "js": [
        "content-tutorial4.js"
      ],
      "run_at": "document_idle"
    }
  ],
Then add the following code to this new file:

Creating content-tutorial4.js:

var html = document.querySelector('html');
var walker = document.createTreeWalker(html, NodeFilter.SHOW_TEXT);
var node;

while (node = walker.nextNode()) {

  if ( node.nodeValue.includes("paragraph") ) {
    newNodeValue = node.nodeValue.replace(/paragraph/gi,"<span class='click-me'>paragraph</span>");
    newElement = document.createElement("template");
    newElement.innerHTML = newNodeValue;
    node.before(newElement.content);
    oldNode = node;
    node = walker.nextNode();
    oldNode.remove();
  }
}

This is the same as the code snippet from Tutorial 2, part 05 except that I've made a change to the <span>. Instead of specifying an inline style attribute with CSS rules, it assigns a CSS class to that span.

The point of using a CSS class is that it makes it easier to use Javascript code to target all HTML elements that are marked with that class. In other words, using CSS classes, we can write Javascript code that is triggered by some user interaction event on any element with that CSS class.

So this finds all instances of the word "paragraph" on the page and puts that word in a <span> element with the CSS class click-me. (The word click-me here is not a special keyword in any sense. I could have used any word I'd like.)

This <span> does not have any CSS style rules associated with it, so it will not appear as visible to the user in any way. Let's add some style rules for this to signal to the user that it is clickable. To add CSS from within a content script, we have to modify manifest.json like so:

Modifying manifest.json:

 "content_scripts": [
    {
      "matches": [
        "*://*/*"
      ],
      "js": [
        "content-tutorial4.js"
      ],
      "css": [
        "content-tutorial4.css"
      ],
      "run_at": "document_idle"
    }
  ],
    

Now, create this file, content-tutorial4.css, and add something like the following CSS rule to it:

Creating content-tutorial4.css:

.click-me {
  border-bottom: dashed 1px blue;
}

Now, to actually make these elements clickable, I'm going to use document.getElementsByClassName() to select a list of all HTML elements with the click-me class, and then use addEventListener() to add code that responds to a click on each of those words. First let's add some simple code just to test that the click event handling is working:

Adding code to the end of content-tutorial4.js:

var clickableWords = document.getElementsByClassName('click-me');
for (var word of clickableWords) {
  word.addEventListener('click', function() {
    alert("An element was clicked!");
  });
}

Reload the extension, reload the page, and try clicking on one of the underlined elements.

Once you have that working, let's delete that alert() code, and add code that creates a popup for the user:

Modifying content-tutorial4.js:

var clickableWords = document.getElementsByClassName('click-me');
for (var word of clickableWords) {
  word.addEventListener('click', function() {
    var popup = document.createElement("div");
    popup.style.width = "500px";
    popup.style.height = "250px";
    popup.style.border = "solid 1px #000";
    popup.style.margin = "10px";
    popup.style.padding = "10px";
    popup.style.position = "fixed";
    popup.style.top = "50px";
    popup.style.left = "50px";
    popup.style.backgroundColor = "#fff";
    popup.style.zIndex = "9999";
    popup.textContent = "Hello this is some sample text.";
    document.body.insertAdjacentElement("afterbegin",popup);
  });
}

This new code creates a new HTML element, which is a <div>, and which is represented in JavaScript with the popup variable. The other lines are setting CSS properties on popup. You can modify these to control the size, position, and color of the <div> as well as any other CSS properties. The line that begins popup.textContent sets the text that will be the content of the pop-up. Finally, the last new line inserts this new HTML element (represented by popup) into the document body.

Reload the extesion, relead the page, and click an element.

But wait! Now we can't close that pop up. Let's add some code so that when we create the popup, we are also creating a new event listener that listens for a click, and when it receives one, removes this new element:

Modifying content-tutorial4.js:

var clickableWords = document.getElementsByClassName('click-me');
for (var word of clickableWords) {
  word.addEventListener('click', function() {
    var popup = document.createElement("div");
    popup.style.width = "500px";
    popup.style.height = "250px";
    popup.style.border = "solid 1px #000";
    popup.style.margin = "10px";
    popup.style.padding = "10px";
    popup.style.position = "fixed";
    popup.style.top = "50px";
    popup.style.left = "50px";
    popup.style.backgroundColor = "#fff";
    popup.style.zIndex = "9999";
    popup.textContent = "Hello this is some sample text. (Click anywhere to close.)";
    document.body.insertAdjacentElement("afterbegin",popup);

    popup.addEventListener('click', function() {
      popup.remove();
    });
  });
}