Skip to content

Latest commit

 

History

History
164 lines (123 loc) · 5.53 KB

howto-write-plugin.md

File metadata and controls

164 lines (123 loc) · 5.53 KB

How To Write A Plugin

An overview of the plugin API is described in doc-plugin

The following example describes an implementation for the SelectionPlugin in Typescript. The goal is to make an editor selectable, so it is highlighted and exposes an selection-hook for further consumption. You can refer to the full implementation in SelectionPlugin.

Using Typescript, we start defining our plugin-options type to expose our selection-hooks

type SelectionPluginOptions = {
  /* when an editor is selected, onSelect is called with the location pointer, editor instance and its options */
  onSelect({ pointer, editor }): void;
  /* called when an editor is deselected */
  onDeselect({ pointer, editor }): void;
}

and introduce a class for our custom plugin taking the options

class SelectionPlugin implements Plugin {
  id = "SelectionPlugin"; // add an id to identify this plugin
  options: SeletionPluginOptions;
  editron: Editron
  
  constructor(options: SelectionPluginOptions) {
    this.options = options; // store passed in hooks for later notifications
  }

  initialize(editron: Editron) {
    this.editron = editron; // store editron instance for later access
  }
}

With this, we can add the SelectionPlugin with our SelectionPluginOptions to editron, which will do nothing yet:

new Editron(schema, data, {
  plugin: [
    new SelectionPlugin({
      onSelect: ({ pointer }) => console.log("select editor at", pointer),
      onDeselect: ({ pointer }) => console.log("deselect editor at", pointer)
    })
  ]
});

Adding Selection Behaviour

Next, we add our selection logic. We track an active element, manage a css-class selected and call our hooks onSelect and onDeselect.

class SelectionPlugin implements Plugin {

  // ...
  options: SeletionPluginOptions;

  // track the currently selected editor instance
  currentSelection: Editor;

  deselect() {
    // stop here, if we have no active selection
    if (this.currentSelection == null) return;

    const editor = this.currentSelection;
    // remove the selection flag from the editors root DOM element
    editor.getElement().classList.remove("selected");
    // call the registered user hook
    this.options.onDeselect({ pointer: editor.getPointer(), editor });
    // and remove the stored selection
    this.currentSelection = null;
  }

  select(editor: Editor) {
    // stop here, if this editor is already selected
    if (this.currentSelection === editor) return;

    // deselect any current active editor
    this.deselect();
    // add a selected flag on the editors root DOM element
    editor.getElement().classList.add("selected");
    // store the editor as selected
    this.currentSelection = editor;
    // and call the registered user hook
    this.onSelect({ pointer: editor.getPointer(), editor });
  }
}

Now that we have a basic Plugin and selection behaviour, we need to register our methods to an event-listener.

Tracking An Instance And Event-Listeners

We will add an event-listener for each editor, so we store a function directly on each editor-instance for later removal. In order to track editor instances that are registered to our selection-plugin, we extend editor instances by adding a configuration field directly on each editor:

interface ModifiedEditor extends Editor {
    __selectionPlugin?: {
        select(event: MouseEvent) => void;
    }
}

This gives us the ability (within Typescript) to store properties onto an editor. Using the onCreateEditor-hook, we store our event-listener for later reuse.

class SelectionPlugin implements Plugin {
  
  // ...
  
  onCreateEditor(pointer, editor: ModifiedEditor, options?): void {
    // only register editors, that are configured as selectable, 
    // for example in a json-schema: { "editron:ui": { selectable: true } }
    if (options?.selectable == null) {
      return;
    }
    
    // store our plugin information on this instance
    editor.__selectionPlugin = {
      // add an event-listener, that we can access again later
      select: (event: MouseEvent) => {
        event.stopPropagation();
        this.select(editor);
    };

    // and finally register the event listener
    editor.getElement().addEventListener("click", editor.__selectionPlugin.select);
  }
}

Being good citizens, we want to remove all custom data and listeners when an editor instance is removed. Therefore we watch the onDestroyEditor-hook for cleanup

  onDestroyEditor(pointer, editor: ModifiedEditor): void {
    // stop here, if this is not a tracked editor instance
    if (editor.__selectionPlugin == null) return;

    // remove the event-listener
    editor.getElement().removeEventListener("click", editor.__selectionPlugin.select);
    // remove our custom properties
    editor.__selectionPlugin = undefined;
  }

Note In case we are relying on an editor's json-pointer, we should add the onChangePointer-hook, which is described in doc-plugin. For an implementation example, refer to the SortablePlugin.

Finally, we add the missing deselect hook and are done implementing the plugin

  initialize(controller: Editron): Plugin {
      this.controller = controller;

      // listen to clicks on document for deselecting any editors
      document.body.addEventListener("click", () => this.deselect());
  }

See the full implementation in SelectionPlugin.