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.
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
andMirador.ImageView
to do what they normally do, but also, to listen for instruction fromMirador.Window
to disable/enable zooming, and to modify their OSD instance accordingly.
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 useFunction.prototype.bind
.
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.
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.
This is where it all comes together. Here's what happens in this plugin's init
function:
- The translations for each locale resource defined in the
locales
property are loaded. - 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.
(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:
-
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
andlistenForActions
, which will be set toMirador.Window
andMirador.Window.prototype.listenForActions
, respectively. -
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 employedFunction.prototype.apply
to call the "old version" of the method and added an additional call toMirador.EventEmitter.subscribe
.Adding new methods is easy; in this example, see
toggleZoomLock
. -
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!
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.