Skip to content

Creación de plugins

Berto edited this page Jan 12, 2017 · 56 revisions

#Manual para la creación de un plugin#

##Preparación del entorno#

Antes de ponernos a programar nuestro plugin debemos crear las carpetas y los ficheros necesarios para ello:

  1. Creamos una carpeta con el nombre del Plugin en el siguiente directorio: ~/dali/plugins/
  2. Dentro de esta carpeta podemos crear más carpetas, en función de lo que queramos que haga.
  • La carpeta visor sirve para almacenar el comportamiento del plugin cuando se está visualizando, ya sea previsualizando o exportando el curso.
  • La carpeta locales almacenará los archivos de traducción.
  • La carpeta css almacenará los estilos.
  • El archivo package.json definirá las dependencias que tenga de librerías externas, ya sea de paquetes npm como de librerías específicas descargadas dentro del proyecto.
  1. Para definir el comportamiento del plugin, crearemos un archivo JavaScript con el mismo nombre que el del plugin , tanto al nivel general como dentro de la carpeta visor (si queremos que el plugin tenga ese comportamiento).

Estructura típica de un plugin
~/dali/plugins/NombreDelPlugin/.js
~/dali/plugins/NombreDelPlugin/visor/.js
~/dali/plugins/NombreDelPlugin/locales/es.js
~/dali/plugins/NombreDelPlugin/locales/en.js

  1. Después de crear los directorios y ficheros para nuestro plugin debemos decir a DALI que va a tener un nuevo plugin, Para ello vamos al fichero de ~/core/config.es6. y añadimos una línea con el nombre del plugin en el apartado pluginList (todas las líneas deben estar separadas por comas excepto la última)
#~/dist/core/config.es6
...
sections_have_content: false,
pluginList: [
  'BasicImage',
  'BasicText',
  'RichText',
  'BasicVideo',
  'Youtube',
  'Webpage',
  'CajasColor',
  'Container',
  'ListaNumerada',
  'RelacionaAll'
],
availableLanguages:[
  'en',
  'es'
]
...

En este momento deberíamos de tener nuestro entorno preparado para empezar a crear el plugin.

##Preparación del esqueleto:# Tras preparar el entorno la siguiente tarea que debemos realizar es colocar el esqueleto base de nuestro plugin para más tarde ir rellenándolo. A lo largo del manual se irá explicando para qué sirve y qué debe ir dentro de cada función. Toda función que no se use no hace falta declararla.

###Preparación del esqueleto para el editor#

export function <NombreDelPlugin>(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){
    }
  }
}

###Preparación del esqueleto para el visor#

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

##Creación del plugin# ###Creación del plugin para el editor#

Una vez que tenemos la estructura de ficheros y el esqueleto necesario para desarrollar nuestro plugin vamos a ir rellenando poco a poco cada una de las partes del mismo.

Observamos es que nuestro plugin recibe como parámetro un objeto base. Este hace referencia al plugin base del que todos los plugins de la aplicación dependen. Principalmente lo necesitaremos para hacer uso de los métodos getState, setState y registerExtraFunction, que veremos más adelante en detalle.

####init# Este método se usa para inicializar parámetros necesarios para el correcto funcionamiento del plugin. Se ejecuta una sola vez mientras se carga la aplicación. Un ejemplo es el registro de las funciones "extra" llamando a base.registerExtraFunction(fn, alias), siendo fn la referencia a la función que queremos utilizar y alias un parámetro opcional que se usará como alias para la función "extra" en el selector de funciones. Un ejemplo de esto sería:

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

De este modo, la función imageClick queda registrada para que todos los plugins puedan acceder a ella a través del alias "click" y recibe como parámetro el HTML del plugin.

####getConfig# Aquí podremos configurar el plugin. Es prácticamente el único método obligatorio (el resto son más o menos opcionales). Debe devolver un objeto con la configuración:

getConfig: function () {
  return {
    name: <NombreDelPlugin>,
    displayName: Dali.i18n.t('PluginName'),
    category: 'image',
    aspectRatioButtonConfig: {
      location: ["main", "__sortable"],
      defaultValue: "checked"
    },
    icon: 'image'
  };
}
  • name: debe ser una cadena de texto con el que hemos utilizado todo el tiempo.
  • displayName (opcional): es el nombre que aparecerá en la aplicación de cara al usuario. Si no se especifica, se usará name. Se puede usar un valor de los archivos de traducción usando Dali.i18n.t("clave") siendo "clave" la clave asignada en estos archivos.
  • category (opcional): define a qué sección de la barra superior debe añadirse, toma uno de los siguientes valores ("text", "image", "multimedia", "animations", "exercises"). Si no se asigna ninguno, tendrá por defecto el valor "text", si se asigna uno inválido, el plugin no aparecerá en la barra superior.
  • icon (opcional): especifica el icono que aparecerá junto al nombre del plugin, debe seleccionarse de esta lista iconos. Por defecto vale "fa-cogs" (es un engranaje). En caso de que iconFromUrl (a continuación) sea verdadero, podrá utilizarse cualquier imagen desde una url en vez de un icono de la lista anterior.
  • iconFromUrl (opcional): es un valor booleano que permite utilizar imágenes en vez de iconos en el apartado icon. Por defecto, es falso.
  • isRich (opcional): es un valor booleano que da acceso a las funcionalidades de plugins enriquecidos (aún en desarrollo). Por defecto, es falso.
  • flavor (opcional): especifica de qué modo ha sido escrito el plugin, puede ser "plain" o "react". Por defecto es "plain" indicando que ha sido escrito usando JavaScript convencional.
  • needsConfigModal (opcional): valor booleano que define si el plugin necesita un diálogo de configuración adicional. Por defecto es falso, pero si se le asigna el valor verdadero, entonces es necesario añadir el método getConfigTemplate al plugin para definir cómo es la interfaz de ese diálogo. Además, será lo primero que se muestre al añadir un plugin.
  • needsTextEdition (opcional): valor booleano usado para especificar si el plugin necesitará hacer uso de las herramientas proporcionadas para la edición de texto. Por defecto es falso. Si se le asigna el valor verdadero, aparecerá en el estado una propiedad "oculta" llamada "__text" que contendrá el texto del plugin. Es la única situación en la que un plugin puede no tener el método getRenderTemplate, ya que en caso de faltar, se genera automáticamente devolviendo el valor de __text.
  • extraTextConfig (opcional): es una cadena de texto separada por comas que especifica la activación de un plugin concreto de CKEditor para este plugin. Un ejemplo de ello sería que quisiéramos activar el plugin de CKEditor que permite cambiar el tamaño de la fuente y crear así un plugin TextoConTamañoDeFuenteSeleccionable. Por defecto, va vacío.
  • needsXMLEdition (opcional): valor booleano que especifica si este plugin necesita de las herramientas de gestión de XMLs. Por defecto es falso.
  • allowFloatingBox (opcional): valor booleano que determina si el plugin podrá ser añadido como caja flotante al curso (sin estar dentro de un contenedor). Por defecto es verdadero.
  • aspectRatioButtonConfig (opcional) DEPRECATED: es un objeto que sirve para configurar el botón que bloquea la relación de aspecto al cambiar el tamaño del plugin. Tiene dos propiedades, una "location" que es un array en el que se va especificando las claves de los distintos antecesores que va a tener en la barra de herramientas (pestaña, acordeón y subacordeón si existiera), y otra "defaultValue" que especifica si tiene que estar activo o no inicialmente (por defecto, no). Esta configuración cambiará a un valor booleano ya que se fijará la localización (["main", "__sortable"]).

####getToolbar# Aquí se configura los botones que queremos que aparezcan en la barra de herramientas. Básicamente se compone de tres partes, pestañas, acordeones y botones (las pestañas por el momento están deshabilitadas ya que sólo puede haber una).

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

Como se puede ver, al igual que en getConfig aquí se devuelve un objeto con toda la estructura. A continuación se explicará su estructura y la de sus descendientes.

  • Este objeto va a tener como propiedades las pestañas (ya hemos dicho antes que de momento sólo puede haber una, que es la pestaña "main"). Obviamente, no puede haber dos pestañas con el mismo nombre
  • Cada pestaña será al mismo tiempo un objeto con dos propiedades, __name (que es una cadena de texto y puede tener el valor que se desee, incluso sacado de los ficheros de traducción) y accordions, que es un diccionario con todos los acordeones que contiene esta pestaña.
  • Cada uno de los acordeones tendrá una clave única por el que se le identificará y una serie de propiedades:
    • __name: nombre que se mostrará en la barra de herramientas.
    • icon: icono que se mostrará (se elegirá de la misma lista).
    • buttons: objeto con todos los botones que contendrá el acordeón.
    • accordions: objeto con los subacordeones que estarán contenidos. Serán iguales en forma excepto por el hecho de que no pueden contener subacordeones a su vez.
    • order (opcional): array que contiene las claves de los botones y subacordeones en el orden en el que se desea que se muestren en la barra de herramientas.
  • Cada uno de los botones será también un objeto con las siguientes propiedades:
    • __name: nombre que se mostrará en la barra de herramientas.
    • type: determinará el tipo de control mostrado en la barra de herramientas. Uno de los siguientes ("colorPicker", "select", "radio", "fancy_radio", "checkbox", "vish_provider", "text", "number", "color", "range").
    • options: las opciones que va a mostrar. Es necesario para "colorPicker", "select", "radio" y "fancy_radio".
    • value: el valor que va a tener. En caso de ser de tipo "checkbox", en vez de value, será checked.
    • units: las unidades que va a tener, si es que tiene sentido (por ejemplo, "px" o "%").
    • min: valor mínimo que puede tener, si es que tiene sentido (por ejemplo, para un "number" o un "range").
    • max: valor máximo que puede tener, si es que tiene sentido (por ejemplo, para un "number" o un "range").
    • step: cambio mínimo, si es que tiene sentido (por ejemplo, para un "number" o un "range").
    • autoManaged: valor booleano que indica si la actualización del valor es automática o no. En caso de no serlo, cuando cambie el valor se llamará al método ´´´handleToolbar(name, value)´´´ pasando como parámetros la clave del botón y el nuevo valor. Por defecto es verdadero.

####getInitialState# Aquí se define el estado que se quiera que tenga el plugin. DALI se encarga de mantener los valores de cada instancia

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

####handleToolbar(name, value)# Como ya se ha dicho antes, cuando se declara que el botón no es autoManaged y cambia su valor, este método es invocado recibiendo como parámetros la clave del botón y el nuevo valor. Lo habitual es usarlo para cambiar valores del estado.

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

####getRenderTemplate(state)# Esta es, junto con getConfig la otra única función obligatoria (excepto si se configura para que necesite texto, que la genera automáticamente si no existiera). El objetivo de este método es devolver una cadena de texto puede hacerse o bien concatenándolas o bien generando los elementos usando document.createElement(), asignándole los valores y devolviendo innerHTML (suponiendo que el flavor seleccionado sea "plain"). Siempre se debe devolver un único elemento raíz y no debe ser una etiqueta con autocierre.

**INCORRECTO**
<img />
<span></span>

**INCORRECTO**
<img />

**CORRECTO**
<div>
  <img />
  <span></span>
</div>

En función del estado del plugin (recibido como parámetro), se puede devolver una interfaz u otra.

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)# Su funcionamiento es el mismo que el de getRenderTemplate, solo que la interfaz que genera es para el diálogo de configuración del plugin.

####afterRender(element, state)# Se utiliza cuando se quiere volver a pintar la interfaz después de que se haya renderizado el plugin por primera vez. Un ejemplo de ello es querer reposicionar elementos en función del tamaño con el que se han generado (si se intentara antes, no habría acceso al elemento físico del DOM). Recibe como parámetros el propio elemento extraído del DOM y el estado del plugin.

####funciones auxiliares(event, element, parent)# Se pueden definir cuantas funciones auxiliares se desee. Si además, se desean utilizar como controladores de eventos de la plantilla de renderizado, entonces recibirán automáticamente el evento lanzado, el elemento que lo disparó y la plantilla completa del plugin. Para que esto funcione correctamente, debe incluirse anteponiendo el prefijo $dali$. al nombre de la función, incluyendo los paréntesis para invocarla, y sin parámetros (serán ignorados).

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

En este caso, "event" contendría el evento de click con toda la información pertinente, "element" el elemento <img> extraído del DOM y "parent" el <div>. Un caso de uso es que al hacer click en una parte de la plantilla, por ejemplo, en una caja desplegable, se quiera ocultar o mostrar. En este caso, si localizamos la caja que hemos de desplegar mediante el identificador, debemos hacer uso del "parent" como contexto para limitar la búsqueda, o en caso contrario puede no encontrar el que se esperaba.

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

###Contenedores de plugins# Si en cualquier momento se desea que un plugin pueda alojar a otros plugins, lo único que hay que hacer es añadir en la plantilla una etiqueta de <plugin />. Esta etiqueta debe estar contenida en un elemento y ser su único descendiente (la forma más sencilla de hacer esto es crear un <div> que tenga las propiedades deseadas (color, tamaño, etc.) y hacer que contenga la etiqueta <plugin />).

Además, el <plugin /> debe tener un atributo llamado "plugin-data-key" que tendrá como valor una clave única dentro del conjunto de <plugin />s que contenga la plantilla, para poder ser identificado correctamente y asignarle su contenido. Si se definiera también una plantilla para el visor, las claves han de ser iguales para poder mostrar el contenido creado en el editor.

A continuación se listarán el resto de atributos que pueden definirse para configurar un contenedor de plugins:

  • "plugin-data-resizable": si se incluye este atributo (no hace falta darle un valor), entonces se activará el redimensionado del mismo.
  • "plugin-data-initialHeight": el valor de este atributo indica el tamaño inicial que se desea que tenga el contenedor de plugins. Puede darse en cualquier unidad, ya sea relativa o absoluta. La forma de determinar el tamaño de los contenedores se realiza del siguiente modo:
  • Si tiene el atributo "plugin-data-height", se usa su valor. En caso de no tenerlo, se crea y se le da el valor calculado a continuación.
  • Si tiene definido el atributo "plugin-data-initialHeight", se usa su valor. Si no, continúa.
  • Si tiene definido el atributo "plugin-data-resizable", se le da el valor 150 (píxeles). Si no, se le asigna el total de la altura del padre (100%). Tras esto, la altura queda guardada en "plugin-data-height" y posteriormente en el estado del plugin.
  • "plugin-data-default": el valor de este atributo es una lista de nombres de plupackage.jsongins separados por espacios que se quiera añadir al contenedor según se cree.
  • "plugin-data-display-name: es el valor que se utiliza en la barra de herramientas para identificar a este contenedor en concreto y poder configurarlo. Si no se asigna ninguno, toma por defecto el valor "Contenedor" y un número.

###Creación del plugin para el visor# La creación del plugin para el visor no requiere de conocimientos adicionales a los ya vistos, si bien es cierto que las funciones auxiliares toman más relevancia aquí ya que es donde se pueden usar (en el modo edición no se puede interactuar con el interior de los plugins). Sin embargo, como no es obligatorio que un plugin tenga desarrollados ambos modos (por ejemplo, en una imagen no tiene sentido), se permite la inclusión de funciones auxiliares por si se quisiera algún tipo de interacción en la visualización (que abriera un enlace web, por poner un caso).

###Estructura de dependencias del plugin# Las dependencias de un módulo de node habitualmente se definen en un package.json. Este es un archivo en formato JSON que incluye la versión, el nombre, elementos de configuración y las dependencias entre otras cosas.

{
		"name": "nombre del plugin", 	        //(campo obligatorio)
		"version" : "version del plugin", 	//(campo obligatorio)
		"dependencies": {
			"nombre de dependencia de npm": "versión del paquete",
		},
		config:{
			"localDependencies":{
				"nombre": "ruta a la librería"
			},
			"aliases": {
				"nombre de la dependencia": "nombre para exportar"
			},
			"css": {
				"nombre" : "ruta al archivo css"
			}
		}
}

En un plugin de Dali Editor se permiten tres tipos de dependencias:

  • Dependencia de npm: se pueden obtener los repositorios de npm, habitualmente son públicas pero pueden ser privadas, son librerías javascript empaquetadas dentro de un repositorio de npm. Dentro del package.json deben estar en el apartado dependencies. Si quiere utilizarse un nombre global para la dependencia utilizada debe utilizarse -> config -> aliases.

  • Dependencia local: para inyectar directamente una librería de javascript dentro de la aplicación se deben usar las dependencias locales. Dentro del JSON deben estar dentro de -> config -> dependencies.

  • Dependencia de estilos: son hojas de estilos css. Dentro del JSON deben estar dentro de -> config -> css.