Custom Plugin Guide

March 16, 2026 · View on GitHub

Technical reference for creating custom SunEditor plugins. Covers all plugin types, hooks, modules, and patterns for both JavaScript and TypeScript.


Table of Contents


Overview

Architecture

All plugins extend a base class from src/interfaces/plugins.js. The inheritance chain:

KernelInjector → Base → PluginCommand / PluginModal / PluginDropdown / ...
  • KernelInjector — Receives the Kernel and exposes this.$ (Deps bag — the shared dependency object, not the Kernel itself).
  • Base — Adds common static properties (key, type, className, options) and instance properties (title, icon, inner, beforeItem, afterItem, replaceButton).
  • Plugin type class — Defines required abstract methods per plugin type.

Key Principles

  1. Class references, not instances — Register plugin classes in options.plugins. The Kernel instantiates them.
  2. Dependency injection — All editor services are accessed via this.$ (the Deps bag), never import core modules directly.
  3. Contracts via interfaces — Plugins can implement multiple contracts (e.g., ModuleModal, EditorComponent) to hook into module lifecycles.

Registration Flow

options.plugins: [MyPlugin]

PluginManager.init()

new MyPlugin(kernel, pluginOptions)  →  super(kernel)  →  this.$ = kernel.$ (Deps bag)

Toolbar buttons updated (title, icon)

Event hooks registered and sorted by priority

Component checkers registered (if static component() exists)

Quick Start

JavaScript

import { PluginCommand } from 'suneditor/src/interfaces';
import { dom } from 'suneditor/src/helper';

class HelloWorld extends PluginCommand {
	static key = 'helloWorld';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Hello World';
		this.icon = '<span style="font-size:14px">HW</span>';
	}

	/**
	 * @override
	 * @type {PluginCommand['action']}
	 */
	action() {
		this.$.html.insert('<p>Hello, World!</p>');
		this.$.history.push(false);
	}
}

export default HelloWorld;

TypeScript

import { PluginCommand } from 'suneditor/src/interfaces';
import type { SunEditor } from 'suneditor/types';

class HelloWorld extends PluginCommand {
	static key = 'helloWorld';

	constructor(kernel: SunEditor.Kernel) {
		super(kernel);
		this.title = 'Hello World';
		this.icon = '<span style="font-size:14px">HW</span>';
	}

	action(): void {
		this.$.html.insert('<p>Hello, World!</p>');
		this.$.history.push(false);
	}
}

export default HelloWorld;

Register

import SUNEDITOR from 'suneditor';
import plugins from 'suneditor/src/plugins';
import HelloWorld from './plugins/helloWorld';

SUNEDITOR.create('editor', {
	plugins: [...plugins, HelloWorld],
	buttonList: [['bold', 'italic', 'helloWorld']],
});

Plugin Types

Source: src/interfaces/plugins.js

Base Classstatic typeRequired MethodsUI BehaviorExamples
PluginCommandcommandaction()Button click executes actionblockquote, list_bulleted
PluginDropdowndropdownaction()Button opens menu, item click calls action()align, font, blockStyle
PluginDropdownFreedropdown-freeButton opens menu, plugin handles own eventstable, fontColor
PluginModalmodalopen()Button opens modal dialoglink, image, video
PluginBrowserbrowseropen(), close()Button opens gallery browserimageGallery
PluginFieldfieldResponds to editor input eventsmention
PluginInputinputToolbar input elementfontSize
PluginPopuppopupshow()Inline popup context menuanchor

PluginCommand

Simplest type. Executes immediately on button click.

import { PluginCommand } from 'suneditor/src/interfaces';
import { dom } from 'suneditor/src/helper';

class ToggleStrikethrough extends PluginCommand {
	static key = 'toggleStrikethrough';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Strikethrough';
		this.icon = 'strikethrough'; // built-in icon key, or raw SVG/HTML
	}

	/**
	 * @hook Editor.EventManager
	 * @type {SunEditor.Hook.Event.Active}
	 */
	active(element, target) {
		if (/^S$/i.test(element?.nodeName)) {
			dom.utils.addClass(target, 'active');
			return true;
		}
		dom.utils.removeClass(target, 'active');
		return false;
	}

	/**
	 * @override
	 * @type {PluginCommand['action']}
	 */
	action() {
		const node = dom.utils.createElement('S');
		this.$.inline.apply(node, { stylesToModify: null, nodesToRemove: null });
	}
}

PluginDropdown

Opens a dropdown menu. on() is called when the menu opens, action() when an item is clicked.

import { PluginDropdown } from 'suneditor/src/interfaces';
import { dom } from 'suneditor/src/helper';

/**
 * @typedef {Object} CustomAlignPluginOptions
 * @property {Array.<"right"|"center"|"left"|"justify">} [items] - Align items
 */

class CustomAlign extends PluginDropdown {
	static key = 'customAlign';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 * @param {CustomAlignPluginOptions} pluginOptions
	 */
	constructor(kernel, pluginOptions) {
		super(kernel);
		this.title = this.$.lang.align;
		this.icon = 'align_left';

		// Build dropdown HTML
		const menu = dom.utils.createElement(
			'div',
			{ class: 'se-dropdown se-list-layer' },
			`<div class="se-list-inner">
        <ul class="se-list-basic">
          <li><button type="button" class="se-btn se-btn-list" data-command="left">Left</button></li>
          <li><button type="button" class="se-btn se-btn-list" data-command="center">Center</button></li>
          <li><button type="button" class="se-btn se-btn-list" data-command="right">Right</button></li>
        </ul>
      </div>`,
		);

		// Register dropdown target
		this.$.menu.initDropdownTarget(CustomAlign, menu);
	}

	/**
	 * @override
	 * @type {PluginDropdown['on']}
	 */
	on(target) {
		// Called when dropdown opens. Update active states.
	}

	/**
	 * @override
	 * @type {PluginDropdown['action']}
	 */
	action(target) {
		const value = target.getAttribute('data-command');
		if (!value) return;

		const lines = this.$.format.getLines();
		for (const line of lines) {
			dom.utils.setStyle(line, 'textAlign', value);
		}

		this.$.menu.dropdownOff();
		this.$.focusManager.focus();
		this.$.history.push(false);
	}
}

PluginDropdownFree

Like dropdown, but the plugin handles its own event logic. No automatic action() dispatch.

import { PluginDropdownFree } from 'suneditor/src/interfaces';

class CustomPicker extends PluginDropdownFree {
  static key = 'customPicker';

  /**
   * @constructor
   * @param {SunEditor.Kernel} kernel - The Kernel instance
   */
  constructor(kernel) {
    super(kernel);
    this.title = 'Custom Picker';
    this.icon = 'color';

    const menu = /* build your custom UI */;
    this.$.menu.initDropdownTarget(CustomPicker, menu);

    // Attach your own event listeners
	this.$.eventManager.addEvent(menu, 'click', this.#handleClick.bind(this));
  }

  on(target) {
    // Called when dropdown opens
  }

  off() {
    // Called when dropdown closes — cleanup state
  }

  #handleClick(e) {
    // Your own event handling logic
    this.$.menu.dropdownOff();
  }
}

PluginModal

Opens a modal dialog. Use with the Modal module.

import { PluginModal } from 'suneditor/src/interfaces';
import Modal from 'suneditor/src/modules/contract/Modal';
import { dom } from 'suneditor/src/helper';

class InsertCode extends PluginModal {
	static key = 'insertCode';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Insert Code';
		this.icon = 'code';

		// Build modal HTML
		const modalEl = dom.utils.createElement(
			'div',
			null,
			`<form>
        <div class="se-modal-header"><button type="button" data-command="close" class="se-btn se-modal-close"></button>
          <span class="se-modal-title">Insert Code</span>
        </div>
        <div class="se-modal-body">
          <textarea class="se-input-form" style="height:200px"></textarea>
        </div>
        <div class="se-modal-footer">
          <button type="submit" class="se-btn-primary"><span>Insert</span></button>
        </div>
      </form>`,
		);

		this.modal = new Modal(this, this.$, modalEl);
		this.textarea = modalEl.querySelector('textarea');
	}

	/**
	 * @override
	 * @type {PluginModal['open']}
	 */
	open() {
		this.modal.open();
	}

	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.Action}
	 */
	async modalAction() {
		const code = this.textarea.value;
		if (!code) return false; // close loading only

		const pre = dom.utils.createElement('PRE');
		const codeEl = dom.utils.createElement('CODE');
		codeEl.textContent = code;
		pre.appendChild(codeEl);

		this.$.html.insert(pre.outerHTML);
		this.$.history.push(false);
		return true; // close modal + loading
	}

	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.On}
	 */
	modalOn(isUpdate) {
		if (!isUpdate) this.textarea.value = '';
		this.textarea.focus();
	}

	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.Off}
	 */
	modalOff() {
		this.textarea.value = '';
	}
}

PluginBrowser

Opens a gallery/browser interface. Requires open() and close().

import { PluginBrowser } from 'suneditor/src/interfaces';
import Browser from 'suneditor/src/modules/contract/Browser';

class MyGallery extends PluginBrowser {
	static key = 'myGallery';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'My Gallery';
		this.icon = 'image';

		this.browser = new Browser(this, this.$ /* browser config */);
	}

	open(onSelectFunction) {
		this.browser.open(onSelectFunction);
	}

	close() {
		this.browser.close();
	}
}

PluginField

Responds to editor input events. Commonly uses onInput hook to detect trigger patterns.

import { PluginField } from 'suneditor/src/interfaces';
import { converter } from 'suneditor/src/helper';

class HashtagDetector extends PluginField {
	static key = 'hashtagDetector';
	static className = '';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.onInput = converter.debounce(this.onInput.bind(this), 200);
	}

	/**
	 * @hook Editor.EventManager
	 * @type {SunEditor.Hook.Event.OnInput}
	 */
	onInput({ frameContext }) {
		const sel = this.$.selection.get();
		const text = sel.anchorNode?.textContent || '';
		const before = text.substring(0, sel.anchorOffset);
		const match = before.match(/#(\w+)$/);

		if (match) {
			// Handle hashtag detection
			console.log('Detected hashtag:', match[1]);
		}
	}
}

PluginInput

Adds an input element to the toolbar instead of a button.

import { PluginInput } from 'suneditor/src/interfaces';

class CustomInput extends PluginInput {
	static key = 'customInput';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Custom Input';
	}

	/**
	 * @override
	 * @type {PluginInput['toolbarInputKeyDown']}
	 */
	toolbarInputKeyDown({ target, event }) {
		if (event.key === 'Enter') {
			event.preventDefault();
			const value = target.value;
			// Handle the input value
		}
	}

	/**
	 * @override
	 * @type {PluginInput['toolbarInputChange']}
	 */
	toolbarInputChange({ target, value }) {
		// Handle input blur/change
	}
}

PluginPopup

Shows inline popup menus (e.g., for link preview).

import { PluginPopup } from 'suneditor/src/interfaces';

class InfoPopup extends PluginPopup {
	static key = 'infoPopup';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Info';
	}

	show() {
		// Display popup UI
	}
}

Static Properties

Every plugin class can define these static properties:

PropertyTypeRequiredDescription
keystringYesUnique plugin identifier. Must match the name used in buttonList.
typestringInheritedSet by the base class. Do not override.
classNamestringNoCSS class added to the plugin's toolbar button.
optionsobjectNoPlugin behavior options. See below.
component(node)functionNo*Static method to detect component DOM nodes. *Required if implementing EditorComponent.

static options

class MyPlugin extends PluginField {
	static options = {
		eventIndex: 100, // Default priority for all event hooks (lower = earlier)
		eventIndex_onKeyDown: 50, // Per-event override
		eventIndex_onInput: 200, // Higher = later execution
		isInputComponent: true, // Allow keyboard input inside component (e.g., table cells)
	};
}

static component(node)

Required for plugins implementing the EditorComponent contract. Returns the component element if the node belongs to this plugin, or null otherwise.

class MyImagePlugin extends PluginModal {
	static component(node) {
		return /^IMG$/i.test(node?.nodeName) ? node : null;
	}
}

Constructor Pattern

Plugin options are defined as a @typedef above the class, and the constructor receives kernel (Kernel instance) + pluginOptions:

/**
 * @typedef {Object} MyPluginOptions
 * @property {boolean} [canResize=true] - Whether the element can be resized.
 * @property {string} [defaultWidth="auto"] - The default width.
 */

/**
 * @class
 * @description MyPlugin description.
 */
class MyPlugin extends PluginModal {
	static key = 'myPlugin';
	static className = 'se-btn-my-plugin';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 * @param {MyPluginOptions} pluginOptions
	 */
	constructor(kernel, pluginOptions) {
		super(kernel); // KernelInjector → this.$ = kernel.$ (Deps bag)

		// Plugin metadata (used by toolbar button)
		this.title = this.$.lang.myPlugin || 'My Plugin';
		this.icon = 'myPlugin'; // icon key from this.$.icons, or raw HTML/SVG

		// Optional: toolbar button content and layout
		this.inner = null; // string (HTML) | HTMLElement | false (hide) | null (use icon)
		this.beforeItem = null; // HTMLElement to insert before the button
		this.afterItem = null; // HTMLElement to insert after the button
		this.replaceButton = null; // HTMLElement to replace the entire default button

		// Plugin members
		this.myState = {};

		// Module instances (if using Modal, Controller, etc.)
		this.modal = new Modal(this, this.$, modalElement);
		this.controller = new Controller(this, this.$, controllerElement, { position: 'bottom' });
	}
}

Parameters:

  • kernel (SunEditor.Kernel) — The Kernel instance (runtime container). Pass to super() to inject this.$ (Deps bag).
  • pluginOptions (object) — Plugin-specific options from options[pluginKey]. Define a @typedef for type checking.

Dependency Bag (this.$)

All plugins access editor services through this.$ (the Deps bag). This shared dependency object is built once by the Kernel (CoreKernel) and provided to all consumers. $ is not the Kernel itself — it is the dependency context that the Kernel provides.

Source: src/core/kernel/kernelInjector.js

Config

PropertyTypeDescription
optionsMapGlobal editor options (shared across all frames)
frameOptionsMapCurrent frame's options (width, height, placeholder, etc.)
contextMapGlobal context (toolbar, statusbar, modal overlay elements)
frameContextMapCurrent frame context (wysiwyg, code, readonly state, etc.)
frameRootsMapAll frame contexts keyed by rootKey
langobjectLanguage strings (e.g., this.$.lang.image)
iconsobjectIcon HTML strings (e.g., this.$.icons.bold)

DOM Logic

PropertyDescription
selectionSelection and range manipulation
htmlHTML get/set, insert, sanitization
formatBlock-level formatting (applyBlock, removeBlock, getLines)
inlineInline formatting (bold, italic, styles)
listFormatList operations (create, edit, nested)
nodeTransformDOM node transformations
charCharacter counting and limits
offsetPosition calculations

Shell Logic

PropertyDescription
componentComponent lifecycle (select, deselect, setInfo)
focusManagerFocus/blur management
pluginManagerPlugin registry and lifecycle
pluginsPlugin instances map (e.g., this.$.plugins.image)
uiUI state (loading, alerts, toast, theme)
commandDispatcherCommand routing and execution
historyUndo/redo stack (push, undo, redo)
shortcutsKeyboard shortcut mapping

Panel Logic

PropertyDescription
toolbarMain toolbar renderer and positioning
subToolbarSub-toolbar (only with _subMode)
menuDropdown menu management (initDropdownTarget, dropdownOff)
viewerView modes (code view, fullscreen, preview)

Services

PropertyDescription
eventManagerPublic event API (addEvent, removeEvent, triggerEvent)
contextProviderContext/FrameContext Map management
optionProviderOptions/FrameOptions Map management
instanceCheckIframe-safe instanceof checks
storeCentral runtime state store
facadeThe editor public API instance

Hooks Reference

Hooks are methods that the editor core or modules call on plugin instances at specific lifecycle points.

JSDoc Type Annotations

All hook methods should be annotated with JSDoc tags to enable type checking and IDE support. There are three annotation patterns:

@hook + @type — Hook Methods

Used for methods called by the editor core or modules. The @hook tag indicates which system calls the method, and @type provides the type signature.

/**
 * @hook Editor.EventManager
 * @type {SunEditor.Hook.Event.Active}
 */
active(element, target) { ... }

/**
 * @hook Editor.EventManager
 * @type {SunEditor.Hook.Event.OnKeyDown}
 */
onKeyDown({ frameContext, event, range, line }) { ... }

/**
 * @hook Editor.Core
 * @type {SunEditor.Hook.Core.Shortcut}
 */
shortcut({ range, info }) { ... }

/**
 * @hook Modules.Modal
 * @type {SunEditor.Hook.Modal.Action}
 */
async modalAction() { ... }

/**
 * @hook Editor.Component
 * @type {SunEditor.Hook.Component.Select}
 */
componentSelect(target) { ... }

Available @hook categories and their @type namespaces:

@hook Category@type NamespaceMethods
Editor.EventManagerSunEditor.Hook.Event.*Active, OnKeyDown, OnInput, OnClick, OnPaste, OnFocus, OnBlur ...
Editor.CoreSunEditor.Hook.Core.*RetainFormat, Shortcut, SetDir, Init
Editor.ComponentSunEditor.Hook.Component.*Select, Deselect, Edit, Destroy, Copy
Modules.ModalSunEditor.Hook.Modal.*Action, On, Init, Off, Resize
Modules.ControllerSunEditor.Hook.Controller.*Action, On, Close
Modules.ColorPickerSunEditor.Hook.ColorPicker.*Action, HueSliderOpen, HueSliderClose
Modules.HueSliderSunEditor.Hook.HueSlider.*Action, CancelAction

@override + @type — Base Class Method Overrides

Used when overriding required/optional methods from the base plugin class:

/**
 * @override
 * @type {PluginModal['open']}
 */
open(target) { ... }

/**
 * @override
 * @type {PluginInput['toolbarInputKeyDown']}
 */
toolbarInputKeyDown({ target, event }) { ... }

@imple + @type — Cross-Plugin Interface Methods

Used when a plugin implements methods from another plugin type via @implements (see extends vs implements):

/**
 * @imple Command
 * @type {PluginCommand['action']}
 */
action(target) { ... }

/**
 * @imple Dropdown
 * @type {PluginDropdown['on']}
 */
on(target) { ... }

Common Hooks (All Plugins)

Source: src/hooks/base.jsCore object

These can be implemented by any plugin type.

HookWhen CalledReturnDescription
active(element, target)Cursor position changesboolean | undefinedUpdate toolbar button active state. Return true if active, false if not, undefined to stop calling for this scope.
init()Editor init / resetOptionsvoidRe-initialize plugin state.
retainFormat()HTML cleaning/validation{query, method}Return a CSS selector query and a method(element) to validate/preserve component format.
shortcut(params)Shortcut key triggeredvoidHandle custom keyboard shortcuts. Params: { range, line, info, event, keyCode, $ }
setDir(dir)RTL/LTR direction changevoidAdjust plugin UI for direction change. dir is 'rtl' or 'ltr'.

active() example:

/**
 * @hook Editor.EventManager
 * @type {SunEditor.Hook.Event.Active}
 */
active(element, target) {
  if (/^BLOCKQUOTE$/i.test(element?.nodeName)) {
    dom.utils.addClass(target, 'active');
    return true;
  }
  dom.utils.removeClass(target, 'active');
  return false;
}

Event Hooks (All Plugins)

Source: src/hooks/base.jsEvent object

Any plugin can implement event hooks. Method names use the lowercase on prefix.

Interruptible Events — Returning a boolean stops the event hook loop:

  • false — Stops remaining plugins and prevents default editor behavior
  • true — Stops remaining plugins, allows default editor behavior
  • void / undefined — Continues to next plugin
HookInterruptibleParams TypeDescription
onKeyDownYesHookParams.KeyEventKey down in editor
onKeyUpYesHookParams.KeyEventKey up in editor
onMouseDownYesHookParams.MouseEventMouse down in editor
onClickYesHookParams.MouseEventClick in editor
onPasteYesHookParams.PastePaste event
onBeforeInputNoHookParams.InputWithDataBefore input processing
onInputNoHookParams.InputWithDataAfter input processing
onMouseUpNoHookParams.MouseEventMouse up in editor
onMouseMoveNoHookParams.MouseEventMouse move in editor
onMouseLeaveNoHookParams.MouseEventMouse leave editor
onScrollNoHookParams.ScrollEditor scroll
onFocusNoHookParams.FocusBlurEditor focus
onBlurNoHookParams.FocusBlurEditor blur
onFilePasteAndDropNoHookParams.FilePasteDropFile paste/drop

Parameter Types (from src/hooks/params.js):

TypeProperties
HookParams.MouseEvent{ frameContext, event }
HookParams.KeyEvent{ frameContext, event, range, line }
HookParams.FocusBlur{ frameContext, event }
HookParams.Scroll{ frameContext, event }
HookParams.InputWithData{ frameContext, event, data }
HookParams.Paste{ frameContext, event, data, doc }
HookParams.FilePasteDrop{ frameContext, event, file }
HookParams.Shortcut{ range, line, info, event, keyCode, $ }
HookParams.CopyComponent{ event, cloneContainer, info }

Async variants: Every event hook can be async. The PluginManager calls both sync and async versions.

Execution priority:

class MyPlugin extends PluginField {
	static options = {
		eventIndex: 100, // Default priority (lower = earlier)
		eventIndex_onKeyDown: 50, // Override: run onKeyDown earlier
		eventIndex_onInput: 200, // Override: run onInput later
	};

	/**
	 * @hook Editor.EventManager
	 * @type {SunEditor.Hook.Event.OnKeyDown}
	 */
	onKeyDown({ event, range }) {
		if (event.key === 'Tab') {
			event.preventDefault();
			this.$.html.insert('    ');
			return false; // Stop further processing
		}
	}
}

Module Hooks (Contract Interfaces)

Source: src/interfaces/contracts.js

When a plugin uses a module (Modal, Controller, etc.), it implements the corresponding contract interface to receive callbacks.

ModuleModal

For plugins using the Modal module.

HookRequiredWhen CalledReturn
modalAction()YesForm submitPromise<boolean>true: close modal + loading, false: close loading only, undefined: close modal only
modalOn(isUpdate)NoAfter modal opensvoidisUpdate: editing existing (true) vs creating new (false)
modalInit()NoBefore modal opens/closesvoid
modalOff(isUpdate)NoAfter modal closesvoid
modalResize()NoModal window resizedvoid

ModuleController

For plugins using the Controller module (floating toolbar).

HookRequiredWhen CalledReturn
controllerAction(target)YesController button clickedvoidtarget: the clicked button element
controllerOn(form, target)NoAfter controller opensvoid
controllerClose()NoBefore controller closesvoid

ModuleColorPicker

For plugins using the ColorPicker module.

HookRequiredWhen CalledReturn
colorPickerAction(color)NoColor selectedvoid
colorPickerHueSliderOpen()NoHue slider openedvoid
colorPickerHueSliderClose()NoHue slider cancelledvoid

ModuleHueSlider

For plugins using the HueSlider module.

HookRequiredWhen CalledReturn
hueSliderAction()YesColor selected in slidervoid
hueSliderCancelAction()NoHue slider cancelledvoid

ModuleBrowser

For plugins using the Browser module.

HookRequiredWhen CalledReturn
browserInit()NoBrowser openedvoid

Component Hooks (EditorComponent Interface)

Source: src/interfaces/contracts.js

For plugins that create static components in the editor (e.g., images, videos, tables, embeds).

Requirements:

  1. Define static component(node) — Returns the component element if the node belongs to this plugin, or null.
  2. Define a public _element property — References the currently controlled DOM element. Used to detect clicks and prevent accidental controller closure.
HookRequiredWhen CalledReturn
componentSelect(target)YesComponent selected (clicked)void | boolean — Return true for special non-figure components
componentDeselect(target)NoComponent deselectedvoid
componentEdit(target)NoComponent edit button clickedvoid
componentDestroy(target)NoComponent deletedPromise<void>
componentCopy(params)NoComponent copy requestedboolean | void — Return false to cancel copy

Component detection flow:

User clicks element in editor

PluginManager.findComponentInfo(element)

Calls each plugin's static component(node) method

First non-null result wins → { target, pluginName, options }

plugin.componentSelect(target) called

Multi-Interface Pattern

A single plugin can combine a base type with multiple contract interfaces. This is useful for plugins that need a modal dialog, a floating controller, and component lifecycle management.

TypeScript

Use implements to compose interfaces:

import { interfaces } from 'suneditor';
import type { SunEditor } from 'suneditor/types';
import Modal from 'suneditor/src/modules/contract/Modal';
import Controller from 'suneditor/src/modules/contract/Controller';

class CustomEmbed extends interfaces.PluginModal
  implements interfaces.ModuleModal, interfaces.ModuleController, interfaces.EditorComponent
{
  static key = 'customEmbed';

  _element: HTMLElement | null = null;

  modal: InstanceType<typeof Modal>;
  controller: InstanceType<typeof Controller>;

  constructor(kernel: SunEditor.Kernel) {
    super(kernel);
    this.title = 'Custom Embed';
    this.icon = 'embed';

    const modalEl = /* build modal HTML */;
    const controllerEl = /* build controller HTML */;

    this.modal = new Modal(this, this.$, modalEl);
    this.controller = new Controller(this, this.$, controllerEl);
  }

  // Static: detect embed components
  static component(node: Node): Node | null {
    return /^IFRAME$/i.test(node?.nodeName) ? node : null;
  }

  /** @override PluginModal */
  open(target?: HTMLElement): void {
    this.modal.open();
  }

  /** @hook Modules.Modal — Action */
  async modalAction(): Promise<boolean> {
    // Handle form submission
    this.$.history.push(false);
    return true;
  }

  /** @hook Modules.Modal — On */
  modalOn(isUpdate: boolean): void {
    // Initialize modal state
  }

  /** @hook Modules.Modal — Off */
  modalOff(isUpdate: boolean): void {
    // Cleanup
  }

  /** @hook Modules.Controller — Action */
  controllerAction(target: HTMLElement): void {
    const command = target.getAttribute('data-command');
    if (command === 'edit') this.modal.open();
    if (command === 'delete') this.componentDestroy(this._element!);
  }

  /** @hook Modules.Controller — Close */
  controllerClose(): void {
    // Cleanup on controller close
  }

  /** @hook Editor.Component — Select */
  componentSelect(target: HTMLElement): void {
    this._element = target;
    this.controller.open(target, null, { isWWTarget: false });
  }

  /** @hook Editor.Component — Deselect */
  componentDeselect(target: HTMLElement): void {
    this._element = null;
  }

  /** @hook Editor.Component — Destroy */
  async componentDestroy(target: HTMLElement): Promise<void> {
    const container = target.parentElement;
    container?.remove();
    this._element = null;
    this.$.focusManager.focus();
    this.$.history.push(false);
  }
}

JavaScript

In JavaScript, simply implement the methods — no implements keyword needed. The editor calls methods by name regardless of declared interfaces.

import { PluginModal } from 'suneditor/src/interfaces';
import Modal from 'suneditor/src/modules/contract/Modal';
import Controller from 'suneditor/src/modules/contract/Controller';

class CustomEmbed extends PluginModal {
	static key = 'customEmbed';

	static component(node) {
		return /^IFRAME$/i.test(node?.nodeName) ? node : null;
	}

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this._element = null;
		this.modal = new Modal(this, this.$, modalEl);
		this.controller = new Controller(this, this.$, controllerEl);
	}

	/**
	 * @override
	 * @type {PluginModal['open']}
	 */
	open() {
		this.modal.open();
	}
	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.Action}
	 */
	async modalAction() {
		/* ... */ return true;
	}
	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.On}
	 */
	modalOn(isUpdate) {
		/* ... */
	}
	/**
	 * @hook Modules.Modal
	 * @type {SunEditor.Hook.Modal.Off}
	 */
	modalOff(isUpdate) {
		/* ... */
	}
	/**
	 * @hook Modules.Controller
	 * @type {SunEditor.Hook.Controller.Action}
	 */
	controllerAction(target) {
		/* ... */
	}
	/**
	 * @hook Editor.Component
	 * @type {SunEditor.Hook.Component.Select}
	 */
	componentSelect(target) {
		this._element = target;
	}
	/**
	 * @hook Editor.Component
	 * @type {SunEditor.Hook.Component.Deselect}
	 */
	componentDeselect(target) {
		this._element = null;
	}
	/**
	 * @hook Editor.Component
	 * @type {SunEditor.Hook.Component.Destroy}
	 */
	async componentDestroy(target) {
		/* ... */
	}
}

Key insight: TypeScript implements only provides compile-time type checking — it enforces that you implement all required methods with correct signatures. At runtime, the behavior is identical to JavaScript.

extends vs implements

A plugin uses extends and implements for different purposes:

KeywordPurposeMultiplicity
extendsInherit from a plugin base class (determines the plugin type)Exactly one
implementsCompose additional interfaces (module contracts or other plugin types)Zero or more

1. extends — Plugin Base Type (single inheritance)

Every plugin extends exactly one base class. This determines its primary type and lifecycle:

extends PluginModal    → type: 'modal'    (required: open())
extends PluginCommand  → type: 'command'  (required: action())
extends PluginInput    → type: 'input'    (optional: toolbarInputKeyDown/Change)
extends PluginDropdown → type: 'dropdown' (required: action())

2. implements — Module Contracts

Plugins implements module contracts to hook into module lifecycles:

class Image extends PluginModal
  implements ModuleModal, ModuleController, EditorComponent { ... }
  • ModuleModalmodalAction(), modalOn(), modalOff()
  • ModuleControllercontrollerAction(), controllerOn()
  • EditorComponentcomponentSelect(), componentDestroy()

3. implements — Cross-Plugin-Type Composition

A plugin can also implements other plugin type interfaces to provide multiple interaction modes. The fontSize plugin is a representative example:

FontSize extends PluginInput          ← base type (toolbar input)
  @implements {PluginCommand}         ← provides action() for inc/dec buttons
  @implements {PluginDropdown}        ← provides on() for dropdown menu

JavaScript — Use @implements JSDoc tags for type hints:

import { PluginCommand, PluginDropdown, PluginInput } from 'suneditor/src/interfaces';

void PluginCommand;
void PluginDropdown;

/**
 * @implements {PluginCommand}
 * @implements {PluginDropdown}
 */
class FontSize extends PluginInput {
	static key = 'fontSize';

	// PluginInput base
	toolbarInputKeyDown(params) {
		/* handle arrow keys, enter */
	}
	toolbarInputChange(params) {
		/* apply typed value */
	}

	// PluginCommand (implements) — inc/dec button clicks
	action(target) {
		/* adjust font size */
	}

	// PluginDropdown (implements) — dropdown open
	on(target) {
		/* highlight active size in list */
	}
}

TypeScript — Use implements keyword:

import { interfaces } from 'suneditor';
import type { SunEditor } from 'suneditor/types';

class FontSize extends interfaces.PluginInput
  implements interfaces.PluginCommand, interfaces.PluginDropdown
{
  static key = 'fontSize';

  toolbarInputKeyDown(params: SunEditor.HookParams.ToolbarInputKeyDown): void { ... }
  toolbarInputChange(params: SunEditor.HookParams.ToolbarInputChange): void { ... }
  action(target: HTMLElement): void { ... }
  on(target: HTMLElement): void { ... }
}

When to use cross-plugin implements: When a single plugin provides multiple interaction modes (e.g., an input field + dropdown menu + command buttons all controlling the same feature). The base extends determines the primary type; implements adds methods from other plugin types that the editor calls by name.


Modules Reference

Source: src/modules/

Modules are UI components that plugins instantiate manually. They are not auto-registered.

ModuleImport PathConstructorPurpose
Modalmodules/contract/Modalnew Modal(inst, $, element)Dialog windows
Controllermodules/contract/Controllernew Controller(inst, $, element, options?)Floating tooltip controllers
Figuremodules/contract/Figurenew Figure(inst, $, controls, options?)Resize/align wrapper for components
ColorPickermodules/contract/ColorPickernew ColorPicker(inst, $, ...)Color palette UI
HueSlidermodules/contract/HueSlidernew HueSlider(inst, $, ...)HSL color wheel
Browsermodules/contract/Browsernew Browser(inst, $, ...)Gallery/file browser UI
FileManagermodules/manager/FileManagernew FileManager(inst, $, options)File upload management
ApiManagermodules/manager/ApiManagernew ApiManager(inst, $, ...)XHR/fetch request management
SelectMenumodules/ui/SelectMenunew SelectMenu(...)Custom dropdown select menus
ModalAnchorEditormodules/ui/ModalAnchorEditornew ModalAnchorEditor($, modal, options)Link/anchor editing form

Constructor pattern: All contract modules receive:

  1. inst — The plugin instance (for calling hook methods back on the plugin)
  2. $ — The deps bag
  3. Module-specific parameters (HTML element, options, etc.)

Complete Examples

Example 1: Word Count Command (JavaScript)

A command plugin that shows the current word count.

import { PluginCommand } from 'suneditor/src/interfaces';

class WordCount extends PluginCommand {
	static key = 'wordCount';

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Word Count';
		this.icon = '<span style="font-size:12px;font-weight:bold">WC</span>';
	}

	/**
	 * @override
	 * @type {PluginCommand['action']}
	 */
	action() {
		const text = this.$.html.get({ format: 'text' });
		const words = text.trim().split(/\s+/).filter(Boolean).length;
		this.$.ui.showToast(`Words: ${words}`, 2000);
	}
}

export default WordCount;

Example 2: Custom Block Style Dropdown (JavaScript)

A dropdown plugin that applies predefined block styles.

import { PluginDropdown } from 'suneditor/src/interfaces';
import { dom } from 'suneditor/src/helper';

class QuickStyle extends PluginDropdown {
	static key = 'quickStyle';

	#styles = [
		{ name: 'Note', class: 'note-block', bg: '#e8f5e9' },
		{ name: 'Warning', class: 'warning-block', bg: '#fff3e0' },
		{ name: 'Info', class: 'info-block', bg: '#e3f2fd' },
	];

	/**
	 * @constructor
	 * @param {SunEditor.Kernel} kernel - The Kernel instance
	 */
	constructor(kernel) {
		super(kernel);
		this.title = 'Quick Style';
		this.icon = 'blockStyle';

		let html = '';
		for (const style of this.#styles) {
			html += `<li><button type="button" class="se-btn se-btn-list" data-command="${style.class}"
        style="background:${style.bg};padding:4px 8px">${style.name}</button></li>`;
		}

		const menu = dom.utils.createElement('div', { class: 'se-dropdown se-list-layer' }, `<div class="se-list-inner"><ul class="se-list-basic">${html}</ul></div>`);

		this.$.menu.initDropdownTarget(QuickStyle, menu);
	}

	/**
	 * @override
	 * @type {PluginDropdown['action']}
	 */
	action(target) {
		const className = target.getAttribute('data-command');
		if (!className) return;

		const lines = this.$.format.getLines();
		for (const line of lines) {
			dom.utils.toggleClass(line, className);
		}

		this.$.menu.dropdownOff();
		this.$.focusManager.focus();
		this.$.history.push(false);
	}
}

export default QuickStyle;

Example 3: Custom Embed Modal with Controller (TypeScript)

A modal plugin with a controller for editing embedded iframes.

import { interfaces } from 'suneditor';
import type { SunEditor } from 'suneditor/types';
import Modal from 'suneditor/src/modules/contract/Modal';
import Controller from 'suneditor/src/modules/contract/Controller';
import { dom } from 'suneditor/src/helper';

class Embed extends interfaces.PluginModal implements interfaces.ModuleModal, interfaces.ModuleController, interfaces.EditorComponent {
	static key = 'embed';

	_element: HTMLIFrameElement | null = null;
	#isUpdate = false;

	modal: InstanceType<typeof Modal>;
	controller: InstanceType<typeof Controller>;
	urlInput: HTMLInputElement;

	static component(node: Node): Node | null {
		const el = dom.check.isFigure(node) ? (node as HTMLElement).firstElementChild : node;
		return /^IFRAME$/i.test(el?.nodeName ?? '') ? el : null;
	}

	constructor(kernel: SunEditor.Kernel) {
		super(kernel);
		this.title = 'Embed';
		this.icon = 'embed';

		// Modal HTML
		const modalEl = dom.utils.createElement(
			'div',
			null,
			`<form>
        <div class="se-modal-header">
          <button type="button" data-command="close" class="se-btn se-modal-close"></button>
          <span class="se-modal-title">Embed URL</span>
        </div>
        <div class="se-modal-body">
          <label>URL</label>
          <input class="se-input-form" type="url" placeholder="https://..." />
        </div>
        <div class="se-modal-footer">
          <button type="submit" class="se-btn-primary"><span>Insert</span></button>
        </div>
      </form>`,
		);

		// Controller HTML
		const controllerEl = dom.utils.createElement(
			'div',
			{ class: 'se-controller' },
			`<div>
        <button type="button" data-command="edit" class="se-btn" title="Edit">Edit</button>
        <button type="button" data-command="delete" class="se-btn" title="Delete">Delete</button>
      </div>`,
		);

		this.modal = new Modal(this, this.$, modalEl);
		this.controller = new Controller(this, this.$, controllerEl);
		this.urlInput = modalEl.querySelector('input')!;
	}

	/** @override PluginModal */
	open(): void {
		this.modal.open();
	}

	/** @hook Modules.Modal — Action */
	async modalAction(): Promise<boolean> {
		const url = this.urlInput.value.trim();
		if (!url) return false;

		if (this.#isUpdate && this._element) {
			this._element.src = url;
		} else {
			const iframe = dom.utils.createElement('IFRAME', {
				src: url,
				width: '560',
				height: '315',
				frameborder: '0',
				allowfullscreen: 'true',
			}) as HTMLIFrameElement;
			this.$.html.insert(iframe.outerHTML);
		}

		this.$.history.push(false);
		return true;
	}

	/** @hook Modules.Modal — On */
	modalOn(isUpdate: boolean): void {
		this.#isUpdate = isUpdate;
		this.urlInput.value = isUpdate && this._element ? this._element.src : '';
		this.urlInput.focus();
	}

	/** @hook Modules.Modal — Init */
	modalInit(): void {
		this.controller.close();
	}

	/** @hook Modules.Modal — Off */
	modalOff(): void {
		this.urlInput.value = '';
	}

	/** @hook Modules.Controller — Action */
	controllerAction(target: HTMLElement): void {
		const command = target.getAttribute('data-command');
		if (command === 'edit') {
			this.modal.open();
		} else if (command === 'delete') {
			this.componentDestroy(this._element!);
		}
	}

	/** @hook Editor.Component — Select */
	componentSelect(target: HTMLElement): void {
		this._element = target as HTMLIFrameElement;
		this.controller.open(target, null, { isWWTarget: false });
	}

	/** @hook Editor.Component — Deselect */
	componentDeselect(): void {
		this._element = null;
	}

	/** @hook Editor.Component — Destroy */
	async componentDestroy(target: HTMLElement): Promise<void> {
		const container = dom.query.getParentElement(target, dom.check.isFigure) || target;
		const focusEl = container.previousElementSibling || container.nextElementSibling;
		dom.utils.removeItem(container);
		this._element = null;
		this.$.focusManager.focusEdge(focusEl);
		this.$.history.push(false);
	}
}

export default Embed;

Plugin Registration

Options Format

Plugins are registered as class references in options.plugins:

import SUNEDITOR from 'suneditor';
import plugins from 'suneditor/src/plugins';
import MyPlugin from './plugins/myPlugin';
import AnotherPlugin from './plugins/anotherPlugin';

// Array format (recommended)
SUNEDITOR.create('editor', {
	plugins: [...plugins, MyPlugin, AnotherPlugin],
	buttonList: [['bold', 'italic', 'myPlugin', 'anotherPlugin']],
});

// Object format
SUNEDITOR.create('editor', {
	plugins: { ...plugins, myPlugin: MyPlugin, anotherPlugin: AnotherPlugin },
	buttonList: [['bold', 'italic', 'myPlugin', 'anotherPlugin']],
});

Plugin Options

Pass plugin-specific options via options[pluginKey]:

SUNEDITOR.create('editor', {
	plugins: [MyPlugin],
	buttonList: [['myPlugin']],
	myPlugin: {
		maxItems: 10,
		apiUrl: '/api/data',
	},
});

These options are passed as the second argument to the constructor: constructor(kernel, pluginOptions).

Registration Rules

  1. Always pass class references — The kernel manages instantiation and lifecycle.

    // Correct
    plugins: [MyPlugin];
    
    // Wrong — kernel cannot manage lifecycle
    plugins: [new MyPlugin()];
    
  2. static key must match buttonList name — The toolbar maps button names to plugin keys.

  3. Plugins without toolbar buttonsPluginField plugins (like mention) don't need to appear in buttonList. They are registered and respond to editor events automatically.


Built-in Reference Implementations

For studying real-world implementations:

PluginTypeComplexityFile
BlockquoteCommandSimplesrc/plugins/command/blockquote.js
AlignDropdownSimplesrc/plugins/dropdown/align.js
FontDropdownMediumsrc/plugins/dropdown/font.js
LinkModal + ControllerMediumsrc/plugins/modal/link.js
ImageModal + Component + FileManagerComplexsrc/plugins/modal/image/index.js
VideoModal + Component + FileManagerComplexsrc/plugins/modal/video/index.js
TableDropdownFree + Component + ControllerComplexsrc/plugins/dropdown/table/index.js
MentionFieldMediumsrc/plugins/field/mention.js
FontSizeInputSimplesrc/plugins/input/fontSize.js