Package

This text will explore the Package file used to define the parts included in, and used by a Writer plugin.

Plugin Structure

A plugin is a collection of many different resources, and the Package file is that collection's bootstrap. Label definitions, node- and converter-registration, adding validatiors, and registering tools and commands, are all placed in the Package file, using the Writer's NPWriterConfigurator-class.

The Package file also contains the plugin's name and id.

Example Plugin Structure

/ se.mydomain.myplugin
  / components
    MyPluginComponent.js
    ChildComponent.js
  MyPluginNode.js
  MyPluginConverter.js 
  MyPluginPackage.js
  index.js

In this example, index.js is the plugin's entrypoint. It's here where the Package file is imported and the plugin is registered in the Writer.

MyPluginPackage.js contains information about the plugin, and registers MyPluginNode.js as a substance node, MyPluginConverter.js as a NewsML-converter, and MyPluginComponent.js as the component used to present MyPluginNode.js.

Depending on the type of plugin, the Package will contain different definitions and registrations.

MyPluginPackage.js

import './scss/link.scss' // Provide an entrypoint for the plugin's style
import MyPluginNode from './MyPluginNode'
import MyPluginConverter from './MyPluginConverter'
import MyPluginComponent from './components/MyPluginComponent'

export default {
    name: 'my-plugin',
    id: 'se.mydomain.myplugin',
    configure: function (configurator, pluginConfig) {

        configurator.addNode(MyPluginNode)
        configurator.addComponent(MyPluginNode.type, MyPluginComponent)
        configurator.addConverter('newsml', MyPluginConverter)

        configurator.addLabel('Hello World', {
            sv: 'Tjena Världen'
        })
    }
}

This example Package file contains definitions for a Node, a Component linked to the Node's type-property, a Converter to import and export the Node-data using NewsML. It also adds a label which can be used within the plugin's components.

To learn more about Nodes, Converters, and Components, have look at their respective documents.

The configurator object

As seen in the Package example above, the supplied configurator-object is used to add different resources used in the plugin. Below are relevant configurator-functions which are useful when developing a plugin.

Common Add-functions

addNode(NodeClass) Registers a node in Substance configuration for use later.

addComponent(type, ComponentClass) Registers a class which extends Substance's Component-class for a Node type. A Component in this instance is a UI representation of a Node class. Only on Component can be registered to one specific type.

addConverter(converterObject) Registers a converter object for a specific output type. A converter is an object used for exporting and importing the data stored in a Node object.

addLabel(label, { translations }) Adds a localized label string which can be fetched in the plugin's components using this.getLabel(string), assuming that the component is extending Substance's Component-class. The first string property is used as fallback if none of the supplied translations are found.

Label Example:

configurator.addLabel('Hello World', {
    sv: 'Hejsan Världen'
})

this.getLabel('Hello World') 
// If the Writer's configured "labelLanguage" property is set 
// to "sv", "Hejsan Världen" is returned, any other language
// will return "Hello World"

addIcon(iconName, options) Registers an icon from FontAwesome, or another registered icon provider for use in the plugin. Example:

configurator.addIcon('my-icon', {'fontawesome': 'fa-external-link'})

$$(Button, {
    icon: 'my-icon'
}).on('click', () => { console.info('Foobar') })

addKeyboardShortcut(combo, spec, globalCombo, description = '') Add keyboard shortcut and connect to a Command.

Keyboard Shortcut Example:

  // configure function in a Package file

  const combo = {
    standard: {
        'default': 'ctrl+d',
        'mac': 'cmd+d'
    }
  }

  configurator.addKeyboardShortcut(combo, {command: 'my-amazing-command'}, true, 'Runs my amazing command in the global context.')

addValidator(Validator) Adds a class which inherits from Validator to validate the Article's content and metadata before saving.

Validator Example:

import {Validator, api} from 'writer'

class MyHeadlineValidator extends Validator {

    constructor(...args) {
        super(...args)
    }

    /**
     * Main validation method
     */
    validate() {
        this.validateHeadline()
    }

    /**
     * Ensure that a valid headline exists
     */
    validateHeadline() {
        const headlines = this.newsItem.querySelectorAll('idf > group element[type="headline"]')
        const headline = headlines[0].childNodes.length === 0 ? '' : headlines[0].firstChild.textContent.trim()

        if (headlines.length === 0 || headline === '') {
            this.addError(
                api.getLabel('Headline is missing or empty!')
            )
        }
    }
}

export {MyHeadlineValidator}

addToSidebar(tabId, pluginConfig, ComponentClass) Registers a Component to be rendered in the Sidebar. tabId is a string, which is the id of the tab, but also the label which is used for the tab. If the tab does not exist, it will be created, and the Component added to this new tab.

addTopBarComponent(id, def, component) Registers a Component to be rendered in the Top bar. The parameter def is an object which contains alignment, whether the Component is placed on the left, or right, side of the top bar.

Topbar Example:

configurator.addTopBarComponent(
    'my-amazing-topbar-component',
    {
        align: 'left' // Or 'right'
    },
    MyAmazingTopBarComponent
)

addPopover(id, def, component) Similar to addTopBarComponent, this function adds an icon in the Top bar which, when clicked opens a popover overlay with its component rendered inside.

Popover Example:

configurator.addPopover(
    'my-amazing-popover',
    {
        icon: 'fa-smiley',
        align: 'right',
        sticky: true    // Set to `true` to keep the popover open when clicking somewhere else, default `false`
    },
    MyAmazingComponent
)

Tools and Commands

Adding a Tool is a simple way to add a button, or some text, which can execute a Command when clicked. The only difference between the add***Tool()-functions is where in the Writer the Tool is added. When adding a Tool, it needs the name of the Command (which was previously registered) it should be bound to, and a class which is used to render the Tool. For a visual representation of all the Writer sections, see the Plugin Overview.

Tool class Example:

import {Tool} from 'substance'
import {UIIcon} from 'writer'


class MyAmazingTool extends Tool {

    render($$) {
        return $$('div').attr('title', this.getLabel('Cheers you up'))
            .append(
                $$('button').addClass('se-tool').append($$(UIIcon, {
                    name: 'check-bold'
                }))
                    .on('click', this.executeCommand) // This will execute the command which name was used to register the Tool 
            )
      }
}

export {MyAmazingTool}

addCommand(name, CommandClass, options) Adds a Command which can be executed by a Tool, or manually using EditorSession. In its simplest form, a Command contains the logic for a Tool.

Command Example:

import {WriterCommand} from 'writer'

class MyAmazingCommand extends WriterCommand {

  /**
  * Executes command with supplied params.
  * When called from Tool class, props from Tools is contained in params.
  * 
  * @param params
  */
  execute(params) {
      console.info('Have a great day!')
      console.info('Also, here\'s the supplied data', params)
  }
}

export {MyAmazingCommand}

addContentMenuTool(commandName, ToolClass) Adds a Tool to the Content Menu, it should return a rendered ContextMenuItem instance.

addContentMenuTopTool(commandName, ToolClass) Adds a Tool to the top of the Content Menu.

addContextMenuTool(commandName, ToolClass) Adds a Tool to the context menu, e.g when right clicking on the editing surface.

addOverlayTool(commandName, ToolClass) Adds a Tool which is displayed when selecting text, such as annotation plugins.