Important V2 Changes
Due to the policy change imposed by Google to protect users from harmful code, there were a number of change required to the extension.
- Update of the manifest to new V2 standard.
- Extension distribution from the Chrome Web Store
- Removal of inline script blocks from HTML pages.
I also took advantage of the changes to upgrade to a new jQuery version.
Introduction
If you are a regular user of CodeProject, or maybe an article moderator, mentor, or even spend your time answering questions in the Q&A section, how many times have you found yourself posting the same thing? How many times have you found yourself heading over to the FAQ list to copy the link to the Article FAQ? Once, twice, ten times? What about going to find a previous post you have made to copy that and use again to save typing it all again?
That is how this utility and article was born. I thought there must be a better way, considered keeping a text file, but discounted that idea, and thought hang on, I'll make another Chrome extension. I've done it once already, so it should be fairly easy to repeat the success of that.
The utility allows the user to create template items that are stored within the browser context, and at the click of a button, insert a selected template item straight into your forum message or answer. You can even just keep small snippets, e.g., a link and be able to quickly add that to your message/answer.
Version 1.1 fixed a couple of bugs, and also introduced a third item type and filtering capabilities within the editor and selector forms.
Topics Covered
In this article, the main items we will look at are:
- Chrome Background pages and PageActions
- Chrome Script injection and execution within Browser page context from within Extension context
- Some JQuery for handling object cloning, item identification
Chrome Extensions - What are they?
Extensions are add-ons for Google Chrome browser, and rather than repeat myself, hop over to my last Chrome Extension Article, where the basics are explained and links to the Chrome Extension and JQuery developer pages can be found. The article can be found here; Chrome Extension - CodeProject Reputation Watcher. If you have never looked at extensions before, I suggest you read this first, as it covers some key extension creation information, such as extension structure, manifest files, packaging and deployment.
NOTE: Google Chrome has changed the way in which packaged extensions can be installed when they are not served by the Chrome Web Store. This is to reduce the risk of drive installs of dodgy extensions. To use the packaged extension from my website, click the link, then switch to the Extensions Page from the Tools Menu, then drag the file from the download bar at the bottom of the browser onto the Extensions Page. You will see a Drop to Install message appear.
What About this Extension?
This extension is very similar, the same ideas around the JavaScript files, JQuery references, etc. are all the same. This extension is different from the previous one in the way it operates and triggers the events and interacts with the users page.
To start with, let's take a look at the manifest file for this extension:
{
"manifest_version": 2,
"name": "CodeProject Template Items",
"short_name": "CPTemplates",
"version": "2.0",
"content_security_policy": "script-src 'self' https://ssl.google-analytics.com 'unsafe-eval'; object-src 'self' ",
"description": "Chrome Extension to provide CodeProject template items.",
"icons": {
"48": "images/bob48.png",
"128": "images/bob128.png"
},
"background": {
"scripts": [ "js/background.js" ]
},
"page_action": {
"default_icon": "images/bob.png",
"default_title": "CodeProject: Insert Template Items",
"default_popup": "selector.html"
},
"options_page": "editor.html",
"permissions": [
"tabs",
"http://*.codeproject.com/"
]
}
The usual suspects can be seen, name, version, description, etc., but there are a couple of differences:
background
- This is the file that runs in the background of the browser, and sets up the browser integration of the extension. Under version 1 this was an html page with inline scripting, but for V2 needed to be extracted to a standalone javascript file. page_action
- This sets up an action that can be triggered by the user under the right set of circumstances, the contained properties setup the icon displayed, the tooltip text and the page to display to the user.
You can also see the 'manifest_versions'
and 'content_security_policy'
changes that were required under the new rules.
Background Script - What is it Doing?
When the browser starts, the extension background is loaded and any script within is executed. Take a look at the page content below:
if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
chrome.pageAction.show(tabId);
}
if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
chrome.pageAction.show(tabId);
}
};
chrome.tabs.onUpdated.addListener(checkForTargetMessageEditorUrl);
When the script loads, it adds a listener to the Browser. This listener waits for the Tabs onUpdated()
event and then fires off the callback function checkForTargetMessageEditorUrl()
. Chrome will pass a reference to the browser tab that has been updated to the callback. The URL of the tab is checked, and if it contains the addresses of applicable webpages, in this case the Message Editor or a Question Page it will show the page action icon defined in the manifest file within the address region of the browser. If the URL is not one being looked for, then the page action icon is not shown. The page action icon in the address bar is shown below:
Now we have a suitable URL checking background code which displays the page action to the user.
The Page Action
Now we have the page action available to the user, when the user clicks the icon, the popup defined in the manifest file is displayed to the user, this is shown below:
The user can now click on the template item she/he wants to make use of, and click Select to inject the content into the browser page. We will cover adding and editing items in this list shortly.
How Does the Content Get Injected?
One of the key things to know about Chrome and the user pages and the extension pages is that they are all kept in isolation. The only common area that both the extension and the web page have is the web page DOM, the extension cannot access the script or variables of the page, and the page cannot access the script or variables of the extension. The only common element is the web page DOM, and of course, the hosting Chrome browser.
Let's take a look at the code executed when the user has clicked on a template item and clicks Select
:
function buttonSelect_Click() {
chrome.windows.getCurrent(function (theWindow) {
chrome.tabs.getSelected(theWindow.id, function (theTab) {
injectToTab(theTab) });
});
}
The click
event effectively asks Chrome, What is the active Chrome Window? The result of this is a Window
object, this is passed into a callback function. Chrome is then asked, What is the active tab of the window you just told me about? Chrome returns a Tab
object to another callback function which is then used to inject the content.
function injectToTab(tab) {
var templateItem = parseInt($("#selectTemplates option:selected").val());
var subject = templateStore[templateItem].Subject;
var body = templateStore[templateItem].Body;
if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById('ctl00_MC_Subject').value +=
unescape('" + escape(subject) + "');"
});
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById('ctl00_MC_ContentText_MessageText').value +=
unescape('" + escape(body) + "');"
});
}
if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById
('ctl00_ctl00_MC_AMC_PostEntryObj_Content_MessageText').value +=
unescape('" + escape(body) + "');"
});
}
}
The function checks to see which item is selected in the list of templates. This then retrieves the item by index from the in memory template array. The code then checks to see which page, based on the URL of the tab, we are injecting into. We then build up a string
representation of the script we want to execute within the context of the page and add into it the relevant properties of the template item. When I was developing this, I discovered that newlines within the string
do not get handled correctly, so we have to use the JavaScript escape
and unescape
methods to pass the data correctly.
Once this string
is built, Chrome is passed the string
representation of the script to then execute within the context of the web page. This script executes and appends the template data to the elements which have been identified by id.
Adding and Storing Template Items
There are two methods of accessing the page used to add/edit template items. The first way is from the Chrome Extensions page, and then click Options the second is by right clicking on the page action icon and selecting Options.
The options page is defined within the manifest in this case editor.html and this page is loaded up. The editor entry page is basically a list of the templates and is shown below:
You can then click Add, or select an existing template to Edit/Delete/Clone that item.
The browser LocalStorage
is used to store a string
representation of the template items held in memory. JSON is used to stringify
the JavaScript objects and pop this into the local storage, and JSON is also used to convert the string
back into the JavaScript object.
When the editor page loads, the storage is initialised, checked if the object has already been defined, if not creates a new empty store. If it already exists, JSON is used to parse
the object back into the array.
The code to initialise the storage and parse
the objects back into memory is shown below:
function initStorage() {
if (localStorage["TemplateStore"] == undefined) {
localStorage["TemplateStore="] = "{}";
}
else {
templateStore = JSON.parse(localStorage["TemplateStore"]);
}
}
The code to store the array to local storage using JSON to stringify
is shown below:
function saveToLocalStorage() {
localStorage["TemplateStore"] = JSON.stringify(templateStore);
}
When the editor loads, if there are existing template items, this array is loaded into the page selection list as option
elements using JQuery. The load operation occurs in two stages, the first checks the current page (used by the filtering), the second loads the templates into the HTML, depending the current filter setting, you can also see the index number being written into the option elements.
function loadTemplates() {
chrome.windows.getCurrent(function (theWindow) {
chrome.tabs.getSelected(theWindow.id, function (theTab) {
if (theTab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
loadTemplatesPart2("Message");
}
else {
if (theTab.url.indexOf(".codeproject.com/Questions/") > -1) {
loadTemplatesPart2("Answer");
}
else {
loadTemplatesPart2("");
}
}
});
});
}
function loadTemplatesPart2(urlType) {
$("#selectTemplates").html("");
if (templateStore.length > 0) {
$("#templateLoading").html("Template store contains: " +
templateStore.length + " item(s).");
var filter = $("#selectFilter option:selected").val();
for (item in templateStore) {
var addit = false;
switch (filter) {
case "All":
addit = true;
break;
case "Auto":
if (templateStore[item].Type == "Snippet") {
addit = true;
}
else {
addit = (templateStore[item].Type == urlType);
}
break;
default:
addit = (templateStore[item].Type == filter);
}
if (addit) {
$("#selectTemplates").html($("#selectTemplates").html() +
"<option value="\"" + item.toString() + "\">" +
templateStore[item].Title + "</option>");
}
}
$("#templateLoading").html($("#templateLoading").html() +
" Displaying: " + $("#selectTemplates").prop("length").toString() + " item(s)");
$("#templateListing").show();
}
else {
$("#templateLoading").html("No items located in local store.");
$("#templateListing").show();
if (!templateSelectMode) {
$("#templateEdit").hide();
$("#buttonEdit").hide();
$("#buttonClone").hide();
$("#buttonDelete").hide();
}
else {
$("#buttonSelect").hide();
}
}
if (!templateSelectMode) {
$("#buttonAdd").show();
}
}
Adding and Edit Template Items
When the user clicks Add, the editor form is displayed where the user can enter the text for the various properties. A new template object is then created and pushed into the array and stored out to local storage.
If the user selects an existing item, this is loaded into the editor, can be updated by the user and pushed back into the array, and written out to the local storage. The edit form is just a hidden DIV
which is swapped out with the template selector DIV
. This edit form is shown below:
The code below demonstrates how the item selected in the list for edit is identified, pulled from the storage array and the form updated with the existing details:
function buttonEdit_Click() {
templateEditMode = true;
templateEditModeItem = parseInt($("#selectTemplates option:selected").val());
switch (templateStore[templateEditModeItem].Type) {
case "Message":
$("#RadioTypeMessage").prop('checked', true);
break;
case "Answer":
$("#RadioTypeAnswer").prop('checked', true);
break;
case "Snippet":
$("#RadioTypeSnippet").prop('checked', true);
break;
}
$("#TextTitle").val(templateStore[templateEditModeItem].Title);
$("#TextSubject").val(templateStore[templateEditModeItem].Subject);
$("#TextBody").val(templateStore[templateEditModeItem].Body);
$("#templateListing").hide();
$("#templateEdit").show();
}
The code below is the save operation performed, for both adding a new item or editing an existing item. When an item is being edited, a new item is created and the old item is replaced in the storage array. This updated array is then written out to local storage:
function buttonSave_Click() {
var item = new Template();
item.Type = $("input[name=RadioType]:checked").val();
item.Title = $("#TextTitle").val().toString();
item.Subject = $("#TextSubject").val().toString();
item.Body = $("#TextBody").val().toString();
if (templateEditMode) {
templateStore[templateEditModeItem] = item;
templateEditMode = false;
}
else {
templateStore.push(item);
}
saveToLocalStorage();
initEditorView();
}
As you can see in the code above, we create a new template by executing new Template()
. Template is actually a function which returns a suitable formed object. The function that performs this is:
function Template() {
this.Type = "";
this.Title = "";
this.Subject = "";
this.Body = "";
}
You may have also noticed on the editor selector form, a Clone button. This allows us to duplicate an existing item. To do this, we use JQuery to perform a deep copy of the existing object into a new object, first we get the existing item from the array, clone it to a new object, add the new object to the array and write the update out to the local storage. The code is shown below:
function buttonClone_Click() {
var original = templateStore[parseInt($("#selectTemplates option:selected").val())];
var copy = $.extend(true, {}, original);
templateStore.push(copy);
saveToLocalStorage();
initEditorView();
}
Filtering Capabilities
Version 1.1 introduced filtering capabilities on the Editor Form and the Selector Form. This allows the user to filter down to work with items if they have many of the three different types (Message/Answer/Snippet).
The selector form also has an AUTO filter (default selection). What this does is checks to see which CodeProject page the user is on, and filters the lists accordingly. If the user is on the Message editor, the list is automatically filtered for Messages and Snippets. If the user is on a Question Page, then the list is filtered automatically for Answers and Snippets.
Filtering is achieved by the use of storing the template array item index within the HTML markup as the value property of the options, and only adding the relevant items to the list, the value is then read to identify the source index within the storage array.
What Else Do I Need to Know?
The Title field is your own freetext for identifying your template item and what it contains, the Subject field is only used by Forum Messages, the Body is used by Forum Messages, Answers and Snippets.
Any More? What's Next?
Refer to the previous extension link for details on packaging, deployment and hosting, etc.
The links at the top of the article are the unpackaged source and also a link to the packaged distribution server. If you use this, any future updates will automatically be picked up by Chrome.
The Future
If there is anything you can think of, any ideas you have, or if you find problems, then leave a message below, and I will see what I can do.
Well, that is it for the time being, and hope you find this of use.
Reference Links
History
- 29th August 2014 - Update to V2 to support new Google policy requirements.
- 23rd October 2013 - Added note regarding install from outwith Chrome Web Store
- 16th September 2011 - V1.1, Added snippet type, filtering capability and fixed Add Item bug
- 22nd August 2011 - V1.0, Initial release