WYSIWYG Plugin Interface

Last modified by Vincent Massol on 2024/11/19 16:12

Description

UI

Administration

For organizing plugins there is already an UI. This feature will also be used to add your own plugins. For adding and managing custom plugins, we just add a link to 'Manage plugins>>', which will redirect you to the space 'WYSIWYGPlugins'.

xwiki-wysiwyg-plugins-1-admin-manage-plugins.png

WYSIWYGPlugins

Stored in /xwiki-platform-core/xwiki-platform-wysiwyg/xwiki-platform-wysiwyg-ui/src/main/resources/WYSIWYGPlugins

  • Overview of existing WYSIWYG plugins in your wiki provided by a livetable; option to edit and delete custom plugins.
  • Button to add new custom plugins

xwiki-wysiwyg-plugins-2-space-manage-plugins.png

Add a new custom WYSIWYG plugin

  • Using the notation of XWiki Classes/Objects/Templates
    • WYSIWYGPluginClass
    • WYSIWYGPluginSheet
    • WYSIWYGPluginTemplate
Defining the custom plugin
  • Document name
    • Plugin name
  • Document title
    • Plugin pretty name
  • Document content
    • Plugin description

xwiki-wysiwyg-plugins-4-plugin.PNG

WYSIWYGPluginClass

xwiki-wysiwyg-plugins-3.2-plugin-class.PNG

  • placeholder: String
    • We can't have empty classes so we just add a placeholder property in the WysiwygPluginClass for now
JavaScriptExtensionClass

Use an JSX object for providing the JavaScript code of the plugin.

xwiki-wysiwyg-plugins-5-plugin.PNG

JavaScriptPlugin example

var wysiwygEditorPlugins = (function(plugins) {
  console.log("Factory Function for Plugin $doc.name called");
  var editor;
  Event.observe(document, 'xwiki:wysiwyg:created', function(event) {
    editor = event.memo.instance;
  });

  myPlugin = function() {
  };

  myPlugin.prototype.addButton= function() {
      console.log("addButton() of plugin $doc.name called");
      button = {"iconURL":"$doc.getAttachmentURL("add2.png")", "title":"Insert"}
      return button;
  };

  myPlugin.prototype.onClick = function(textArea, config) {
      console.log("onClick() of plugin $doc.name called");
      editor.getCommandManager().execute('inserthtml', 'foo');
  };

  myPlugin.prototype.init = function(textArea, config) {
    console.log("Constructor of plugin $doc.name called");
  };
  myPlugin.prototype.getUIExtensions = function() {
    console.log("getUIExtensions() of plugin $doc.name called");
    return null;
  };

  // Export the plugin class.
  plugins['$escapetool.javascript($doc.name)'] = myPlugin;
  return plugins;
})(wysiwygEditorPlugins || {});

Implementation

org.xwiki.gwt.wysiwyg.client

WysiwygEditorFactory.java
...
 private WysiwygEditorFactory()
    {
        ...

        // Determines the JavaScriptPlugins provided as XWiki Documents and adds a Factory to the PluginManager.
        JavaScriptPluginFactory.registerPlugins(pfm);
    }

xwiki-platform/xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/macros.vm

  • Load the custom plugins.
## Load the custom plugins.
#set($configuredListOfPlugins = $xwiki.getDocument("XWiki.WysiwygEditorConfig").getObject("WysiwygEditorConfigClass").getProperty("plugins").getValue().split(" "))
#set($listOfCustomPlugins = [])
#foreach ($pluginName in $configuredListOfPlugins)
  #set ($pluginDocumentReference = $services.model.resolveDocument("WYSIWYGPlugins.$pluginName"))
  #if ($xwiki.exists($pluginDocumentReference))
    $xwiki.jsx.use($pluginDocumentReference)
 $listOfCustomPlugins.add("$pluginName")
  #end
#end
  • The WYSIWYG editor needs to detect the available custom plugin factories and initialize the plugins. To achieve that we use a global variable which provides the plugin names. For that var wysiwygEditorPlugins = {}; is instantiated in "XWikiWysiwyg.js".

org.xwiki.gwt.wysiwyg.client.plugin.javascript

JavaScriptPluginFactory.java

Within the names the plugins will be registered in the PluginfactoryManager.

package org.xwiki.gwt.wysiwyg.client.plugin.javascript;

import org.xwiki.gwt.wysiwyg.client.plugin.Plugin;
import org.xwiki.gwt.wysiwyg.client.plugin.PluginFactory;
import org.xwiki.gwt.wysiwyg.client.plugin.PluginFactoryManager;

import com.google.gwt.core.client.JsArrayString;

public class JavaScriptPluginFactory implements PluginFactory
{
    private final String m_Name;

    private JavaScriptPluginFactory(final String name)
    {
        this.m_Name = name;
    }

    /**
     * @see org.xwiki.gwt.wysiwyg.client.plugin.PluginFactory#newInstance()
     */
    @Override
    public Plugin newInstance()
    {
        return new JavaScriptPlugin(m_Name);
    }

    /**
     * @see org.xwiki.gwt.wysiwyg.client.plugin.PluginFactory#getPluginName()
     */
    @Override
    public String getPluginName()
    {
        return m_Name;
    }

    /**
     * @return
     */
    private static JavaScriptPluginFactory getInstance(final String name)
    {
        return new JavaScriptPluginFactory(name);
    }

    private static native JsArrayString getPluginNames() /*-{
        function getAllMethods(object) {
            return Object.getOwnPropertyNames(object).filter(
                    function(property) {
                        return typeof object[property] == 'function';
                    });
        }
        return getAllMethods($wnd.wysiwygEditorPlugins);
    }-*/;

    public static void registerPlugins(final PluginFactoryManager pfm) {
        JsArrayString pluginNames = getPluginNames();
        for(int i = 0; i < pluginNames.length(); i++) {
            pfm.addPluginFactory(JavaScriptPluginFactory.getInstance(pluginNames.get(i)));
        }
    }
}
JavaScriptPlugin.java
Warning

The following code is only a draft.

So far the instantiated plugins can add a button to the a tool bar and add a onClick feature to it.

package org.xwiki.gwt.wysiwyg.client.plugin.javascript;

import java.util.HashMap;
import java.util.Map;

import org.xwiki.gwt.dom.client.JavaScriptObject;
import org.xwiki.gwt.user.client.Config;
import org.xwiki.gwt.user.client.ui.rta.RichTextArea;
import org.xwiki.gwt.user.client.ui.rta.cmd.Command;
import org.xwiki.gwt.wysiwyg.client.plugin.internal.AbstractStatefulPlugin;
import org.xwiki.gwt.wysiwyg.client.plugin.internal.FocusWidgetUIExtension;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ToggleButton;

public class JavaScriptPlugin extends AbstractStatefulPlugin implements ClickHandler
{

    /**
     * The association between tool bar buttons and the commands that are executed when these buttons are clicked.
     */
    private final Map<ToggleButton, Command> buttons = new HashMap<ToggleButton, Command>();

    /**
     * User interface extension for the editor tool bar.
     */
    private final FocusWidgetUIExtension toolBarExtension = new FocusWidgetUIExtension("toolbar");

    /** Plugin name */
    private final String m_Name;

    /** JavaScript object */
    private final JavaScriptObject m_PluginObject;

    public String getName()
    {
        return m_Name;
    }

    /** Javascript code */
    public JavaScriptPlugin(final String name)
    {
        GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler()
        {
            @Override
            public void onUncaughtException(final Throwable e)
            {
                e.printStackTrace();
            }
        });

        this.m_Name = name;

        JavaScriptObject plugin = null;

        try {
            plugin = createPlugin(name);
        } catch(Exception ex) {
            throw new IllegalStateException("Plugin " + name + " could not be created", ex);
        }
        if(plugin == null) {
            throw new IllegalStateException("Plugin " + name + " not found");
        }
        m_PluginObject = plugin;
    }

    /**
     * Ensures that the same methods of different javascript plugins are clearly addressable to the corresponding
     * plugin. See onClick() for an example.
     */
    private native JavaScriptObject createPlugin(String name)
    /*-{
        $wnd.console.log("Creating Plugin " + name );

        plugin = $wnd.wysiwygEditorPlugins[name];
        if (typeof plugin == 'function') {
            object = new plugin();
            $wnd.console.log("Plugin " + name + " created");

            return object;
        }
        return null;
    }-*/;

    /**
     * {@inheritDoc}
     *
     * @see AbstractStatefulPlugin#init(RichTextArea, Config)
     */
    @Override
    public void init(final RichTextArea textArea, final Config config)
    {
        super.init(textArea, config);

        initPlugin(m_PluginObject, textArea, config, m_Name);
        exportDestroy(m_PluginObject, m_Name);
        exportUpdate(m_PluginObject, m_Name);
        this.addButton(m_Name, new Command(m_Name));

        if(toolBarExtension.getFeatures().length > 0)
        {
            registerTextAreaHandlers();
            getUIExtensionList().add(toolBarExtension);
        }
    }

    private native void initPlugin(JavaScriptObject plugin, final RichTextArea textArea, final Config config, String name) /*-{
       if(typeof plugin.init == "function") {
           $wnd.console.log("Initializing Plugin " + name);
       plugin.init(textArea, config);
           $wnd.console.log("Plugin " + name + " successfully initialized");
       } else {
           $wnd.console.log("Plugin " + name + " has no init() function");
       }
    }-*/;

    /**
     * Creates a tool bar feature and adds it to the tool bar. *r. *
     */

    private ToggleButton addButton(final String featureName, final Command command)
    {
        JavaScriptObject jsonButton = null;
        try{
            jsonButton = addButtonNative(m_PluginObject, m_Name);

            String iconURL = (String)jsonButton.get("iconURL");
            String title = (String)jsonButton.get("title");
            Image image = new Image();
            image.setUrl(iconURL);
            image.setPixelSize(20, 20);
            ToggleButton button = null;

            button = new ToggleButton(image);
            saveRegistration(button.addClickHandler(this));
            button.setTitle(title);
            toolBarExtension.addFeature(featureName, button);
            buttons.put(button, command);

            return button;

        }catch(Exception e){
            addButtonException(m_PluginObject, m_Name);
            return null;
        }
    }

    private native void addButtonException(JavaScriptObject plugin, String name)  /*-{
        $wnd.console.log("No valid json for addButton() function of plugin " + name + " returned");
    }-*/;

    private native JavaScriptObject addButtonNative(JavaScriptObject plugin, String name)
    /*-{
       if(typeof plugin.addButton == "function") {
           $wnd.console.log("Calling addButton() of plugin " + name);
           button = plugin.addButton();
           if(typeof button != 'undefined'){
               $wnd.console.log("addButton() of plugin " + name + " successfully called");
               return button;
           }else{
               $wnd.console.log("No button for Plugin " + name + " returned");
               return null;
           }
       } else {
           $wnd.console.log("Plugin " + name + " has no addButton() function");
           return null;
       }
    }-*/;

    @Override
    public void onClick(final ClickEvent event) {
        onClickNative(m_PluginObject, getTextArea(), getConfig(), m_Name);
    }

    /**
     * @see ClickHandler#onClick(ClickEvent)
     */
    private native void onClickNative(JavaScriptObject plugin, final RichTextArea textArea, final Config config, String name)
    /*-{
       if(typeof plugin.onClick == "function") {
           $wnd.console.log("Calling onClick of plugin " + name);
           plugin.onClick(textArea, config);
           $wnd.console.log("onClick() of plugin " + name + " successfully called");
       } else {
           $wnd.console.log("Plugin " + name + " has no onClick function");
       }
    }-*/;

    /**
     * @see AbstractStatefulPlugin#destroy()
     */
    @Override
    public void destroy()
    {
        for(ToggleButton button : buttons.keySet())
        {
            button.removeFromParent();
        }
        buttons.clear();

        toolBarExtension.clearFeatures();

        super.destroy();
    }

    public native void exportDestroy(JavaScriptObject plugin, String name) /*-{
        plugin.__proto__.destroy = function(){
            $wnd.console.log("Calling destroy of plugin " + name);
            this.@org.xwiki.gwt.wysiwyg.client.plugin.javascript.JavaScriptPlugin::destroy();
            $wnd.console.log("destroy() of plugin " + name + " successfully called");
        }
  }-*/;


    /**
     * @see AbstractStatefulPlugin#update()
     */
    @Override
    public void update()
    {
        for(Map.Entry<ToggleButton, Command> entry : buttons.entrySet())
        {
            if(entry.getKey().isEnabled())
            {
                entry.getKey().setDown(getTextArea().getCommandManager().isExecuted(entry.getValue()));
            }
        }
    }

    private native void exportUpdate(JavaScriptObject plugin, String name)
    /*-{
        plugin.__proto__.update = function(){
            $wnd.console.log("Calling update() of plugin " + name);
            this.@org.xwiki.gwt.wysiwyg.client.plugin.javascript.JavaScriptPlugin::update();
            $wnd.console.log("update() of plugin " + name + " successfully called");
        }
    }-*/;
}

TODO

  • Define an "interface" for the JavaScript plugins to make a concrete implemetation of the JavaScriptPlugin class

 

Get Connected