-
Notifications
You must be signed in to change notification settings - Fork 164
Best practice
Go back to Create your own plugin
Here are some tips on how to build high quality plugins for RoosterJs.
Plugin is a set of code to provide extended functionalities of the editor, or override the default behavior. So that in theory one plugin can be turned on/off and others should not be impacted. A plugin should have a very clear responsibility that which functionalities it should provide, and which should not, so that turn off one plugin will only turn off the functionalities it provides.
Before you start to write a new plugin, please list what goal you'd like to archieve, divide the goal into separated behaviors, and group the behaviors into several areas, each area should be turned on/off together, one area can be turned off and others should still work, then each area can be implemented by a plugin.
Since any plugin should be able to turn off, any plugin should not take hard dependency to another one, otherwise if plugin is disabled, plugins which depend on that one will not work as well.
Once an event is fired from editor, it will go through all plugins, each plugin will check if it should handle that event. As a result, if any plugin take too long to check, the performace of the whole editor will be impacted.
To save time, plugin should check the cheapest condition first, and let it fail in most cases, then check the next cheap condition. If a plugin needs to do DOM operation to determine whether it should handle the event, this should be the last condition to check.
For example, in HyperLink plugin it needs to check if user is inputting Enter key or Space key after a string looks like a hyperlink, it will turn the text into link. To do the check, we need to check:
- This is a KeyDown event
- The input key is Enter or Space
- The text before cursor is a link
It is very cheap to check "KeyDown" event, we just need to check eventType property of the event object, so we should do this first. To check if the text is a link, we need to do DOM operation which is expensive, we should do it at last.
public onPluginEvent(event: PluginEvent): void {
switch (event.eventType) {
case PluginEventType.KeyDown:
let keyboardEvt = (event as PluginDomEvent).rawEvent as KeyboardEvent;
if (keyboardEvt.which == KEY_ENTER || keyboardEvt.which == KEY_SPACE) {
this.autoLink(event); // We will do DOM operation here
}
break;
}
}
The PluginEvent interface has an event cache object:
interface PluginEvent {
eventType: PluginEventType;
eventDataCache?: { [key: string]: any };
}
This is used for caching some expensive calculation result so that it can be shared between plugins or between willHandleEventExclusively
method and onPluginEvent
method.
Sometimes a plugin wants to do some expensive check to determine if it should handle an event exclusively, then in onPluginEvent method, it needs to use the checking result again, in that case we should not calculate again, but use the cached result in event object.
To store and retrieve cache object, there is an API help you do the job:
function cacheGetEventData<T>(event: PluginEvent, cacheKey: string, eventDataBuilder: () => T): T;
For more details of this API, please see Event Cache API
If there is any async callback inside a plugin, it will be better to check null for editor object before do any action. This is because an editor can be disposed after the event is fired and before the async callback is triggered. Operating an a disposed editor will case a lot of issues.
For example, in ContentEdit plugin we use window.requestAnimationFrame()
method to start an async call. Inside the callback it checks whether editor is still available:
let document = this.editor.getDocument();
document.defaultView.requestAnimationFrame(() => {
if (this.editor) {
let br = document.createElement('br');
this.editor.insertNode(br);
let range = document.createRange();
range.setStartAfter(br);
this.editor.updateSelection(range);
}
});