The spell checker hack as a Chrome extension

I described the motivation behind this hack and how it works in my last post Spell checker hack for Ghost. In this post, I will try to describe the whole process of creating/deploying a Chrome extension which wraps the hack.

The Hack

The whole "hack" is super simple. It just adds a link element to the toolbar and attaches its click event. The click event handler toggles between displaying the text area behind the CodeMirror view element and updates its value. That's it. Nothing complicated.

You can find more in the embedded comments

rawtext.js:

var run = function() {

    // if there is no editor in the current view it is probably wrong page that passed url validation
    if (!window.Ghost || !window.Ghost.currentView || !window.Ghost.currentView.editor) return;

    var spellCheckHack = function() {
        // hookup everything we need
        var editor = window.Ghost.currentView.editor;
        var textArea = editor.getTextArea();
        var $textArea = $(textArea);
        var $codeMirror = $textArea.next();

        // create the "RAW TEXT" button
        var $btn = $('<a href="#" style="float:right" class="rawtext">RAW TEXT</a>');

        // tweak the textarea positions (move it bellow bottom toolbar)
        $textArea.css('top', '35px');
        $textArea.css('bottom', '45px');
        $textArea.css('right', '10px');
        $textArea.css('left', '10px');
        $textArea.css('padding', '10px');
        $textArea.css('width', 'auto');
        $textArea.css('font-size', '18px');

        var visible = false;        
        var hack = {};

        hack.hookup = function() {
            var $floatingHeader = $textArea.closest('.entry-markdown').find('.floatingheader');

            // check if it is hookuped already
            if ($floatingHeader.find('.rawtext').length) {
                return;
            }

            $btn.appendTo($floatingHeader);
            $btn.on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (visible) {
                    hack.hide();
                } else { 
                    hack.show();
                }
            });

            // update it before clicking to save
            $textArea.on("blur", function(){
                editor.setValue($textArea.val());
            });
        };

        hack.show = function() {
            $textArea.show();
            $codeMirror.hide();
            var value = editor.getValue();
            $textArea.val(value);
            visible = true;
            $btn.html("EDITOR");
        };

        hack.hide = function() {
            $textArea.hide();
            $codeMirror.show();
            editor.setValue($textArea.val());
            visible = false;
            $btn.html("RAW TEXT");           
        };

        return hack;
    };

    spellCheckHack().hookup();
};

// wait to the ghost backbone views initialisation is done
setTimeout(run, 200);  

Extension

The development of Chrome extensions is an easy process but I think the documentation could be a little bit better http://developer.chrome.com/extensions/overview.html.

I will cover only parts that are used by our extension. Simply, because that's all I know :)

There are 3 different type of extension points we use:

  • content script
  • page action
  • background script

Content script

The page actions should do the hard work. It has access to DOM but it's script is running in a different context. That means it has no access to script variables used on the opened page.

But we need to do that. OK, we still have access to DOM, that means we can easily add a script element which will load a script from our extension and inject our hack to the Ghost editor page.

inject.js:

var injectedScript = document.createElement('script');  
injectedScript.src = chrome.extension.getURL("rawtext.js");  
injectedScript.onload = function() {  
    this.parentNode.removeChild(this);
};
document.documentElement.appendChild(injectedScript);  

Page action

We want to display the RAW icon on the page if the "hack" is injected. That is exactly the point where we need to use page action. There is no action at all in-fact. We define this action just to display the icon. If you click on it, it displays the info as well but the info box doesn't have any functionality.

The page action is defined in the manifest that you can find in the section below.

Background script

The page action is displayed for all pages by default. If we want to "enable" it for some pages only, we need to use a background script which does this job.

function showPageAction(tabId, changeInfo, tab) {  
    if (tab.url.indexOf('/ghost/editor') > -1) {
        chrome.pageAction.show(tabId);
    }   
}

chrome.tabs.onUpdated.addListener(showPageAction);  

Bundling

We have it all prepared. Now it's time to bundle it together.

The simplest bundle can be just a flat directory. The most important thing is the manifest file which describes the bundle itself. Lets look at our manifest.

manifest.json:

{
  "manifest_version": 2,

     "name": "Ghost Editor - RAW TEXT Button",
     "description": "Adds RAW TEXT button to your ghost editor. Switches editor to the simple mode where the browser spell checking is working.",
     "version": "1.1",
     "icons": { 
         "48": "icon48.png",
         "128": "icon128.png"
    },
     "background": { 
         "scripts": ["background.js"] 
    }, 
    "content_scripts": [
        { 
            "matches": [
                "http://*/ghost/editor/*",
                "https://*/ghost/editor/*"
            ], 
            "js": ["inject.js"] 
        }
      ],
      "page_action": {
        "default_name": "Extends Ghost editor for RAW TEXT",
        "default_icon": "icon19.png",
        "default_popup": "popup.html"
    },      
      "permissions": [
          "tabs"
      ],
      "web_accessible_resources": [
          "rawtext.js"
      ]
}

The important parts are:

  • background.scripts - our background script definition
  • content_scripts - define the content script (inject.js) should be used for all pages of the matching urls
  • page_action - defines the icon which is displayed on the injected page
  • permissions - the background script accesses the tabs and we need to have permission to do that
  • web_accessible_resources - all resources that are used by our extension need to be defined here otherwise they don't load

The bundle itself is just a zip file containing all our files, icons included.

As I said it's just a zip file but who would like to do that over and over again. No one. Fortunately there are tools that can save us from this boring task. For example Grunt. Here you can find how to install it Getting Started

Our grunt file can look like this, gruntfile.js:

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    zip: {
      extension: {
      cwd: './../src',
      // Files to zip together
      src: ['./../src/*.{js,png,html,json}'],

      // Destination of zip file
      dest: '../out/extension.zip'
    },

    }
  });

  grunt.loadNpmTasks('grunt-zip');

  grunt.registerTask('default', ['zip']);

};

Deploying

To be able to deploy Chrome extensions to the Chrome Web Store you need to pay a $5 one-time developer fee. Then you can access your developer dashboard. If you think the $5 is almost a bargain maybe you will change your mind after a few minutes spent in the dashboard. Google should probably pay you for using it. It's just awful.

However, you are able to upload your extensions. All you need is just the zip file with bundled extension and then fill in a few titles, descriptions, icons, screenshots, ... It depends what you want to offer your users.

Don't forget to be patient after clicking the "Publish" button. It takes forever for the browser to finish this action.

As you could see creating and deploying a Chrome extension isn't rocket science. You can create and deploy one in about an hour.

The hardest part is to come up with an idea that the extension could solve, at least for me.

You can find all source codes at github (https://github.com/pvasek/GhostRawTextExtension)

Any feedback would be appreciated.