Interactive Editor Plugin Guide

Overview

Version 2.0 of the template framework unifies all types of templates into a common, programmable structure that allows developers to create and contribute templates that can be directly embedded in the authoring tool. Currently, the item data are created via the portal the corresponding templates are embedded in the content during authoring.

This doesn't give an author a lot of control over configuring the template, and its behavior. Moreover, it is also limited to the data available as part of an item. The template v2.0 frameworks make it possible for templates to be directly configured from within the authoring tool. Developers can write the custom runtime logic, custom configuration editor, and custom editing experience (WYSIWYG) to make it easier for authors to discover and reuse these templates

These templates can include informational aspects such as a Flashcard that presents the word details or include engaging tasks such as exploring a noun and its related nouns, verbs, etc. Such templates are used for enabling learning tasks - Reading, Writing/Scribbling, Recording, Exploring, Interacting, and so on. The actual templates can range from simple single stage tasks (flashcard, scribble pad, recorder activity) to complex multi-stage tasks that guide the learner through a pre-configured learning activity.

The following aspects of extensions in the authoring tool are described:

Registration and lifecycle

Registering a plugin associates a class (prototype) with a custom plugin object. The element provides callbacks to manage its lifecycle. Uses behaviors to share code.

The plugins core is implemented in its plugin.js file. The file is loaded by the plugin manager and initialized during the load time. The plugins themselves are instantiated when the user creates that object in the editor.

Namespaces: Proposed namespace is editor.* for Editor plugins, renderer.* for Renderer plugins. The community contributed plugins should follow the same convention - org.editor.* and org.renderer.*

var editor.Plugin = Class.extend({
    initialize: function(data) {}, // When the plugin is registered, it can set up its dependencies
    properties: function(data) {}, // The attributes of the plugin
    converter: function(instance) {}, // returns the converter object for this plugin
    editor: function(instance) {}, // returns the canvas editor object for the given instance
    views: function(instance) {}, // returns the views associated with this plugin
    behaviors: function(instance) {}, // returns the behavior mixins for the given instance
    create: function(data) {}, // Instantiate an object of the plugin's type (e.g on canvas)
    remove: function(data) {} // When the object of the plugin's type is deleted from canvas
// TODO - base class methods
})
Properties

For content plugins only, the properties exposed by the plugin are declared. This allows the plugin to inherit the default behaviors related to rendering the properties. Properties are declared by the plugin as follows:

[
    {
        "propertyName": "", // Name of the property
        "title": "", // Display label
        "description": "", // Tooltip text
        "category": "", // Category of the property for rendring        
        "dataType": "Text", // Data type (number, text, word, wordlist, color)
        "encoding": "", // How the property is saved (attribute, json, cdata)
        "required": true,
        "displayProperty": "Editable",
        "defaultValue": "",    
        "renderingHints": "{'editor': 'colorpicker'}"
    },
    ... // other properties
]

The base plugin declares the default properties for all plugins:

  1. Bounding Box - x, y, w, h

  2. Border - stroke (border color), stroke-width (border size)

  3. Shadow - shadow, offsetX, offsetY, blur

Each custom plugin can extend the properties list and add its own declarable properties. Any other config is generated and handled directly by the plugin and added to its CDATA section config.

Converter & Parser

Converter modifies the content DOM tree and adds the current instance to the DOM. Any cross-plugin modifications (e.g. horizontal concerns) should not be implemented as converters, but as converter behavior that can receive and apply across multiple instances.

var editor.Converter = Class.extend({
    // Converts the current object to ECML. Passes the ECML DOM object
    toECML: function(instance, dom) {}
    // Converts the ECML node into the appropriate plugin object
    fromECML: function(node, dom) {}
})
Editing

Editor is the base class for all canvas editor plugins. The base class extends the Fabric.js rendering system and provides default functionality to render the editor object on canvas. Default for any editor shows a thumbnail for that object on the canvas.

var editor.Editor = Class.extend({
    // Returns the native fabric object to render this plugin's editor
    canvasObject: function(instance) {}
})
View

View is a visual widget (non-canvas) that is placed in the layout. View is instantiated by the plugin and is manipulated by its behaviors. Any actions within the view are handled by its own handlers. It may or may not fire events for its actions. The view is expected to be bound to its object and update the state internally.

var editor.View = Class.extend({
    // Renders the view
    render: function(instance) {}
})
Actions and Events

Actions are executables that are defined by a plugin. Action can be bound to a toolbar, menu bar etc. When the action is clicked, the callback method is called allowing the action to perfom its task. In addition, the action also fires the custom action event to allow any other behaviors to respond to the action. Clicking the action would generally show/hide a view, or add/edit state on the canvas.

var editor.Action = Class.extend({
    // Returns the metadata about the action that is handled by the toolbar
    descriptor: function(instance) {},
    // Renders the action on the toolbar. The default action renderer renders the action on toolbar
    // based on the metadata descriptor. Only if the action requires custom rendering,
    // would a plugin need to override the render function
    render: function(instance) {}
    //
})

Action Data

[
    {
    "icon": "", // Name of the property
    "title": "", // Display label
    "description": "", // Tooltip text
    "category": "", // Category of the property for rendring
    "state": "", // Current state of the action (e.g. selected tool)
    "enabled": true, // Whether the action in toolbar is enabled or not
    "visible": true, // Whether the action in toolbar is visible or not
    "type": "" // Type of action (button, dropdown, dropdown with selection, custom)
    "view": object // Show the view object on click of this action
    "event": "" // Custom event name for this action
    "handler": function() {} // Handler function reference to call on click
    },
    ... // other properties
]

Note: There may be a need to bind the action to some specific property. E.g. when we put a color selector action, bind it to the color property automatically. Need to think this through.

Behaviors

The basic interfaces are as follows:

Text Provider

Text providers are plugins that expose a textual representation of their content. This allows the authoring tool and other plugins to work with the text and leverage the language platform (e.g. to work with the parser API).

var editor.SamplePlugin = editor.Behavior.extend({
    text: function(data) {}, // Returns a plain-text extract of the object
    // E.g. a Read-Along plugin can return the text value of the contents
    // A complex plugin could return a concatenated string representation
});

State Listener

Listener for state in the authoring tool. The callback pattern allows the behavior to listen for events while the action is happening (e.g. rotating), or after the action is completed (e.g. rotated). If the action is cancelled, the end event is called with state=CANCELLED. Understandably, the progress events are called on every tick while the end event is called once per action (on finished or cancelled).

var editor.SamplePlugin = editor.Behavior.extend({
    selected: function(instances[], state) {}, // Called when the object(s) is selected
    exited: function(instances[], state) {}, // Called when selection is ended
    editing: function(instances[], state) {}, // Called when the object is being edited (config view i
    changed: function(instances[], state, data) {}, // Called after the editing is completed
    moving: function(instances[], state) {}, // Called when object(s) are being moved
    moved: function(instances[], state) {}, // Called after the move is completed
    resizing: function(instances[], state) {}, // Called when selection is resizing
    resized: function(instances[], state) {}, // Called when resizing is completed
    rotating: function(instances[], state) {}, // Called when selection is being rotated
    rotated: function(instances[], state) {}, // Called when selection is rotated
});

Clipboard Listener

Listener for clipboard events - when an object is copied on the clipboard or when it is pasted.

var editor.SamplePlugin = editor.Behavior.extend({
    // Called when the selected objects are being copied
    // (allows plugin to control the state it copies to clipboard)
    copy: function(instances[], state) {},
    // Called after the objects are copied to clipboard
    // (e.g. allows plugins to enable the paste button)
    afterCopy: function(instances[], state) {},
    // Called before paste
    // (e.g. allows plugins to transform data before pasting)
    // (e.g. read-along was copied to clipboard, when pasting in text, its text is copied)
    paste: function(instances[], state) {},
    // Called after the paste is completed
    // (e.g. allows plugins to respond to the paste operation)
    // (e.g. invoke the language parse API after text is pasted)
    afterPaste: function(instances[], state, data) {}
});

Action Listener

Listener for custom actions. E.g. when read-along editing is completed, it fires the event, that is handled by the appropriate listener.

var editor.SamplePlugin = editor.Behavior.extend({
    // Called after the custom action has finished executing
on: function(event, state) {}
});

Note: This mechanism doesn't allow listeners to prevent the action from happening (there is no "beforeXX" event callback and no way defined to cancel an event). This is intentional. If an action has to be disabled based on some condition, the listener should observe the condition and disable the action itself (prevent the user from taking the action rather than trying to cancel it).

Reusable Views/Plugin Artifacts

  1. PropertyConfigFormView - Popup that generates a form based on properties data. For most simple widgets, this form view might be sufficient to configure them

  2. DefaultButtonAction - Default toolbar action (simple button).

  3. WordSelectorView - Uses APIs and provides UI to select a single word from repo

  4. WordlistSelectorView - Uses APIs and provides a UI to select multiple words from repo

Property Observer

Handles the property change notifications for the plugins

var editor.SamplePlugin = editor.Behavior.extend({
    changed: function(property, data) {}, // Called when a property value is modified
});
Examples/Illustrations

1. Read-Along Widget (Content Plugin)

Plugin.js

var editor.ReadAlongPlugin = editor.TextPlugin.extend({
    initialize: function() {
        // Instantiate ReadAlongConfigAction
        var data = {
        "name": "Attach Read-Along",
        "icon": "fa-link",
        "view": "ReadAlongConfigView"
        }
        var action = new DefaultButtonAction(data);
        // Add the action to ContextToolbar, for all TextPlugins and their derivatives
        toolbarManager.add(ContextToolbar, action, "TextPlugin");
        },
        // INHERITED - Reuses available primitive text editor on canvas
        editor: function() {
        return new TextCanvasEditor(this);
        }
        // Custom view that configures the read-along text
        views: function() {
        return new ReadAlongConfigView();
    }// Converter functions
    // -------------------
    // Produces the following output
    //
    // <htext xywh color...>
    //
    <cdata[[read along config]]>
    // </htext>
    //
    toECML: function(instance, dom) {}
    // Parses the htext cdata section
    fromECML: function(node, dom) {}
    // Behaviors
    // ----------------------
    // INHERITED - returns the plain text value of the text plugin
    text: function(data) {},
})

ReadAlongConfigView.js

var editor.ReadAlongConfigView = editor.View.extend({
// Renders the read along creator view, sets the configuration of the read-along on the
// instance object passed to it
// If the view is launched on a TextPlugin object, replace it with the ReadAlongPlugin object
render: function(instance) {}
})

2. Editor Extension - Word Complexity Provider

plugin.js

var editor.WordComplexityPlugin = editor.Plugin.extend({
    initialize: function() {
    // Instantiate WordComplexityView
    // Register event listeners
    // Register word complexity listener for all TextPlugin objects
    // Read-along also inherits from TextPlugin (all text plugin and its derivatives)
    eventManager.register("TextPlugin", new WordComplexityListener());
    },
    // Custom view that configures the read-along text
    views: function() {
    return new WordComplexityView(); // TODO: Configure where the view docks?
    }
})

WordComplexityListener.js

var editor.SamplePlugin = editor.Behavior.extend({
    selected: function(instances[], state) {
    // If parsed result of the instance available, activate view with the parsed results
    // If not, make API call for parse API
    },
    exited: function(instances[], state) {
    // Hide the view
    },
    changed: function(instances[], state, data) {
    // Fire the API call, cache the results for this instance
    },
});

WordComplexityView.js

var editor.WordComplexityView = editor.View.extend({
    // Renders the sidebar view
    render: function(instance) {}
})

3. ECML Template

Example of a template that has read-along and recorder widgets on the same page.

<theme>
    <manifest>
    <!-- All the assets used in the read-along or recorders (e.g. recorder icon), plugins -->
    </manifest>
    <template id="xxx">
        <htext>Click here to edit</htext>
        <recorder>...</recorder>
    </template>
</theme>

plugin.js

var editor.DefaultTemplatePlugin = editor.Plugin.extend({
    initialize: function() {
    // Instantiate DefaultTemplateSelectorView
    },
    // Custom view that allows user to select templates
    views: function() {
    return new DefaultTemplateSelectorView();
    }
    // Editor - Since the template consists of other elements, they are available for editing
    // via their respective plugins
    // TODO - PluginManager needs to discover embedded plugins via their ECML name, and then
    // load them (invoke the plugin lifecycle)
})

DefaultTemplateSelectorView.js

var editor.DefaultTemplateSelectorView = editor.View.extend({
    // Renders the template selector popup
    // Upon selection of a template, calls an internal merge function
    render: function(instance) {}
    // Takes the selected template body, and merges its manifest with the ECML DOM
    // Adds template DOM contents on the current stage for that template
    merge: function(instance) {}
})

Last updated