Annotator documentation

Warning

Beware: rapidly changing documentation!

This is the bleeding-edge documentation for Annotator which will be changing rapidly as we home in on Annotator v2.0. Information here may be inaccurate, prone to change, and otherwise unreliable. You may well want to consult the stable documentation instead.

Welcome to the documentation for Annotator, an open-source JavaScript library for building annotation systems on the web. At its simplest, Annotator is an application that allows users to make textual annotations on any web page. You can deploy it using just a few lines of integration code.

Annotator is also a library of composable tools for annotation applications. It includes tools for capturing and manipulating DOM selections, tools for storage and persistence of annotations, and a variety of user interface components. You can use a few or many of these components together to build your own custom annotation-based application.

You can start learning about how to install and deploy Annotator below:

Installing

Annotator is a JavaScript library, and there are two main approaches to using it. You can either use the standalone packaged files, or you can install it from the npm package repository and integrate the source code into your own browserify or webpack toolchain.

Built packages

Releases are published on our GitHub repository. The released zip file will contain minified, production-ready JavaScript files which you can include in your application.

To load Annotator with the default set of components, place the following <script> tag towards the bottom of the document <body>:

<script src="annotator.min.js"></script>

To load additional plugins, append the relevant <script> tags:

<script src="annotator-document.min.js"></script>

Now you can continue to Configuring and using Annotator.

npm package

We also publish an annotator package to npm. This package is not particularly useful in a Node.js context, but can be used by browserify or webpack. Please see the documentation for these packages for more information on using them.

Configuring and using Annotator

This document assumes you have already downloaded and installed Annotator into your web page. If you have not, please read Installing before continuing.

The basics

When Annotator is loaded into the page, it exposes a single root object, annotator, which provides access to the main annotator.App object and all other included components.

To create a new App instance with a default configuration for the whole document, you can run:

var ann = new annotator.App(document.body);

You can configure the default App using the options argument to the constructor:

var ann = new annotator.App(document.body, {
   readOnly: true
});

See the docs for App for more details.

Storage

Some kind of storage is needed to persist the annotations that you create while on a web page.

To do this you can use the Annotator.Storage.HTTPStorage component and a remote JSON API. Storage API describes the API implemented by the reference backend. It is this backend that runs the AnnotateIt web service.

Annotator.Storage.HTTPStorage component

This storage component sends annotations (serialised as JSON) to a server that implements the Storage API: manufacturing/widgets.

Methods

The following methods are implemented by this component.

  • create: POSTs an annotation (serialised as JSON) to the server. Called when the annotator publishes the “annotationCreated” event. The annotation is updated with any data (such as a newly created id) returned from the server.
  • update: PUTs an annotation (serialised as JSON) on the server under its id. Called when the annotator publishes the “annotationUpdated” event. The annotation is updated with any data (such as a newly created id) returned from the server.
  • destroy: Issues a DELETE request to server for the annotation.
  • search: GETs all annotations relevant to the query. Should return a JSON object with a rows property containing an array of annotations.

Backends

For an example backend check out our annotator-store project on GitHub which you can use or examine as the basis for your own store. If you’re looking to get up and running quickly then annotateit.org will store your annotations remotely under your account.

Interface Overview

This plugin adds no additional UI to the Annotator but will display error notifications if a request to the store fails.

Warning

OUT OF DATE DOCUMENTATION

The paragraphs that follow contain out-of-date documentation that we haven’t yet got round to updating.

Usage

Adding the store plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin and retrieve the annotator object using .data('annotator'). Then add the Store plugin.

var content = $('#content').annotator();
    content.annotator('addPlugin', 'Store', {
      // The endpoint of the store on your server.
      prefix: '/store/endpoint',

      // Attach the uri of the current page to all annotations to allow search.
      annotationData: {
        'uri': 'http://this/document/only'
      },

      // This will perform a "search" action when the plugin loads. Will
      // request the last 20 annotations for the current url.
      // eg. /store/endpoint/search?limit=20&uri=http://this/document/only
      loadFromSearch: {
        'limit': 20,
        'uri': 'http://this/document/only'
      }
    });
Options

The following options are made available for customisation of the store.

  • prefix: The store endpoint.
  • annotationData: An object literal containing any data to attach to the annotation on submission.
  • loadFromSearch: Search options for using the “search” action.
  • urls: Custom URL paths.
  • showViewPermissionsCheckbox: If true will display the “anyone can view this annotation” checkbox.
  • showEditPermissionsCheckbox: If true will display the “anyone can edit this annotation” checkbox.
prefix

This is the API endpoint. If the server supports Cross Origin Resource Sharing (CORS) a full URL can be used here. Defaults to /store.

NOTE: The trailing slash should be omitted.

Example:

$('#content').annotator('addPlugin', 'Store', {
  prefix: '/store/endpoint'
});
annotationData

Custom meta data that will be attached to every annotation that is sent to the server. This will override previous values.

Example:

$('#content').annotator('addPlugin', 'Store', {
  // Attach a uri property to every annotation sent to the server.
  annotationData: {
    'uri': 'http://this/document/only'
  }
});
loadFromSearch

An object literal containing query string parameters to query the store. If loadFromSearch is set, then we load the first batch of annotations from the ‘search’ URL as set in options.urls instead of the registry path ‘prefix/read’. Defaults to false.

Example:

$('#content').annotator('addPlugin', 'Store', {
  loadFromSearch: {
    'limit': 0,
    'all_fields': 1,
    'uri': 'http://this/document/only'
  }
});
urls

The server URLs for each available action (excluding prefix). These URLs can point anywhere but must respond to the appropriate HTTP method. The :id token can be used anywhere in the URL and will be replaced with the annotation id.

Methods for actions are as follows:

create:  POST
update:  PUT
destroy: DELETE
search:  GET

Example:

$('#content').annotator('addPlugin', 'Store', {
  urls: {
    // These are the default URLs.
    create:  '/annotations',
    update:  '/annotations/:id',
    destroy: '/annotations/:id',
    search:  '/search'
  }
}):

Storage API

This document details the HTTP API used by the Annotator.Storage.HTTPStorage component. It is targeted at developers interested in developing their own backend servers that integrate with Annotator, or developing tools that integrate with existing instances of the API.

The storage API attempts to follow the principles of REST, and uses JSON as its primary interchange format.

Endpoints

root
GET /api

API root. Returns an object containing store metadata, including hypermedia links to the rest of the API.

Example request:

GET /api
Host: example.com
Accept: application/json

Example response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Length, Content-Type, Location
Content-Length: 1419
Content-Type: application/json

{
    "message": "Annotator Store API",
    "links": {
        "annotation": {
            "create": {
                "desc": "Create a new annotation",
                "method": "POST",
                "url": "http://example.com/api/annotations"
            },
            "delete": {
                "desc": "Delete an annotation",
                "method": "DELETE",
                "url": "http://example.com/api/annotations/:id"
            },
            "read": {
                "desc": "Get an existing annotation",
                "method": "GET",
                "url": "http://example.com/api/annotations/:id"
            },
            "update": {
                "desc": "Update an existing annotation",
                "method": "PUT",
                "url": "http://example.com/api/annotations/:id"
            }
        },
        "search": {
            "desc": "Basic search API",
            "method": "GET",
            "url": "http://example.com/api/search"
        }
    }
}
Request Headers:
 
  • Accept – desired response content type
Response Headers:
 
Status Codes:
read
GET /api/annotations/(string: id)

Retrieve a single annotation.

Example request:

GET /api/annotations/utalbWjUaZK5ifydnohjmA
Host: example.com
Accept: application/json

Example response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    "created": "2013-08-26T13:31:49.339078+00:00",
    "updated": "2013-08-26T14:09:14.121339+00:00",
    "id": "utalbWjUQZK5ifydnohjmA",
    "uri": "http://example.com/foo",
    "user": "acct:johndoe@example.org",
    ...
}
Request Headers:
 
  • Accept – desired response content type
Response Headers:
 
Status Codes:
create
POST /api/annotations

Create a new annotation.

Example request:

POST /api/annotations
Host: example.com
Accept: application/json
Content-Type: application/json;charset=UTF-8

{
    "uri": "http://example.org/",
    "user": "joebloggs",
    "permissions": {
        "read": ["group:__world__"],
        "update": ["joebloggs"],
        "delete": ["joebloggs"],
        "admin": ["joebloggs"],
    },
    "target": [ ... ],
    "text": "This is an annotation I made."
}

Example response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    "id": "AUxWM-HasREW1YKAwhil",
    "uri": "http://example.org/",
    "user": "joebloggs",
    ...
}
Parameters:
  • id – annotation’s unique id
Request Headers:
 
Response Headers:
 
Response JSON Object:
 
  • id (string) – unique id of new annotation
Status Codes:
update
PUT /api/annotations/(string: id)

Update the annotation with the given id. Requires a valid authentication token.

Example request:

PUT /api/annotations/AUxWM-HasREW1YKAwhil
Host: example.com
Accept: application/json
Content-Type: application/json;charset=UTF-8

{
    "uri": "http://example.org/foo",
}

Example response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    "id": "AUxWM-HasREW1YKAwhil",
    "updated": "2015-03-26T13:09:42.646509+00:00"
    "uri": "http://example.org/foo",
    "user": "joebloggs",
    ...
}
Parameters:
  • id – annotation’s unique id
Request Headers:
 
Response Headers:
 
Status Codes:
delete
DELETE /api/annotations/(string: id)

Delete the annotation with the given id. Requires a valid authentication token.

Example request:

DELETE /api/annotations/AUxWM-HasREW1YKAwhil
Host: example.com
Accept: application/json

Example response:

HTTP/1.1 204 No Content
Content-Length: 0
Parameters:
  • id – annotation’s unique id
Request Headers:
 
  • Accept – desired response content type
Response Headers:
 
Status Codes:

Storage Implementations

Warning

OUT OF DATE DOCUMENTATION

The paragraphs that follow contain out-of-date documentation that we haven’t yet got round to updating.

Auth plugin

The Auth plugin complements the store by providing authentication for requests. This may be necessary if you are running the Store on a separate domain or using a third party service like annotateit.org.

The plugin works by requesting an authentication token from the local server and then provides this in all requests to the store. For more details see the specification.

Usage

Adding the Auth plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin. Then call the .addPlugin() method eg. .annotator('addPlugin', 'Auth').

var content = $('#content'));
content.annotator('addPlugin', 'Auth', {
  tokenUrl: '/auth/token'
});

Options

The following options are available to the Auth plugin.

  • tokenUrl: The URL to request the token from. Defaults to /auth/token.
  • token: An auth token. If this is present it will not be requested from the server. Defaults to null.
  • autoFetch: Whether to fetch the token when the plugin is loaded. Defaults to true
Token format

For details of the token format, see the page on Annotator’s Authentication system.

Plugins

Annotator has a highly modular architecture, and a great deal of functionality is provided by plugins. These pages document these plugins and how they work together.

Filter plugin

This plugin allows the user to navigate and filter the displayed annotations.

Interface Overview

The plugin adds a toolbar to the top of the window. This contains the available filters that can be applied to the current annotations.

Usage

Adding the Filter plugin to the annotator is very simple. Add the annotator to the page using the .annotator() jQuery plugin. Then call the .addPlugin() method by calling .annotator('addPlugin', 'Filter').

var content = $('#content').annotator().annotator('addPlugin', 'Filter');
Options

There are several options available to customise the plugin.

  • filters: This is an array of filter objects. These will be added to the toolbar on load.
  • addAnnotationFilter: If true this will display the default filter that searches the annotation text.
Filters

Filters are very easy to create. The options require two properties a label and an annotation property to search for. For example if we wanted to filter on an annotations quoted text we can create the following filter.

content.annotator('addPlugin', 'Filter', {
  filters: [
    {
      label: 'Quote',
      property: 'quote'
    }
  ]
});

You can also customise the filter logic that determines if an annotation should be filtered by providing an isFiltered function. This function receives the contents of the filter input as well as the annotation property. It should return true if the annotation should remain highlighted.

Heres an example that uses the annotation.tags property, which is an array of tags:

content.annotator('addPlugin', 'Filter', {
  filters: [
    {
      label: 'Tag',
      property: 'tags',
      isFiltered: function (input, tags) {
        if (input && tags && tags.length) {
          var keywords = input.split(/\s+/g);
          for (var i = 0; i < keywords.length; i += 1) {
            for (var j = 0; j < tags.length; j += 1) {
              if (tags[j].indexOf(keywords[i]) !== -1) {
                return true;
              }
            }
          }
        }
        return false;
    }}
  ]
});

Markdown Plugin

The Markdown plugin allows you to use Markdown in your annotation comments. It will then render them in the Viewer.

Requirements

This plugin requires that the Showdown Markdown library be loaded in the page before the plugin is added to the annotator. To do this simply download the showdown.js and include it on your page before the annotator.

<script src="javascript/jquery.js"></script>
<script src="javascript/showdown.js"></script>
<script src="javascript/annotator.min.js"></script>
<script src="javascript/annotator.markdown.min.js"></script>

Usage

Adding the Markdown plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin and retrieve the annotator object using .data('annotator'). Then add the Markdown plugin.

var content = $('#content').annotator();
content.annotator('addPlugin', 'Markdown');
Options

There are no options available for this plugin

Warning

OUT OF DATE DOCUMENTATION

The paragraphs that follow contain out-of-date documentation that we haven’t yet got round to updating.

Permissions plugin

This plugin handles setting the user and permissions properties on annotations as well as providing some enhancements to the interface.

Interface Overview

The following elements are added to the Annotator interface by this plugin.

Viewer

The plugin adds a section to a viewed annotation displaying the name of the user who created it. It also checks the annotation’s permissions to see if the current user can edit/delete the current annotation and displays controls appropriately.

Editor

The plugin adds two fields with checkboxes to the annotation editor (these are only displayed if the current user has admin permissions on the annotation). One to allow anyone to view the annotation and one to allow anyone to edit the annotation.

Usage

Adding the permissions plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin and retrieve the annotator object using .data('annotator'). We now add the plugin and pass an options object to set the current user.

var annotator = $('#content').annotator().data('annotator');
annotator.addPlugin('Permissions', {
  user: 'Alice'
});

By default all annotations are publicly viewable/editable/deleteable. We can set our own permissions using the options object.

var annotator = $('#content').annotator().data('annotator');
annotator.addPlugin('Permissions', {
  user: 'Alice',
  permissions: {
    'read':   [],
    'update': ['Alice'],
    'delete': ['Alice'],
    'admin':  ['Alice']
  }
});

Now only our current user can edit the annotations but anyone can view them.

Options

The options object allows you to completely define the way permissions are handled for your site.

  • user: The current user (required).
  • permissions: An object defining annotation permissions.
  • userId: A callback that returns the user id.
  • userString: A callback that returns the users name.
  • userAuthorize: A callback that allows custom authorisation.
  • showViewPermissionsCheckbox: If false hides the “Anyone can view…” checkbox.
  • showEditPermissionsCheckbox: If false hides the “Anyone can edit…” checkbox.
user (required)

This value sets the current user and will be attached to all newly created annotations. It can be as simple as a username string or if your users objects are more complex an object literal.

// Simple example.
annotator.addPlugin('Permissions', {
  user: 'Alice'
});

// Complex example.
annotator.addPlugin('Permissions', {
  user: {
    id: 6,
    username: 'Alice',
    location: 'Brighton, UK'
  }
});

If you do decide to use an object for your user as well as permissions you’ll need to also provide userId and userString callbacks. See below for more information.

permissions

Permissions set who is allowed to do what to your annotations. There are four actions:

  • read: Who can view the annotation
  • update: Who can edit the annotation
  • delete: Who can delete the annotation
  • admin: Who can change these permissions on the annotation

Each action should be an array of tokens. An empty array means that anyone can perform that action. Generally the token will just be the users id. If you need something more complex (like groups) you can use your own syntax and provide a userAuthorize callback with your options.

Here’s a simple example of setting the permissions so that only the current user can perform all actions:

annotator.addPlugin('Permissions', {
  user: 'Alice',
  permissions: {
    'read':   ['Alice'],
    'update': ['Alice'],
    'delete': ['Alice'],
    'admin':  ['Alice']
  }
});

Or here is an example using numerical user ids:

annotator.addPlugin('Permissions', {
  user: {id: 6, name:'Alice'},
  permissions: {
    'read':   [6],
    'update': [6],
    'delete': [6],
    'admin':  [6]
  }
});
userId(user)

This is a callback that accepts a user parameter and returns the identifier. By default this assumes you will be using strings for your ids and simply returns the parameter. However if you are using a user object you’ll need to implement this:

annotator.addPlugin('Permissions', {
  user: {id: 6, name:'Alice'},
  userId: function (user) {
    if (user && user.id) {
      return user.id;
    }
    return user;
  }
});
// When called.
userId({id: 6, name:'Alice'}) // => Returns 6

NOTE: This function should handle null being passed as a parameter. This is done when checking a globally editable annotation.

userString(user)

This is a callback that accepts a user parameter and returns the human readable name for display. By default this assumes you will be using a string to represent your users name and id so simply returns the parameter. However if you are using a user object you’ll need to implement this:

annotator.addPlugin('Permissions', {
  user: {id: 6, name:'Alice'},
  userString: function (user) {
    if (user && user.name) {
      return user.name;
    }
    return user;
  }
});
// When called.
userString({id: 6, name:'Alice'}) // => Returns 'Alice'
userAuthorize(action, annotation, user)

This is another callback that allows you to implement your own authorization logic. It receives three arguments:

  • action: Action that is being checked, ‘update’, ‘delete’ or ‘admin’. ‘create’ does not call this callback
  • annotation: The entire annotation object; note that the permissions subobject is at annotation.permissions
  • user: current user, as passed in to the permissions plugin

Your function will check to see if the user can perform an action based on these values.

The default implementation assumes that the user is a simple string and the tokens used (within annotation.permissions) are also strings so simply checks that the user is one of the tokens for the current action.

// This is the default implementation as an example.
annotator.addPlugin('Permissions', {
  user: 'Alice',
    userAuthorize: function(action, annotation, user) {
      var token, tokens, _i, _len;
      if (annotation.permissions) {
        tokens = annotation.permissions[action] || [];
        if (tokens.length === 0) {
          return true;
        }
        for (_i = 0, _len = tokens.length; _i < _len; _i++) {
          token = tokens[_i];
          if (this.userId(user) === token) {
            return true;
          }
        }
        return false;
      } else if (annotation.user) {
        if (user) {
          return this.userId(user) === this.userId(annotation.user);
        } else {
          return false;
        }
      }
      return true;
    },
});
// When called.
userAuthorize('update', aliceAnnotation, 'Alice') // => Returns true
userAuthorize('Alice', bobAnnotation, 'Bob')   // => Returns false

Tags plugin

This plugin allows the user to tag their annotations with keywords.

Interface Overview

The following elements are added to the Annotator interface by this plugin.

Viewer

The plugin adds a section to a viewed annotation displaying any tags that have been added.

Editor

The plugin adds an input field to the editor allowing the user to enter a space separated list of tags.

Usage

Adding the tags plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin. Then call the .addPlugin() method by calling .annotator('addPlugin', 'Tags').

var content = $('#content').annotator().annotator('addPlugin', 'Tags');
Options

There are no options available for this plugin

Adding autocompletion of tags

See this example using jQueryUI autocomplete.

Unsupported plugin

The Annotator only supports browsers that have the window.getSelection() method (for a table of support please see this Quirksmode article). This plugin provides a notification to users of these unsupported browsers letting them know that the plugin has not loaded.

Usage

Adding the unsupported plugin to the annotator is very simple. Simply add the annotator to the page using the .annotator() jQuery plugin. Then call the .addPlugin() method eg. .annotator('addPlugin', 'Unsupported').

var content = $('#content').annotator();
content.annotator('addPlugin', 'Unsupported');
Options

You can provide options

  • message: A customised message that you wish to display to users.
message

The message that you wish to display to users.

var annotator = $('#content').annotator().data('annotator');

annotator.addPlugin('Unsupported', {
  message: "We're sorry the Annotator is not supported by this browser"
});

Plugin development

Getting Started

Building a plugin is very simple. Simply attach a function that creates your plugin to the Annotator.Plugin namespace. The function will receive the following arguments.

element
The DOM element that is currently being annotated.

Additional arguments (such as options) can be passed in by the user when the plugin is added to the Annotator. These will be passed in after the element.

Annotator.Plugin.HelloWorld = function (element) {
  var myPlugin = {};
  // Create your plugin here. Then return it.
  return myPlugin;
};

Using Your Plugin

Adding your plugin to the annotator is the same as for all supported plugins. Simply call “addPlugin” on the annotator and pass in the name of the plugin and any options. For example:

// Setup the annotator on the page.
var content = $('#content').annotator();

// Add your plugin.
content.annotator('addPlugin', 'HelloWorld' /*, any other options */);

Setup

When the annotator creates your plugin it will take the following steps.

  1. Call your Plugin function passing in the annotated element plus any additional arguments. (The Annotator calls the function with new allowing you to use a constructor function if you wish).
  2. Attaches the current instance of the Annotator to the .annotator property of the plugin.
  3. Calls .pluginInit() if the method exists on your plugin.

pluginInit()

If your plugin has a pluginInit() method it will be called after the annotator has been attached to your plugin. You can use it to set up the plugin.

In this example we add a field to the viewer that contains the text provided when the plugin was added.

Annotator.Plugin.Message = function (element, message) {
  var plugin = {};

  plugin.pluginInit = function () {
      this.annotator.viewer.addField({
        load: function (field, annotation) {
          field.innerHTML = message;
        }
      })
  };

  return plugin;
}

Usage:

// Setup the annotator on the page.
var content = $('#content').annotator();

// Add your plugin to the annotator and display the message "Hello World"
// in the viewer.
content.annotator('addPlugin', 'Message', 'Hello World');

Extending Annotator.Plugin

All supported Annotator plugins use a base “class” that has some useful features such as event handling. To use this you simply need to extend the Annotator.Plugin function.

// This is now a constructor and needs to be called with `new`.
Annotator.Plugin.MyPlugin = function (element, options) {

  // Call the Annotator.Plugin constructor this sets up the .element and
  // .options properties.
  Annotator.Plugin.apply(this, arguments);

  // Set up the rest of your plugin.
};

// Set the plugin prototype. This gives us all of the Annotator.Plugin methods.
Annotator.Plugin.MyPlugin.prototype = new Annotator.Plugin();

// Now add your own custom methods.
Annotator.Plugin.MyPlugin.prototype.pluginInit = function () {
  // Do something here.
};

If you’re using jQuery you can make this process a lot neater.

Annotator.Plugin.MyPlugin = function (element, options) {
  // Same as before.
};

jQuery.extend(Annotator.Plugin.MyPlugin.prototype, new Annotator.Plugin(), {
  events: {},
  options: {
    // Any default options.
  }
  pluginInit: function () {

  },
  myCustomMethod: function () {

  }
});

Annotator.Plugin API

The Annotator.Plugin provides the following methods and properties.

element

This is the DOM element currently being annotated wrapped in a jQuery wrapper.

options

This is the options object, you can set default options when you create the object and they will be overridden by those provided when the plugin is created.

events

These can be either DOM events to be listened for within the .element or custom events defined by you. Custom events will not receive the event property that is passed to DOM event listeners. These are bound when the plugin is instantiated.

publish(name, parameters)

Publish a custom event to all subscribers.

  • name: The event name.
  • parameters: An array of parameters to pass to the subscriber.

subscribe(name, callback)

Subscribe to a custom event. This can be used to subscribe to your own events or those broadcast by the annotator and other plugins.

  • name: The event name.
  • callback: A callback to be fired when the event is published. The callback will receive any arguments sent when the event is published.

unsubscribe(name, callback)

Unsubscribe from an event.

  • name: The event name.
  • callback: The callback to be unsubscribed.

Annotator Events

The annotator fires the following events at key points in its operation. You can subscribe to them using the .subscribe() method. This can be called on either the .annotator object or if you’re extending Annotator.Plugin the plugin instance itself. The events are as follows:

beforeAnnotationCreated(annotation)
called immediately before an annotation is created. If you need to modify the annotation before it is saved use this event.
annotationCreated(annotation)
called when the annotation is created use this to store the annotations.
beforeAnnotationUpdated(annotation)
as above, but just before an existing annotation is saved.
annotationUpdated(annotation)
as above, but for an existing annotation which has just been edited.
annotationDeleted(annotation)
called when the user deletes an annotation.
annotationEditorShown(editor, annotation)
called when the annotation editor is presented to the user.
annotationEditorHidden(editor)
called when the annotation editor is hidden, both when submitted and when editing is cancelled.
annotationEditorSubmit(editor, annotation)
called when the annotation editor is submitted.
annotationViewerShown(viewer, annotations)
called when the annotation viewer is shown and provides the annotations being displayed.
annotationViewerTextField(field, annotation)
called when the text field displaying the annotation comment in the viewer is created.

Example

A plugin that logs annotation activity to the console.

Annotator.Plugin.StoreLogger = function (element) {
  return {
    pluginInit: function () {
      this.annotator
          .subscribe("annotationCreated", function (annotation) {
            console.info("The annotation: %o has just been created!", annotation)
          })
          .subscribe("annotationUpdated", function (annotation) {
            console.info("The annotation: %o has just been updated!", annotation)
          })
          .subscribe("annotationDeleted", function (annotation) {
            console.info("The annotation: %o has just been deleted!", annotation)
          });
    }
  }
};

Annotation format

An annotation is a JSON document that contains a number of fields describing the position and content of an annotation within a specified document:

{
  "id": "39fc339cf058bd22176771b3e3187329",  # unique id (added by backend)
  "annotator_schema_version": "v1.0",        # schema version: default v1.0
  "created": "2011-05-24T18:52:08.036814",   # created datetime in iso8601 format (added by backend)
  "updated": "2011-05-26T12:17:05.012544",   # updated datetime in iso8601 format (added by backend)
  "text": "A note I wrote",                  # content of annotation
  "quote": "the text that was annotated",    # the annotated text (added by frontend)
  "uri": "http://example.com",               # URI of annotated document (added by frontend)
  "ranges": [                                # list of ranges covered by annotation (usually only one entry)
    {
      "start": "/p[69]/span/span",           # (relative) XPath to start element
      "end": "/p[70]/span/span",             # (relative) XPath to end element
      "startOffset": 0,                      # character offset within start element
      "endOffset": 120                       # character offset within end element
    }
  ],
  "user": "alice",                           # user id of annotation owner (can also be an object with an 'id' property)
  "consumer": "annotateit",                  # consumer key of backend
  "tags": [ "review", "error" ],             # list of tags (from Tags plugin)
  "permissions": {                           # annotation permissions (from Permissions/AnnotateItPermissions plugin)
    "read": ["group:__world__"],
    "admin": [],
    "update": [],
    "delete": []
  }
}

Note that this annotation includes some info stored by plugins (notably the Permissions plugin and Tags plugin).

This basic schema is completely extensible. It can be added to by plugins, and any fields added by the frontend should be preserved by backend implementations. For example, the plugins/store (which adds persistence of annotations) allow you to specify arbitrary additional fields using the annotationData attribute.

Authentication

What’s the authentication system for?

The simplest way to explain the role of the authentication system is by example. Consider the following:

  1. Alice builds a website with documents which need annotating, DocLand.
  2. Alice registers DocLand with AnnotateIt, and receives a “consumer key/secret” pair.
  3. Alice’s users (Bob is one of them) login to her DocLand, and receive an authentication token, which is a cryptographic combination of (among other things) their unique user ID at DocLand, and DocLand’s “consumer secret”.
  4. Bob’s browser sends requests to AnnotateIt to save annotations, and these include the authentication token as part of the payload.
  5. AnnotateIt can verify the Bob is a real user from DocLand, and thus stores his annotation.

So why go to all this trouble? Well, the point is really to save you trouble. By implementing this authentication system (which shares key ideas with the industry standard OAuth) you can provide your users with the ability to annotate documents on your website without needing to worry about implementing your own Annotator backend. You can use AnnotateIt to provide the backend: all you have to do is implement a token generator on your website (described below).

This is the simple explanation, but if you’re in need of more technical details, keep reading.

Technical overview

How do we authorise users’ browsers to create annotations on a Consumer’s behalf? There are three (and a half) entities involved:

  1. The Service Provider (SP; AnnotateIt in the above example)
  2. The Consumer (C; DocLand)
  3. The User (U; Bob), and the User Agent (UA; Bob’s browser)

Annotations are stored by the SP, which provides an API that the Annotator’s “Store” plugin understands.

Text to be annotated, and configuration of the clientside Annotator, is provided by the Consumer.

Users will typically register with the Consumer – we make no assumptions about your user registration/authentication process other than that it exists – and the UA will, when visiting appropriate sections of C’s site, request an authToken from C. Typically, an authToken will only be provided if U is currently logged into C’s site.

Technical specification

It’s unlikely you’ll need to understand all of the following to get up and running using AnnotateIt – you can probably just copy and paste the Python example given below – but it’s worth reading what follows if you’re doing anything unusual (such as giving out tokens to unauthenticated users).

The Annotator authToken is a type of JSON Web Token. This document won’t describe the details of the JWT specification, other than to say that the token payload is signed by the consumer secret with the HMAC-SHA256 algorithm, allowing the backend to verify that the contents of the token haven’t been interfered with while travelling from the consumer. Numerous language implementations exist already (PyJWT, jwt for Ruby, php-jwt, JWT-CodeIgniter...).

The required contents of the token payload are:

key description example
consumerKey the consumer key issued by the backend store "602368a0e905492fae87697edad14c3a"
userId the consumer’s unique identifier for the user to whom the token was issued "alice"
issuedAt the ISO8601 time at which the token was issued "2012-03-23T10:51:18Z"
ttl the number of seconds after issuedAt for which the token is valid 86400

You may wish the payload to contain other information (e.g. userRole or userGroups) and arbitrary additional keys may be added to the token. This will only be useful if the Annotator client and the SP pay attention to these keys.

Lastly, note that the Annotator frontend does not verify the authenticity of the tokens it receives. Only the SP is required to verify authenticity of auth tokens before authorizing a request from the Annotator frontend.

For reference, here’s a Python implementation of a token generator, suitable for dropping straight into your Flask or Django project:

import datetime
import jwt

# Replace these with your details
CONSUMER_KEY = 'yourconsumerkey'
CONSUMER_SECRET = 'yourconsumersecret'

# Only change this if you're sure you know what you're doing
CONSUMER_TTL = 86400

def generate_token(user_id):
    return jwt.encode({
      'consumerKey': CONSUMER_KEY,
      'userId': user_id,
      'issuedAt': _now().isoformat() + 'Z',
      'ttl': CONSUMER_TTL
    }, CONSUMER_SECRET)

def _now():
    return datetime.datetime.utcnow().replace(microsecond=0)

Now all you need to do is expose an endpoint in your web application that returns the token to logged-in users (say, http://example.com/api/token), and you can set up the Annotator like so:

$(body).annotator()
       .annotator('setupPlugins', {tokenUrl: 'http://example.com/api/token'});

Internationalisation and localisation (I18N, L10N)

Annotator now has rudimentary support for localisation of its interface.

For users

If you wish to use a provided translation, you need to add a link tag pointing to the .po file, as well as include gettext.js before you load the Annotator. For example, for a French translation:

<link rel="gettext" type="application/x-po" href="locale/fr/annotator.po">
<script src="lib/vendor/gettext.js"></script>

This should be all you need to do to get the Annotator interface displayed in French.

For translators

We now use Transifex to manage localisation efforts on Annotator. If you wish to contribute a translation you’ll first need to sign up for a free account at

https://www.transifex.net/plans/signup/free/

Once you’re signed up, you can go to

https://www.transifex.net/projects/p/annotator/

and get translating!

For developers

Any localisable string in the core of Annotator should be wrapped with a call to the gettext function, _t, e.g.

console.log(_t("Hello, world!"))

Any localisable string in an Annotator plugin should be wrapped with a call to the gettext function, Annotator._t, e.g.

console.log(Annotator._t("Hello from a plugin!"))

To update the localisation template (locale/annotator.pot), you should run the i18n:update Cake task:

cake i18n:update

You should leave it up to individual translators to update their individual .po files with the locale/l10n-update tool.

Annotator Roadmap

This document lays out the planned schedule and roadmap for the future development of Annotator.

For each release below, the planned features reflect what the core team intend to work on, but are not an exhaustive list of what could be in the release. From the release of Annotator 2.0 onwards, we will operate a time-based release process, and any features merged by the relevant cutoff dates will be in the release.

Note

This is a living document. Nothing herein constitutes a guarantee that a given Annotator release will contain a given feature, or that a release will happen on a specified date.

2.0

What will be in 2.0

  • Improved internal API
  • UI component library (the UI was previously “baked in” to Annotator)
  • Support (for most features) for Internet Explorer 8 and up
  • Internal data model consistent with Open Annotation
  • A (beta-quality) storage component that speaks OA JSON-LD
  • Core code translated from CoffeeScript to JavaScript

Schedule

The following dates are subject to change as needed.

November 15, 2014 Annotator 2.0 alpha; major feature freeze
December 1, 2014 Annotator 2.0 beta; complete feature freeze
January 15, 2015 Annotator 2.0 RC1; translation string freeze
2 weeks after RC1 Annotator 2.0 final (or RC2 if needed)

The long period between a beta release and RC1 takes account of a) Christmas and the holiday season and b) time for other developers to test and report bugs.

2.1

The main goals for this release, which we aim to ship by May 1, 2015 (with a major feature freeze on Mar 15):

  • Support for selections made using the keyboard
  • Support in the core for annotation on touch devices
  • Support for multiple typed selectors in annotations
  • Support for components which resolve (‘reanchor’) an annotation’s selectors into a form suitable for display in the page

2.2

The main goals for this release, which we aim to ship by Aug 1, 2015 (with a major feature freeze on Jun 15):

  • Support for annotation of additional media types (images, possibly video) in the core

2.3

The main goals for this release, which we aim to ship by Nov 1, 2015 (with a major feature freeze on Sep 15):

  • Improved highlight rendering (faster, doesn’t modify underlying DOM)
  • Replace existing XPath-based selector code with Rangy

API documentation

annotator package

class annotator.Annotator(element[, options])

Annotator represents a reasonable default annotator configuration, providing a default set of plugins and a user interface.

NOTE: If the Annotator is not supported by the current browser it will not perform any setup and simply return a basic object. This allows plugins to still be loaded but will not function as expected. It is reccomended to call Annotator.supported() before creating the instance or using the Unsupported plugin which will notify users that the Annotator will not work.

Examples:

var app = new annotator.Annotator(document.body);
Parameters:
  • element (Element) – DOM Element to attach to.
  • options (Object) – Configuration options.
annotator.Annotator.prototype.destroy()

Destroy the current Annotator instance, unbinding all events and disposing of all relevant elements.

annotator.supported([details=false, scope=window])

Examines scope (by default the global window object) to determine if Annotator can be used in this environment.

Returns Boolean:
 

Whether Annotator can be used in scope, if details is false.

Returns Object:

If details is true. Properties:

  • supported: Boolean, whether Annotator can be used in scope.
  • details: Array of String reasons why Annotator cannot be used.

annotator.core package

class annotator.core.Annotator

Annotator is the coordination point for all annotation functionality. On its own it provides only the necessary code for coordinating the lifecycle of annotation objects. It requires at least a storage plugin to be useful.

annotator.core.Annotator.prototype.addPlugin(plugin)

Register a plugin

Examples:

function creationNotifier(registry) {
    return {
        onAnnotationCreated: function (ann) {
            console.log("annotationCreated", ann);
        }
    }
}

annotator
  .addPlugin(annotator.plugin.Tags)
  .addPlugin(creationNotifier)
Parameters:plugin – A plugin to instantiate. A plugin is a function that accepts a Registry object for the current Annotator and returns a plugin object. A plugin object may define function properties wi
Returns:The Annotator instance, to allow chained method calls.
annotator.core.Annotator.prototype.destroy()

Destroy the current instance

Destroys all remnants of the current AnnotatorBase instance by calling the destroy method, if it exists, on each plugin object.

Returns Promise:
 Resolved when all plugin destroy hooks are completed.
annotator.core.Annotator.prototype.runHook(name[, args])

Run the named hook with the provided arguments

Returns Promise:
 Resolved when all over the hook handlers are complete.
annotator.core.Annotator.prototype.setAuthorizer(authorizerFunc)

Set the authorizer implementation

Parameters:authorizerFunc (Function) – A function returning an authorizer component. An authorizer component must implement the Authorizer interface.
Returns:The Annotator instance, to allow chained method calls.
annotator.core.Annotator.prototype.setIdentifier(identifierFunc)

Set the identifier implementation

Parameters:identifierFunc (Function) – A function returning an identifier component. An identifier component must implement the Identifier interface.
Returns:The Annotator instance, to allow chained method calls.
annotator.core.Annotator.prototype.setNotifier(notifierFunc)

Set the notifier implementation

Parameters:notifierFunc (Function) – A function returning a notifier component. A notifier component must implement the Notifier interface.
Returns:The Annotator instance, to allow chained method calls.
annotator.core.Annotator.prototype.setStorage(storageFunc)

Set the storage implementation

Parameters:storageFunc (Function) – A function returning a storage component. A storage component must implement the Storage interface.
Returns:The Annotator instance, to allow chained method calls.
annotator.core.Annotator.extend(object)

Create a new object which inherits from the Annotator class.

annotator.storage package

annotator.storage.DebugStorage()

DebugStorage is a storage component that can be used to print details of the annotation persistence processes to the console when developing other parts of Annotator.

annotator.storage.NullStorage()

NullStorage is a no-op storage component. It swallows all calls and does the bare minimum needed. Needless to say, it does not provide any real persistence.

class annotator.storage.HTTPStorageImpl([options])

HTTPStorageImpl is a storage component that talks to a simple remote API that can be implemented with any web framework.

Parameters:options (Object) – Configuration options.
annotator.storage.HTTPStorageImpl.prototype.create(annotation)

Create an annotation.

Examples:

store.create({text: "my new annotation comment"})
// => Results in an HTTP POST request to the server containing the
//    annotation as serialised JSON.
Parameters:annotation (Object) – An annotation.
Returns jqXHR:The request object.
annotator.storage.HTTPStorageImpl.prototype.update(annotation)

Update an annotation.

Examples:

store.update({id: "blah", text: "updated annotation comment"})
// => Results in an HTTP PUT request to the server containing the
//    annotation as serialised JSON.
Parameters:annotation (Object) – An annotation. Must contain an id.
Returns jqXHR:The request object.
annotator.storage.HTTPStorageImpl.prototype.delete(annotation)

Delete an annotation.

Examples:

store.delete({id: "blah"})
// => Results in an HTTP DELETE request to the server.
Parameters:annotation (Object) – An annotation. Must contain an id.
Returns jqXHR:The request object.
annotator.storage.HTTPStorageImpl.prototype.query(queryObj)

Searches for annotations matching the specified query.

Parameters:queryObj (Object) – An object describing the query.
Returns Promise:
 Resolves to an object containing query results and meta.
annotator.storage.HTTPStorageImpl.prototype.setHeader(name, value)

Set a custom HTTP header to be sent with every request.

Examples:

store.setHeader('X-My-Custom-Header', 'MyCustomValue')
Parameters:
  • name (string) – The header name.
  • value (string) – The header value.
annotator.storage.HTTPStorageImpl.options

Available configuration options for HTTPStorageImpl.

annotator.storage.HTTPStorageImpl.options.emulateHTTP

Should the plugin emulate HTTP methods like PUT and DELETE for interaction with legacy web servers? Setting this to true will fake HTTP PUT and DELETE requests with an HTTP POST, and will set the request header X-HTTP-Method-Override with the name of the desired method.

Default: false

annotator.storage.HTTPStorageImpl.options.emulateJSON

Should the plugin emulate JSON POST/PUT payloads by sending its requests as application/x-www-form-urlencoded with a single key, “json”

Default: false

annotator.storage.HTTPStorageImpl.options.headers

A set of custom headers that will be sent with every request. See also the setHeader method.

Default: {}

annotator.storage.HTTPStorageImpl.options.onError

Callback, called if a remote request throws an error.

annotator.storage.HTTPStorageImpl.options.prefix

This is the API endpoint. If the server supports Cross Origin Resource Sharing (CORS) a full URL can be used here.

Default: '/store'

annotator.storage.HTTPStorageImpl.options.urls

The server URLs for each available action. These URLs can be anything but must respond to the appropriate HTTP method. The URLs are Level 1 URI Templates as defined in RFC6570:

class annotator.storage.StorageAdapter(store, runHook)

StorageAdapter wraps a concrete implementation of the Storage interface, and ensures that the appropriate hooks are fired when annotations are created, updated, deleted, etc.

Parameters:
  • store – The Store implementation which manages persistence
  • runHook (Function) – A function which can be used to run lifecycle hooks
annotator.storage.StorageAdapter.prototype.create(obj)

Creates and returns a new annotation object.

Runs the ‘beforeAnnotationCreated’ hook to allow the new annotation to be initialized or its creation prevented.

Runs the ‘annotationCreated’ hook when the new annotation has been created by the store.

Examples:

registry.on('beforeAnnotationCreated', function (annotation) {
    annotation.myProperty = 'This is a custom property';
});
registry.create({}); // Resolves to {myProperty: "This is a…"}
Parameters:annotation (Object) – An object from which to create an annotation.
Returns Promise:
 Resolves to annotation object when stored.
annotator.storage.StorageAdapter.prototype.update(obj)

Updates an annotation.

Runs the ‘beforeAnnotationUpdated’ hook to allow an annotation to be modified before being passed to the store, or for an update to be prevented.

Runs the ‘annotationUpdated’ hook when the annotation has been updated by the store.

Examples:

annotation = {tags: 'apples oranges pears'};
registry.on('beforeAnnotationUpdated', function (annotation) {
    // validate or modify a property.
    annotation.tags = annotation.tags.split(' ')
});
registry.update(annotation)
// => Resolves to {tags: ["apples", "oranges", "pears"]}
Parameters:annotation (Object) – An annotation object to update.
Returns Promise:
 Resolves to annotation object when stored.
annotator.storage.StorageAdapter.prototype.delete(obj)

Deletes the annotation.

Runs the ‘beforeAnnotationDeleted’ hook to allow an annotation to be modified before being passed to the store, or for the a deletion to be prevented.

Runs the ‘annotationDeleted’ hook when the annotation has been deleted by the store.

Parameters:annotation (Object) – An annotation object to delete.
Returns Promise:
 Resolves to annotation object when deleted.
annotator.storage.StorageAdapter.prototype.query(query)

Queries the store

Parameters:query (Object) – A query. This may be interpreted differently by different stores.
Returns Promise:
 Resolves to the store return value.
annotator.storage.StorageAdapter.prototype.load(query)

Load and draw annotations from a given query.

Runs the ‘load’ hook to allow plugins to respond to annotations being loaded.

Parameters:query (Object) – A query. This may be interpreted differently by different stores.
Returns Promise:
 Resolves when loading is complete.

annotator.authorizer package

class annotator.authorizer.DefaultAuthorizer([options])

Default authorizer

Parameters:options (Object) –

Configuration options.

  • userId: Custom function mapping an identity to a userId.
annotator.authorizer.DefaultAuthorizer.prototype.permits(action, annotation, identity)

Determines whether the user identified by identity is permitted to perform the specified action on the given annotation.

If the annotation has a “permissions” object property, then actions will be permitted if either of the following are true:

  1. annotation.permissions[action] is undefined or null,
  2. annotation.permissions[action] is an Array containing the userId of the passed identity.

If the annotation has a “user” property, then actions will be permitted only if the userId of identity matches this “user” property.

If the annotation has neither a “permissions” property nor a “user” property, then all actions will be permitted.

Parameters:
  • action (String) – The action the user wishes to perform
  • annotation (Object) –
  • identity – The identity of the user
Returns Boolean:
 

Whether the action is permitted

annotator.authorizer.DefaultAuthorizer.prototype.userId(identity)

A function for mapping an identity to a primitive userId. This default implementation simply returns the identity, and can be used with identities that are primitives (strings, integers).

Parameters:identity – A user identity.
Returns:The userId of the passed identity.

annotator.identifier package

annotator.identifier.Default(identity)

Default identifier implementation.

Parameters:identity – The identity to report as the current user.
Returns:A function returning an identifier object.

annotator.notifier package

class annotator.notifier.BannerNotifier(message[, severity=notifier.INFO])

BannerNotifier is simple notifier system that can be used to display information, warnings and errors to the user.

Parameters:
  • message (String) – The notice message text.
  • severity – The severity of the notice (one of notifier.INFO, notifier.SUCCESS, or notifier.ERROR)
annotator.notifier.BannerNotifier.prototype.close()

Close the notifier.

Returns:The notifier instance.

Indices and tables