Skip to content

Instantly share code, notes, and snippets.

@markmatney
Last active July 31, 2020 05:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markmatney/795a65e294a3ad1bcff3bd53eec4120b to your computer and use it in GitHub Desktop.
Save markmatney/795a65e294a3ad1bcff3bd53eec4120b to your computer and use it in GitHub Desktop.
a walkthrough of a simple plugin for the Mirador viewer

Creating a simple plugin for Mirador 2.x

In an effort to help address one of the primary pain points of working with Mirador (namely, safely modifying or extending the core feature set while keeping complexity down), we've taken cues from other institutions and wrote a plugin for a very simple feature that's implemented somewhere in our fork, allowing anyone to add the feature to their Mirador 2.x instance without modifying the core source files at all.

This work is based on that of the Bavarian State Library who have previously written plugins for Mirador, all of which can be found here. This plugin's source code is at https://github.com/UCLALibrary/mirador-disable-zoom. Please see that repository for installation instructions and information on the plugin's use case. You'll find some helpful comments within the source files, nearly all of which are duplicated and elaborated upon here.

Overview

In order to implement the "disable zoom button" feature, we need to do the following:

  • Add a Mirador.Window-level button that a user can click to disable/enable zooming within that window. The button should be emboldened when zooming is disabled.
  • Tell Mirador.Window to do what it normally does, but also, whenever the user clicks this new button, to tell the (underlying) active view (e.g. ImageView, BookView) to tell its OpenSeadragon (OSD) instance to stop zooming.
  • Tell Mirador.BookView and Mirador.ImageView to do what they normally do, but also, to listen for instruction from Mirador.Window to disable/enable zooming, and to modify their OSD instance accordingly.

Walkthrough

The global scope

At the top level, the script declares an object that contains all of the plugin code, and then calls its init method:

var MiradorPlugin = {

    locales: { ... },
    
    template: Mirador.Handlebars.compile( ... ),
    
    init: function() { ... }
    
};

$(document).ready(function() {
    MiradorPlugin.init();
});

NOTE:

  • These property names are modeled after conventions used in the Mirador source code. You can name them __whateverYouWant__.
  • This script assumes that Mirador has already been loaded, i.e., that the names Mirador, jQuery/$, i18next, etc. are defined.
  • We use the convention var _this = this; (as does Mirador) to access object properties inside nested functions, but you can also use Function.prototype.bind.

MiradorPlugin.locales

The value of the locales property is a JSON object formatted like the value of the resources property shown here. In this case, there is only one locale-dependent string: the text that displays when a user hovers the mouse over the button we're about to add (tooltip). Plugins should strive to support all of the locales that Mirador supports.

MiradorPlugin.template

Currently, Mirador uses Handlebars to render HTML; each module that adds something to the view has a template property that contains a compiled Handlebars template. When a module's init function is called, this template (a function) is called (sometimes with no arguments, other times with a configuration object), and the return value is appended to the DOM.

MiradorPlugin.init

This is where it all comes together. Here's what happens in this plugin's init function:

  1. The translations for each locale resource defined in the locales property are loaded.
  2. Core Mirador modules are altered to implement the plugin.

Step 0 just requires registering a callback on the onInitialized event that loops through the locales dictionary.

Altering core Mirador modules

(NOTE: Readers will find it helpful to follow along in the plugin source code while reading this section.)

To implement your feature, you'll need to do any number of several things to any number of the core modules (Mirador.Window, Mirador.Workspace, etc.), including:

  • override constructors
  • override methods
  • add new methods
  • register PubSub events (same effect as adding code to listenForActions)
  • register event handlers on user interaction (same effect as adding code to bindEvents)

To do this without modifying the core source files and without breaking things requires some cleverness. I'll use my work with Mirador.Window as an example. I've annotated the source code with comments of the form /* N. ( ... ) */ which denote sections of the code where I am doing each step of the following list.

For each module that we want to modify in some way:

  1. Declare variables for the constructor and any methods that we'll override.

    This allows us to save and refer to the "old version" of these functions when we re-bind their names. In this example, I've declared the variables constructor and listenForActions, which will be set to Mirador.Window and Mirador.Window.prototype.listenForActions, respectively.

  2. Override methods and register (and document!) new ones.

    Now that we've saved the "old version" of each method we plan to override, we can override them. For example, for our "new version" of listenForActions, we first want the "old version" of that function to execute, followed by some additional code that we define in order to implement our feature. In this example, I've employed Function.prototype.apply to call the "old version" of the method and added an additional call to Mirador.EventEmitter.subscribe.

    Adding new methods is easy; in this example, see toggleZoomLock.

  3. Override the constructor.

    In this constructor, I call the "old version" of the constructor (passing it the original configuration object, extended with an additional property that keeps track of whether or not zooming is disabled). After that, I compile the button template and attach it to the DOM, and register an event listener on the button.

    IMPORTANT: Be sure to re-assign the prototype after this step so that other plugins can access it!

Summary

This project has given me a few questions to think about:

  • What would a generalized plugin authoring process for Mirador look like?
    • Best practices: can a generic framework be devised? Something fork-able or subclass-able?
    • Publishing and discovery: is hosting publicly on GitHub and tagging the repo with something like mirador-plugins sufficient? What about NPM/Bower? mirador-awesome?
    • Installation: some Grunt/Gulp task to build Mirador with a list of chosen plugins?
    • Testing: package plugins with Mirador as a devDependency? How to test interaction between plugins?
  • How does/would the level of effort required to ship extended Mirador functionality in the form of a plugin compare to that of modifying the core source files themselves? How do we determine which is the better route for any given feature?
  • What changes need to happen to the Mirador core in order to better support a plugin ecosystem?

That's it! I hope this is helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment