commajs
The Plugin Interface
Introduction
At this point, we assume that you are already familiar with Comma DOM and you have installed the commajs-core package. You can distribute your plugin with any commajs package - commajs-cre is needed only for development.
The Plugin Interface is the Javascript interface you will need to implement in order to develop your own plugins with CommaJS.
Together, let's develop the link plugin distributed with CommaJS. The link plugin manages hyperlinks inside a document.
Overview
We will call our plugin, MyLinkPlugin. It will need to extend the Plugin Interface, as follows:
import {Plugin} from 'commajs-core';

export default class MyLinkPlugin extends Plugin { 
    constructor(rootNode) { }

    PluginDidMount() { }

    Fire(pluginAction, nodeFiring) { }

    static GetName() { }

    static CreateNew(_document, params) { }

    static GetTagToParseOnPaste() { }

    static CreateNewFromPastedNode(_document, node) { }
    
};
GetName()
The first function you want to implement is GetName.
static GetName() { 
    return "myLinkPlugin"
}
Your plugin must have a name to be registered within CommaJS. The name must be unique or CommaJS will throw an error. GetName is the static function that commajs uses - you guessed it - to get the name of a plugin.
To register your plugin, initialize commajs-core as follows:
import MyLinkPlugin from "./myLinkPlugin"; 

let options = {
    defaultStyle: {
        fontSize: "14px",
        fontFamily: "Arial",
        fontColor: "#39434d"
    },
    plugins: [
        { pClass: MyLinkPlugin }
    ]
}

let commaEditor = new CommaJS(idDiv, options);
And to insert your plugin in the document:
commaEditor.InsertPluginAtSection(MyLinkPlugin.GetName());
CreateNew()
We can now start working on our plugin. When you call commaEditor.InsertPluginAtSection(....), CommaJS will call the static function CreateNew of your plugin.
static CreateNew(_document, params) { }
_document is the current Document instance.
params is anything you passed as second parameter of commaEditor.InsertPluginAtSection(....). For our MyLinkPlugin we will need an url and a label:
commaEditor.InsertPluginAtSection(MyLinkPlugin.GetName(), {
    url: "http://www.commajs.com",
    label: "Visit CommaJS"
});
Again, the second parameter of InsertPluginAtSection can be a string, an object, or anything else you may need.
We can now start implementing our plugin. Obviously, the best HTML tag to use for hyperlinks is "<a>" but we want to show you features that would not makes sense if we use "<a>". We already mentioned that, when building your own plugin, you should focus on your HTML, CSS and JS first. For our MyLinkPlugin, the plain HTML, CSS and JS will be something like the following:
<span class="myLinkPlugin">Visit CommaJS</span>
Also, we want manage the onClick event:
onClick="window.open("http://www.commajs.com")"
Now that we know what we want to build, we can implement our CreateNew function:
static CreateNew(_document, params) {         
    const url = params.url;
    const label = params.label;

    const elementAttributes = [{name:'anchor-target', value: url }, {name:'class', value: "myLinkPlugin" }];
    
    let currentNodeStyle = _document.GetCurrentStyle(); //we want to use the same style (font family, color, size...)

    let linkNode = TextNode.CreateNew(_document, "span", label, currentNodeStyle, elementAttributes);
    _document.InsertNodeAtCursor(linkNode);

    return new MyLinkPlugin(linkNode);
}
Note that using TextNode, our link's label will be editable by users as normal text. The url is stored in the attribute anchor-target for now.
In this example, the CreateNew function returned an instance of our plugin to be registered in CommaJS. CreateNew can also return an array of instances or nothing (undefined).
constructor()
The constructor of a plugin is a special method for creating and initializing an instance of that plugin. The first important thing to do is to call super() and initialize Plugin, our object's parent.
constructor(rootNode) {
    super(rootNode);
}
One important feature of the original link plugin in CommaJS is the ability to specify a custom onClick function. This is something that you probably won't need to do - you can just build your own functionalities inside your plugin - but it's a great way to show you how to pass information from CommaJS configuration options to your plugin.
To pass a custom onClick function we can change our configuration to:
    ...
    plugins: [
        { 
            pClass: MyLinkPlugin,
            options: { 
                onClick: () => {  }
            }  
        }
    ]
    ...
In our constructor, we can retrieve the options as:
constructor(rootNode) {
    super(rootNode);

    this.options = this.rootNode.document.GetPluginOptions(MyLinkPlugin.GetName());
}
We now have all the ingredients to manage the onClick event for our plugin.
PluginDidMount()
The PluginDidMount is invoked immediately after a plugin is mounted (i.e., inserted into the tree). Initialization that requires DOM nodes should go here.
To manage the onclick event we can simply retrieve the DOM element and add an event listener:
PluginDidMount() {
    let el = this.rootNode.domElement;

    //First, we need to retrieve the url from the dom:
    this.url = el.getAttribute('anchor-target');

    el.addEventListener("click", () => {
        
        //if the user has specified its own onClick event:

        let openNormal = true; 

        if(this.options!=undefined) {
            if(this.options.onClick!=undefined) {
                openNormal = this.options.onClick(this.url); 
            }
        }

        if(openNormal) {
            window.open(this.url, '_blank');    
        }

    });
}
As you can see, the rootNode.domElement property directly exposes the underlying HTML DOM element for an easy access.
Now our plugin should work perfectly. But what happens if the user pastes HTML containing an "<a>" tag?
GetTagToParseOnPaste()
The GetTagToParseOnPaste tells CommaJS to use the CreateNewFromPastedNode function of the plugin when one or more specific tags are paste inside a document.
In our case is just one "a":
static GetTagToParseOnPaste() {
    return ["a"];
}
CreateNewFromPastedNode()
The CreateNewFromPastedNode is called when HTML is pasted inside CommaJS that contains any tag listed in GetTagToParseOnPaste.
In our case "a":
static CreateNewFromPastedNode(_document, element) {
    let url = element.getAttribute("href");
    let label = element.textContent;
    
    return MyLinkPlugin.CreateNew(_document, {
        url: url,
        label: label
    });  
}
Fire()
The last function you may need to implement is Fire. This function passes control to your plugin for all editing events that may occur while a user is writing.
For example, instead of adding an even listener to our element to manage the click event we could have:
Fire(pluginAction, nodeFiring) {
    let res = false;

    switch(pluginAction) {
        case PluginActions.CLICK:
            
            //same code as before

            res = true;
            break;
    }

    return res;
}
If the function returns true, CommaJS will stop the default behavior for that action.
We decided to not follow this approach only because we wanted to show you how to directly access and manipulate the HTML DOM. The two approaches are equivalent, and you can choose the one you prefer.
This is the list of all the supported actions:
PluginActions.CLICK: user clicked on a node of this plugin instance
PluginActions.NEW_CHAR_ADDED: fired after a new char has been added by the user
PluginActions.ENTER: user pressed enter
PluginActions.TAB: user pressed tab
PluginActions.ESC: user pressed esc
PluginActions.DELETE: user pressed delete
PluginActions.LOST_FOCUS: user moved cursor outside this plugin instance
PluginActions.ARROW_UP: user pressed arrow up
PluginActions.ARROW_DOWN: user pressed arrow down
PluginActions.ARROW_LEFT: user pressed arrow left
PluginActions.ARROW_RIGHT: user pressed arrow right
PluginActions.SHIFT_ARROW_UP: user pressed shift and arrow up
PluginActions.SHIFT_ARROW_DOWN: user pressed shift and arrow down
PluginActions.SHIFT_ARROW_LEFT: user pressed shift and arrow left
PluginActions.SHIFT_ARROW_RIGHT: user pressed shift and arrow right
PluginActions.SHIFT_DOWN: user pressed shift and page down
PluginActions.SHIFT_UP: user pressed shift and page up
PluginActions.BEFORE_COPY: fired before a copy event that involves this plugin instance
PluginActions.BEFORE_CUT: fired before a cut event that involves this plugin instance
PluginActions.BEFORE_PASTE: fired before a paste event that involves this plugin instance
Conclusions
This is the full code for our plugin:
import {TextNode, Plugin} from "commajs-core";                
                
export default class MyLinkPlugin extends Plugin {
    constructor(rootNode) {
        super(rootNode);
        this.options = this.rootNode.document.GetPluginOptions(MyLinkPlugin.GetName());		
    }

    OnClick() {
        let openNormal = true; 
    
        if(this.options!=undefined) {
            if(this.options.onClick!=undefined) {
                //if the user has specified its own onClick event:
                openNormal = this.options.onClick(this.url); 
            }
        }

        if(openNormal) {
            window.open(this.url, '_blank');    
        }
    }

    PluginDidMount() {
        let el = this.rootNode.domElement;   
        
        this.url = el.getAttribute('anchor-target'); //url from the dom:
    
        el.addEventListener("click", () => {
            this.OnClick();
        });
    }

    /*
    // In case you want to use the Fire function to manage the onClick event, 
    // remove the event listener from PluginDidMount and uncomment the Fire function
    Fire() {
        let res = false;

        switch(pluginAction) {
            case PluginActions.CLICK:                
                this.OnClick();
                res = true;
                break;
        }

        return res;
    }
    */

    static CreateNew(_document, params) {
        const url = params.url;
        const label = params.label;

        const elementAttributes = [{name:'anchor-target', value: url }];
        
        let currentNodeStyle = _document.GetCurrentStyle();

        let linkNode = TextNode.CreateNew(_document, "span", label, currentNodeStyle, elementAttributes);
        _document.InsertNodeAtCursor(linkNode);

        return new MyLinkPlugin(linkNode);
    }

    static GetName() {
        return "uniquePluginName";
    }

    static GetTagToParseOnPaste() {
        return ["a"];
    }

    static CreateNewFromPastedNode(_document, element) {
        let url = element.getAttribute("href");
        let label = element.textContent;
        
        return MyLinkPlugin.CreateNew(_document, {
            url: url,
            label: label
        });  
    }
}
Overview
GetName()
CreateNew()
constructor()
PluginDidMount()
GetTagToParseOnPaste()
CreateNewFromPastedNode()
Fire()
Conclusions
© 2020 Plugg, Inc. All rights reserved. Various trademarks held by their respective owners.
Plugg, Inc. - San Francisco, CA 94114, United States