Skip to content

Creación de plugins

Yago González edited this page Feb 7, 2018 · 16 revisions

Manual for plugin creation

If you want to create your own plugin, you're in the right section in the docs. You can use any existant plugin as a base to create your own plugin. It is very easy to take react existant plugins and integrate them inside an aplication as plugins.

Enviroment preparation

Option 1: Using the CLI

  1. From the project directory run: yarn run create-plugin "Name Of The plugin" This will create a folder inside the directory plugins named NameOfThePlugin containing all the files you need.

  2. In order to include it in Ediphy you need to add it to pluginList in ~/core/config.es6.

The plugin that you have created will provide a simple plugin that displays a div with the text "Hello Ediphy". If you take a close look to the template, you can see that the second word (Ediphy) is bound to the state, which can be changed from the toolbar.

This plugin should be taken as a starting point for more complex plugins. A folder to hold the language translation (locales) and a folder for the viewer template will also be created.

The command also allows specifying the plugin category (example: yarn run create-plugin "Name Of The plugin" category text), and whether the plugin is rich (yarn run create-plugin "Name Of The plugin" category text) or if it does not need a viewer template (yarn run create-plugin "Name Of The plugin" no-visor).

Option 2: From scratch

Before we start our plugin we need to create the folders and the files needed:

  1. We create a folder inside the plugin in the following folder: **~/dali/plugins/<PluginName>**

  2. Inside this folder we can create more folders, depending on the plugin design.

    • The visor folder is used to store the plugin behaviour when is being visualized, previsualized or exporting it.
    • The locales folder will store the translation files.
    • The css folder will store the styles.
    • The package.json will define the dependencies that will have external libraries, either npm packages or specific libraries downloaded inside the project.
  3. To define the plugin behaviour, we will create a Javascript file with the same name , in general or inside the visor plugin folder (if we want the plugin to have that behaviour).

    Typical plugin structure

    ediphy/plugins
     └── <PluginName>
        ├── <PluginName>.js
        ├── locales
        |  ├── en.js
        |  └── es.js
        └── visor
           └── <PluginName>.js
    
    
  4. After creating the files and folders for our plugin we should say to DALI that is going to have a new plugin, for this we will go to the ~/core/config.es6 file and we add a new line with the plugin name in the plujginList (all these lines should be separated by a comma except the last one).

    #~/dist/core/config.es6
    ...
    sections_have_content: false,
    pluginList: [
      'BasicImage',
      'BasicText',
      'RichText',
      'BasicVideo',
      'Youtube',
      'Webpage',
      'CajasColor',
      'Container',
      'ListaNumerada',
      'RelacionaAll'
    ],
    availableLanguages:[
      'en',
      'es'
    ]
    ...
    

Now we should have our enviroment prepared to create the plugin.

Skeletal preparation:

After we prepare the enviroment the following task is to do is to place the plugin base structure and fil it. During this manual we will explain what is it for and what any function does. Every function that is nos used doesn't need to be declared.

Structure preparation for editor

export function <PluginName>(base){
  return{
    init: function(){
    },
    getConfig: function(){
    },
    getToolbar: function(){
    },
    getInitialState: function(){
    },
    getRenderTemplate: function(state){
    },
    getConfigTemplate: function(state){
    },
    afterRender: function(element, state){
    },
    handleToolbar: function(name, value){
    },
    _funciones auxiliares_: function(event, element, parent){
    }
  }
}

Structure preparation for visor

export function <NombreDelPlugin>(base){
  return{
    init: function(){
    },
    getRenderTemplate: function(state){
    },
    _funciones auxiliares_: function(event, element, parent){
    }
  }
}

Plugin creation

Plugin creation for the editor

Once we have the folder and the file structure we will fill every part of it. We observe that our plugin recieves as parameter an object base. This makes reference to the base plugin that all the application plugins depend from. Basically we will need to use the methods getState, setState and registerExtraFunction, that we will see later on.

init

This method is used to initialized the neccesary parameters for the proper plugin behaviour. It is executed only once while the application is loading. An example is the register of the functions "extra" calling to base.registerExtraFunction(fn,alias), being fn the reference to the function that we want to use and alias an aditional parameter that will be used as an alias for the "extra"function in the functions selector. An example of this would be:

export function BasicImage(base) {
  return {
    init: function () {
      base.registerExtraFunction(this.imageClick, "click");
    },
    ...
    imageClick: function (element) {
      alert("Hi");
    }
  };
}

This way, the functiobn imageClick is registered so all the plugin can access it throught the click alias recieved as HTML parámeter from the plugin.

getConfig

Here we can set up the plugin. It is basically the only mandatory method (rest of them are in a higher or lower level optional). It should return an object with the configuration:

getConfig: function () {
  return {
    name: <PluginName>,
    displayName: Ediphy.i18n.t('PluginName'),
    category: 'image',
    aspectRatioButtonConfig: {
      location: ["main", "__sortable"],
      defaultValue: "checked"
    },
    icon: 'image'
  };
}
  • name: it should be a text chain with we used all the time.
  • displayName (optional): it is the name that will appear in the application and that the user will see. If it is not specified, it will be used name. A value from the translation files can be used using Ediphy.i18n.t("key) being key the key assigned in this local files.
  • category (optional): define which sections in the top bar should be added to. Takes one of the following values ("text", "image", "multimedia"). If none is assigned, it will have the value "text" by default, if the assigned is invalid, plugin won't appear.
  • icon (optional): tells which icon will have next to the plugin name, it should be choosen from this list iconos. By default it will be "fa-cogs" (which is an engine). In case iconFromUrl (the following) is true, any image from an url can be used instead.
  • iconFromUrl (optional): a boolean value that allows to use images instead of icons in the icon option. By default, it is false.
  • isRich (optional): it is a boolean value that gives you access to enriched plugins functionalities (still in development, see section "Enriched Plugins"). By default it is false.
  • flavor (optional): tells which mode has been the plugin written, it can be either "plain" or "react. By default it is "plain" telling that it has been written using conventional Javascript.
  • needsConfigModal (optional): boolean value defines that if the plugin needs an initial aditional configuration dialog. By default it is false, but if a true value is assigned, then it is neccesary to add the method getConfigTemplate to the plugin to define how the interfaz of this dialog would be. also, it will be the first that will be shown when the plugin is added.
  • needsTextEdition (optional): boolean value used to specify if the plugin will need the provided tools for text edition. If false by default. It is assigned a true value, a new "hidden" property will appear in the state called "__text" that will contain plugin text. This is the only situation where a plugin can't have the method getRenderTemplate, because in case it fails, it will automatically generate returning the value of __text.
  • extraTextConfig (optional): this is a text chain separated by commas that specifies the concrete plugin activation of CKEditor for this plugin. An example of this would be that we would like to activate the plugin of CKEditor that will alow to change the size of the font and therefore create a plugin TextWithFontSizeSelectable. By default, it goes empty.
  • needsXMLEdition (optional): boolean value that specifies if this plugin needs the XML management tools. By default it is false.
  • allowFloatingBox (optional): boolean value that tells if the plugin can be added as a floating box to the course (without being inside a container). By default it is true.
  • aspectRatioButtonConfig (optional) DEPRECATED: it is an object that is used to set up a button that blocks the aspect relation when changing the plugin size. It has two properties, one "location" that is an array where it is specified the keys of the different ancestors that a toolbar will have (tabs, accordion and subaccordion if existing), and another "defaultValue" that tells if it has to be active or not inicially (by default, not). This config will change its value to a boolean that will fix the localization(["main", "__sortable"]).
  • aspectRatioButtonConfig (optional) DEPRECATED: it is an object that is used to config the button that blocks the aspect relation when changing the plugin size. It has two properties, a "location" one that is an array that is telling the keys of the different ancestors that will have in the toolbar(tabs, accordion and subaccordion if existing), and another "defaultValue" that tells if it has to be active or not initially (by default, not). This configuration changes to a boolean value that will set the localization (["main", "__sortable"]).
  • needsPointerEventsAllowed (optional): boolean value that lets to manipulate the plugin with the mouse when editing, instead of only in visualization, easing its configuration (use example: center and zoom the map). By default it is false.
  • limitToOneInstance (optional): boolean value that lets to limit to only one instance of the plugin per page.

Also in getConfig() intiial values width and height are specified. Slides can only have plugins with relative units(%) because it scales with screen size.

  • initialWidth: Tells the initial width of the plugin in the documents. If not specified, default value is '20%'. It can be a percentage value, in pixels or 'auto'.
  • initialHeight: Tells the initial height of the plugin in the documents. If not specified, default value is '20%'. It can be a percentage value, in pixels or 'auto'.
  • initialWidthSlide: Tells the initial width of the plugin in the documents. If not specified, default value is initialWidth. It can be a percentage value, in pixels or 'auto'. It is mandatory if initialWidth is in px.
  • initialHeightSlide: Tells the initial width of the plugin in the documents. If not specified, default value is ìnitialHeight. It can be a percentage value, in pixels or 'auto'. It is mandatory if ìnitialHeight is in px.

getToolbar

Here the buttons that we want to appear in the toolbar are set up. Basically it is compound by three parts: tabs, accordions and buttons (tabs are disabled right now because only one can exist).

getToolbar: function () {
  return {
    main: {
      __name: "Main",
      accordions: {
        basic: {
          __name: Ediphy.i18n.t('BasicVideo.Video'),
          icon: 'link',
          buttons: {
            url: {
              __name: Ediphy.i18n.t('BasicVideo.URL'),
              type: 'text',
              value: base.getState().url,
              autoManaged: false
            },
            controls: {
              __name: Ediphy.i18n.t('BasicVideo.Show_controls'),
              type: 'checkbox',
              checked: base.getState().controls,
              autoManaged: false
            },
            autoplay: {
              __name: Ediphy.i18n.t('BasicVideo.Autoplay'),
              type: 'checkbox',
              checked: base.getState().autoplay,
              autoManaged: false
            }
          }
        },
        style: {
          __name: Ediphy.i18n.t('BasicVideo.box_style'),
          icon: 'palette',
          buttons: {
            padding: {
              __name: Ediphy.i18n.t('BasicVideo.padding'),
              type: 'number',
              value: 0,
              min: 0,
              units: 'px',
              max: 100
            },
            borderStyle: {
              __name: Ediphy.i18n.t('BasicVideo.border_style'),
              type: 'select',
              value: 'solid',
              options: ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset', 'initial', 'inherit']
            },
            borderColor: {
              __name: Ediphy.i18n.t('BasicVideo.border_color'),
              type: 'color',
              value: '#000000'
            }
          }
        }
      }
    }
  };
}

As we can see, as in getConfig here an object with the whole structure is returnet. In the following section the structure will be explained and its descendants.

  • This object will have as many properties as the tabs (as we said before that meanwhile only one can exist, "main" tab). Obviously, there can't be two tabs with the same name.
  • Each tab will be at the same time an object with two properties, __name (that is a text chain that can have any desired value, even the ones got from translation files) and accordions, that is a dictionary with all the accordions that contains this tab.
  • Every of the accordions will have a unique key by the one that would be identified and a series of properties:
    • __name: name that will be shown in the toolbars.
    • icon: icon that will be shown (it will be chosen from the same list).
    • buttons: object that contains all the buttons of the accordion.
    • accordions: object with the subaccordions that will be contained. They would be in form except by the fact that can't contain subaccordions at the same time.
    • order (optional): array that contains the button keys and subaccordions in the order that is desired to be shown in the toolbar.
  • Every single button will be an object with the following properties too:
    • name: name that will be shown in the toolbar.
    • type: tells the kind of control show in the toolbar. One of the following:
      • color: overwrites the input native color of HTML to allow color selection with the transparency level.
      • radio: Input radio. To choose between n options.
      • fancy_radio: The same input as *radio* but with icons as buttons instead of text. Requires some aditional fields exclusive for this plugin:
      • checkbox: Checkbox input. (Uses checked instead of value).
      • vish_provider: Input from a file from ViSH platform
      • text: Text input.
      • number: Numbers input
      • range: Range input (slider).
      • select: Select input. Needs options field with an array where to choose from.
    • options: the options that will be shown. It is neccesary for select, radio and fancy_radio.
    • tooltips: text to be shown when hovering every button with the mouse. Exclusive of fancy_radio.
    • icons: Material icon name for each options (ej. 'warning'). Excluve of fancy_radio. It will be chonse from the list from material icons. It is specified in form of an array of strings in the same order of the options.
    • value: the value that is going to have. In case of being of the kind "checkbox", instead of value, it will be checked.
    • units: the units that is going to have, if that makes sense (i.e, "px" or "%" for a number or a range)
    • min: the minimum value that is going to have, if that makes sense (i.e., for a number or a range).
    • max: the maximum value that is going to have, if that makes sense (i.e., for a number or a range).
    • step: minimum change, if that makes sense (i.e., for a number or a range).
    • autoManaged: boolean value that tells if the update of value is automatic or not. Otherwise, when the value changes it will be called the method ´handleToolbar(name,value)´ passing as parameters the button key and the new value. By default it is true.

getInitialState

Here the state the plugin is going to have is defined. DALI is the one that mantains the value of every instance

getInitialState: function () {
  return {
    url: 'http://nemanjakovacevic.net/wp-content/uploads/2013/07/placeholder.png'
  };
}

handleToolbar(name, value)

As said before, when delaring the button is not autoManaged and its value is changed, this method is invoked recieving as parameters the key value of the button and the new value. Normally, it is used to change state values.

handleToolbar: function (name, value) {
  base.setState(name, value);
}

getRenderTemplate(state)

This is, as well as getConfig the only other mandatory function (except if it is configured to need text, that generates it automatically if not existing). The purpose of this method is to return a string can be done either by concat them or generating the elements using document.createElement(), assigning them the values and returning them innerHTML (supossing that selected flavor is "plain"). Only a root element should be returned and should be a selfclosing tag.

**INCORRECT**
<img />
<span></span>

**INCORRECT**
<img />

**CORRECT**
<div>
  <img />
  <span></span>
</div>

Instead of a plugin state (recieved as parameter), it should be returned from one interface or other.

getRenderTemplate: function (state) {
  return "<video " + 
    ((state.controls === "checked") ? " controls " : "") + 
    ((state.autoplay === "checked") ? " autoplay " : "") + 
    " style=\"width: 100%; height: 100%; pointer-events: 'none'; z-index:0;\" src=\"" + 
    state.url + "\" class=\"basicImageClass\"></video>";
}

getConfigTemplate(state)

Its behaviour is the same as in getRenderTemplate, only in the interface is generated for the configuration plugin dialog.

afterRender(element, state)

It is used when you want to re-render the interface after of being rendered for the first time. An example of this is to want to reposition the elements by the size that they have been generated (if this was tried before, there wouldn't be access to the physical element of the DOM). Recieves as parameters the own element extracted by the DOM and the plugin state.

pointerEventsCallback(state)

It is called to this function to activate/deactivate pointerEvents in the plugins that allow it. It has two parameters:

  • bool: true if mouse events are activated and false if they are not.
  • toolbarState: actual plugin State.
pointerEventsCallback: function(bool, toolbarState) {
    // Show manual controlers only when pointer events are activated
    base.setState('showControls', bool);
}

auxiliar functions(event, element, parent)

Any amount of auxiliar functions can be defined. Alse, if they want to be used a event controllers of the rendering template, then they will automatically recieve the thrown event, the element that shot it and the whole plugin template. For this to work correctly, it should be included putting the prefix $dali$. in front of the function name, including the parenthesis to invoke it, without parámeters (that would be ignored).

getRenderTemplate: function(state){
  return "<div><img onClick=\"$dali$.showDiv()\" /></div>";
},
showDiv: function(event, element, parent){
  alert("Hola");
}

In this case, "event" would contain the click event with all the information about it, "element" the element <img> extracted from the DOM and "parent" the <div>. One using case is that when clicking in some part of the template, i.e., a floating box, that want to be hidden or shown. In this case, if we find the box and we want to deploy by the use of an identificator, we should use the "parent" as context to limit the search, or otherwise it wouldn't find what is expected.

$( "div[id^='bloque']", parent).slideUp( "fast", function() {});

Plugin containers

If in any moment it is desired that a plugin can hold any other plugins, the only thing that we need to do is to add to the template a tag of <plugin />. This label should be contained inside an element and should be its only descendant (the easiest way to do this is creating a <div> that has the desried properties (color, size,etc.) and to make it to contain the tag <plugin />).

Also, the <plugin /> should have an attribute called plugin-data-key taht will have as a value a unique key inside the group of <plugin />s that have the template, to be able to be identified correctly and assign it its content. If a visor template would be also defined, keys should be equal to be able to show the created content inside the editor.

Now the rest of the attributes that can be defined in a plugin container will be listed:

  • plugin-data-resizable: if this attribute is included (doesn't need to be a value), then the resize of itself will be activated.
  • plugin-data-initialHeight: the value of this attibute will tell the initial size that the plugin container is defined. It can be any unit, either relative or absolute. The way to determine the size of the containers will be the following way:
  • If it has the attribute plugin-data-height", this value is used. Otherwise, it is created and given.
  • If it has the attribute plugin-data-initialHeight, the value is used. Otherways, keeps going.
  • If it has the attribute plugin-data-resizable, it is given the value 150(pixels). Otherwise, it is assigned the whole parent height (100%). Afterwards, the height gets stored in "plugin-data-height" and aftewards the plugin state.
  • plugin-data-default: the value of this attributes is a list of names of plugins separated by spaces that want to be added to the container when created.
  • plugin-data-display-name: it is the value that uses the toolbar to identify this container and configure it. If not assigned any, takes the default value "Container" and a number.

Plugin creation for the visor

Plugin creation for the visor doesn't need new knowledge, but aditional functions are more relevante here because they can be used (in editing mode, there's no way you can interect with plugin inside). Nevertheless, it is not mandatory that a plugin have both modes (i.e., in an image it doesn't make sense), it is allowed the inclusion of auxiliar functions in case some visualization interaction is needed (i.e. a web link is opened).

Dependencies plugin structure

The dependencies of a node module are normally defined in a package.json. This is a JSON file that includes de version, name, config elements and dependencies among other things.

{
    "name": "plugin name", 	        //(mandatory field)
    "version" : "plugin version", 	//(mandatory field)
    "dependencies": {
        "dependency name": "package versionj",
    },
    config:{
        "localDependencies":{
            "name": "library route"
        },
        "aliases": {
            "dependency name": "name to be exported"
        },
        "css": {
            "name" : "route to css"
        }
    }
}

Inside a plugin there are three kind of dependencies:

  • NPM dependency: it can be obtained from npm, they are normally public but can be private, they are javascript libraries packed inside a npm repository. Inside package.json they must be in dependencies part. If it wants to be used a global name for the depedency used should be used -> config -> aliases.

  • Local dependency: to inject directly a javascript library inside the app local dependencies should be used. Inside the json should be in -> config -> dependencies.

  • Styles dependency: they are css styles sheets. Inside the JSON should be in -> config -> css.

Clone this wiki locally