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.

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.

What about this extension?

This extension is very similiar, 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 to operates and triggers the events and intgeracts with the users page.

To start with, lets take a look at the manifest file for this extension;

{
  "name": "CodeProject Template Items",
  "version": "1.0",
  "description": "Chrome Extension to provide CodeProject template items.",
  "icons":{"48":"images/bob48.png", "128":"images/bob128.png"},	//Define any icon sizes and the files that you want to use with them. 48/128 etc.
  "background_page": "cptemplates.html",
  "page_action": {
    "default_icon": "images/bob.png",                           // image to show in the address bar
    "default_title": "CodeProject: Insert Template Items",      // text to show in the tooltip
    "default_popup": "selector.html"                            // template selector page
  },
  "options_page": "editor.html",			// Page to handle the options for the extension, used to edit the templates
  "permissions": [
    "tabs", "http://*.codeproject.com/"             
  ]
  "update_url": "http://www.dave-auld.net/ChromeExtensions/CPTemplatesChromeExt/updates.xml"		// Auto Update location
}

The usual suspects can be seen, name, version, description etc, but there are a couple of difference with;

background_page - This is the file that runs in the background of the browser, and sets up the browser integration of the extension.

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.

Background Page - What is it doing?

When the browser starts, the extension background page is loaded and any script within is executed. Take a look at the page content below;

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <title>CodeProject Template Items - Background Page</title>
        <script type="text/javascript">

            // Called when the url of a tab changes.
            function checkForTargetMessageEditorUrl(tabId, changeInfo, tab) {
                // Check if the address contains the Message Editor URL;
                if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
                    // ... show the page action.
                    chrome.pageAction.show(tabId);
                }
                // Check if the address contains the Question URL;
                if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
                    // ... show the page action.
                    chrome.pageAction.show(tabId);
                }
            };

            // Add a handler to listen for Address Bar changes in the browser, set the callback function
            chrome.tabs.onUpdated.addListener(checkForTargetMessageEditorUrl);            
    </script>
</head>

<body bgcolor="#ffcc66">
    <p>CodeProject Template Items Background Page Process - Nothing to see, move along please.</p>
</body>
</html>

Pretty standard HTML page, with integrated script. When the page 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;

pageActionIcon.png

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;

selector_page.png 

The user can now click on the template item they want 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 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;

//-----------------------------------------
//     Button Events - Selector
//-----------------------------------------
function buttonSelect_Click() {
    //
    //Get the current window and then the current tab of that window
    //This should relate to the page on which to do the injection
    chrome.windows.getCurrent(function (theWindow) {
        chrome.tabs.getSelected(theWindow.id, function (theTab) { 
            injectToTab(theTab) });
    });
}

The click event effectivly 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) {
    //Build Valid Content for remote execution
    var templateItem = parseInt($("#selectTemplates option:selected").val());
    var subject = templateStore[templateItem].Subject;
    var body = templateStore[templateItem].Body;
    
    // Check if the address contains the Message Editor URL;
    if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
        
        //Append the subject
        chrome.tabs.executeScript(tab.id, { code:
            "document.getElementById('ctl00_MC_Subject').value += unescape('" + escape(subject) + "');"
        });
        
        //Append the body
        chrome.tabs.executeScript(tab.id, { code:
            "document.getElementById('ctl00_MC_ContentText_MessageText').value += unescape('" + escape(body) + "');"
        });
    }

    // Check if the address contains the Question URL;
    if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
        //Append the answer
        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 represtation 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; 

editor_select.png

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 stringy 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() {
    //Write out the template store to the local storage
    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 code that achieves this is shown below;

function loadTemplates() {

    //Clear the list box of any items
    $("#selectTemplates").html(""); //Clear the existing selection box

    if (templateStore.length > 0) {
        //Load Template Items
        $("#templateLoading").html("Template store contains: " + templateStore.length + " item(s).");

        //Get each items Title and add it to the list select
        for (item in templateStore) {
            $("#selectTemplates").html($("#selectTemplates").html() + "<option value=\"" + item.toString() + "\">" + templateStore[item].Title + "</option>");
        }
        $("#templateListing").show();
    }
    else {
        // No items to Load
        $("#templateLoading").html("No items located in local store.");
        $("#templateListing").show();

        if (!templateSelectMode) {
            $("#templateEdit").hide();
            $("#buttonEdit").hide();
            $("#buttonClone").hide();
            $("#buttonDelete").hide();
        }
        else {
            $("#buttonSelect").hide();
        }
    }
    
    //Add always is shown (in editor form).
    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;

editor_page.png

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());

    if (templateStore[templateEditModeItem].Type == "Message") {
        $("#RadioTypeMessage").prop('checked', true);
    }
    else {
        $("#RadioTypeAnswer").prop('checked', true);
    }

    $("#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() {
    //Create new item
    var item = new Template();

    //Add item properties
    item.Type = $("input[name=RadioType]:checked").val();
    item.Title = $("#TextTitle").val().toString();
    item.Subject = $("#TextSubject").val().toString();
    item.Body = $("#TextBody").val().toString();

    if (templateEditMode) {
        //Edit Mode
        templateStore[templateEditModeItem] = item;
        templateEditMode = false;
    }
    else {
        //Add Mode
        templateStore.push(item);
    }

    //Save to local storage
    saveToLocalStorage();

    //Restart
    initEditorView();
}

As you can see in the code above, we create a new template by executing new Template(). Template is actually function which returns a suitable formed object. The function that performs this is;

//Template Object Constructor
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() {
    //Get the current selected item
    var original = templateStore[parseInt($("#selectTemplates option:selected").val())];
    
    //Perfrom a deep copy of the original using JQuery
    var copy = $.extend(true, {}, original);

    //Push it into the store at the end
    templateStore.push(copy);

    //save
    saveToLocalStorage();

    initEditorView();
}

What does the Message / Answer Radio buttons do?

With the initial release of the extension, all they do is store some additional info into the object. This is to allow potential future update of the extension to handle filtering within the selector page. I added it now so as not to break the existing local storage functionality.

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 both Forum Messages and Answers. Also you can use the Body for just quick snippets you want to store for use in the Forums or Answers.

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

Hopefully looking to add some filtering into the list selectors, but haven't a clue when I will get round to this, so have released this version just to get going. If there is anything you can think of, any ideas you have, or if you find problems, then leave a message below.

Well, that is it for the time being, and hope you find this of use.

Reference Links

History

  • 22nd August 2011 - V1.0, Initial Release
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"