Citizen Ex is a browser extension for Safari, Firefox, and Chrome, which constantly calculates the user's "algorithmic citizenship" while showing them where the websites they visit are physically located.
Find out more or download and install the completed extension at http://citizen-ex.com.
If you haven’t yet, install Node. A bunch of packages are used to make file management easier, so stuff doesn’t have to be copied and pasted between the extensions.
Once you have Node, in the project’s root directory run the following to install packages:
$ npm install
This will use the list of requirements from package.json
to install what’s missing.
I have broken up JavaScript files into smaller, more manageable ones. To inject some HTML into the current page it has to be included in JS files, and you can’t simply load the required ones from the extension. Because keeping HTML templates as JS strings in existing JS files is unmanageable, those little templates are now kept separately and inserted into JS files using Gulp.
Gulp runs several tasks: looks for those HTML file insertions, then concatenates all of the required JS in the right order, and saves the resulting file in the right location. The final file is regenerated every time something changes in the /extensions/templates
directory.
It’s easier to organise the files, but the downside is that you have to leave Gulp running on the console.
From the project’s root directory run the following:
$ gulp
This will watch for any changes. Currently it only overwrites the extensions/chrome/injected/setup.js
file, as it includes HTML snippets.
All the tasks come from /.gulpfile.js
, which is relatively readable.
The extensions
directory is where all the extension-related code lives. Each extension has its own subdirectory, but they share some code from the templates
dir.
Templates directory has some shared JavaScript files and HTML templates to be inserted into the page.
The main areas of interest are:
extensions/templates/html
: HTML snippets injected into the pageextensions/templates/js/sidebar.js
: Backbone model which stores data and provides useful functionsextensions/templates/js/sidebar_pane.js
: Backbone view which listens to model changes and knows how to render itself
This file defines the LogEntry class for use in the extension.
In this file you can find the Backbone model storing data used in the extension, and utility functions that you can use from the view. I will be renaming it to something more appropriate.
Includes all code relating to the Backbone view. The model and the view communicate whenever changes or events occur so that any updates can be reflected immediately.
Backbone in this project is used to provide a way of organising code and managing state. Things may update at different times, and we want to display the latest information. Backbone helps with that. It has a lot of features that are not used here.
Backbone model is designed to store logic and data. In the extension we have a Sidebar model (no longer a good name, to be changed).
The sidebar model is defined, and then we create one instance of it:
var sidebar = new Sidebar();
If you want to store something on the model, you can do it like so:
sidebar.set({ catName: 'Felix', height: 50 }); // setting multiple things at once is possible
// or
sidebar.set('catName', 'Felix'); // one value only
To retrieve a piece of information:
sidebar.get('catName'); // returns 'Felix'
There are a few useful functions that are run at different times.
Some of the ones likely to be accessed from the view:
sidebar.getCitizenshipForDays(n)
: Returns an object with citizenship info for any number of days — designed for displaying as the badge.sidebar.getTabEntriesForDays(n)
: Returns an object with data for tabs open for the past n days — designed to be placed on the map.
Other interesting ones, mostly used to set things up:
sidebar.setUpCitizenship()
: Fetches from storage the info about all countries visited and remebers it.sidebar.setUpOpenTabsCitizenship()
: Fetches the info about countries visited for currently open tabs and remebers it.
Backbone views are meant to use the model for any calculations, storing data etc, and only have information about what to render, what events to handle, and which model changes to respond to.
I named the view SidebarPane but it will change.
The view is defined and then I create one instance of it for every “pane” (this was made when I thought there would be multiple pages with different content).
We want to be able to do stuff on click etc. Events are handled inside the view declaration using the events
object:
events: {
'click .erase': 'eraseData',
'click a': 'togglePane'
}
When the .erase
link is clicked, the view runs this function:
eraseData: function(event) {
event.preventDefault();
this.model.eraseData();
}
this.model.eraseData()
tells the model to run its own function called eraseData
, which actually erases stuff. The view just
sends the model a message about it. When you need the view to make changes to data, or get any calculations done, etc. this is the strategy you should use.
Model can constantly update its own data, for exaple when we initialise it we attempt to fetch the citizenship data. This can take a while, but once it is found, it is stored. The view can subscribe to changes on the model, so that it knows when to update: every time we save data, remove it or change it the view can get notified.
This subscription is set up when the view is first created, inside the initialize
function:
this.listenTo(this.model, 'change', this.render);
We tell this view to listen to any changes on the model, and when they occur it will run the render()
function. render
is the customary name.
In our case the setup is slightly different. We have a few different listeners set up, because some need extra stuff. Consider the following code:
this.listenTo(this.model, 'change:entry', function(model, logEntry) {
this.model.setUpCitizenship();
this.render(model, this.model.get('activePane'));
})
We listen to changes on entry
property (representing info about the current tab). sidebar.set('entry', { ... })
is set only once, so that function only runs when that data becomes available. It then fetches and calculates the citizenship, and only then runs the render
function.
The following happens any time change occurs. It’s general — we throw away the entire view and render it out again with fresh data.
render: function(model, pane) {
// check if this view instance is for the current pane
if (pane) {
switch (pane.name) {
case this.name:
// if so, we take all data from the model
// and use the template to render it out
this.$el.html(this.template(this.model.toJSON()));
// and animate
this.$el.slideDown();
break;
default:
this.$el.slideUp();
break;
}
} else {
this.$el.slideUp();
}
}
I will shortly update the information about the structure of the background script.