Loading...
Loading...
Purpose: Technical reference for developers and AI agents. Defines architecture, conventions, and development workflow.
SunEditor is a WYSIWYG editor written in pure vanilla JavaScript (ES2022+) with no runtime dependencies.
It uses JSDoc for type definitions and TypeScript for type checking.
The editor supports a modular plugin architecture where features can be enabled/disabled as needed.
Architecture Components:
CoreKernel): Central runtime container — orchestrates initialization, builds the Deps bag, manages Store$): Shared dependency object built by the Kernel — all services in one object. Not the Kernel itself.Terminology:
| Subject | Name | Description |
|---|---|---|
CoreKernel instance | Kernel | Central runtime container (init, DI, lifecycle) |
kernel.$ / this.$ | Deps (dependency bag) | Shared dependency object — NOT the Kernel itself |
kernel.store | Store | Central runtime state management |
suneditor/
├── src/
│ ├── core/
│ │ ├── kernel/ # L1: Dependency container & state
│ │ ├── config/ # L2: Configuration & providers
│ │ ├── logic/
│ │ │ ├── dom/ # DOM manipulation (selection, format, inline, html, ...)
│ │ │ ├── shell/ # Editor operations (component, history, focus, ...)
│ │ │ └── panel/ # Panel UI (toolbar, menu, viewer)
│ │ ├── event/ # L4: Event orchestration (Redux-like)
│ │ │ ├── actions/
│ │ │ ├── handlers/
│ │ │ ├── reducers/
│ │ │ ├── rules/
│ │ │ ├── effects/
│ │ │ └── support/
│ │ ├── schema/ # Data definitions (context, options)
│ │ └── section/ # DOM construction
│ ├── plugins/
│ │ ├── command/ # Direct actions
│ │ ├── dropdown/ # Dropdown menus
│ │ ├── modal/ # Dialog plugins
│ │ ├── browser/ # Gallery plugins
│ │ ├── field/ # Autocomplete
│ │ ├── input/ # Toolbar inputs
│ │ └── popup/ # Inline controllers
│ ├── modules/
│ │ ├── contract/ # Module contracts (Modal, Controller, Figure, ...)
│ │ ├── manager/ # Managers (FileManager, ApiManager)
│ │ └── ui/ # UI utilities (SelectMenu, ModalAnchorEditor)
│ ├── hooks/ # Hook interface definitions
│ ├── interfaces/ # Plugin base classes & contracts
│ ├── helper/ # Pure utility functions
│ │ └── dom/ # DOM utilities
│ ├── assets/ # Static assets (icons, CSS, design)
│ ├── langs/ # i18n language files
│ └── themes/ # CSS theme files
├── test/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── types/ # Generated TypeScript definitions
├── webpack/ # Build configuration
└── dist/ # Built bundles (not tracked in git)
Runtime Environment:
Development Environment:
Type System:
.d.ts)npm run ts-buildTesting Stack:
For detailed internal engineering, see ARCHITECTURE.md.
Layer Architecture:
| Layer | Directory | Responsibility | Examples |
|---|---|---|---|
| L1 | kernel/ | Kernel (runtime container), Store (state), Deps bag ($) | CoreKernel, Store, KernelInjector |
| L2 | config/ | Configuration, context, options, event API | ContextProvider, OptionProvider, InstanceCheck, EventManager |
| L3 | logic/ | Business logic, DOM operations, UI | Selection, Format, Component, Toolbar, History |
| L4 | event/ | Internal DOM event processing | EventOrchestrator, handlers, reducers, rules, executor, effects |
Initialization Order:
1. suneditor.create() → Validates target, merges options
2. new Editor() → Creates editor instance
3. Constructor() → Builds DOM (toolbar, statusbar, wysiwyg frames)
4. new CoreKernel() → Kernel (runtime container)
a. L1: Store (state management)
b. Deps Phase 1: Config deps added to $ (L2)
c. L3: Logic instances created (dom, shell, panel)
d. Deps Phase 2: Logic deps added to $ (Deps bag complete)
e. L3 Init Pass: _init() called on L3 instances that need post-Phase 2 setup
f. L4: EventOrchestrator
5. editor.#Create() → Plugin registration, event setup
6. editor.#editorInit() → Frame init, triggers onload event
src/plugins/)Plugins are modular features that extend editor functionality.
Architecture Pattern: ES6 classes extending plugin type base classes from src/interfaces/plugins.js, which extend KernelInjector (injects this.$ — the Deps bag).
Inheritance Chain:
KernelInjector → Base → PluginCommand/PluginModal/PluginDropdown/...
↓
constructor(kernel) → super(kernel) → this.$ = kernel.$
Plugin Type Base Classes:
| Base Class | Type | Required Methods | Examples |
|---|---|---|---|
PluginCommand | command | action() | blockquote, list_bulleted, list_numbered, exportPDF |
PluginDropdown | dropdown | action() | align, font, fontColor, blockStyle, lineHeight |
PluginDropdownFree | dropdown-free | (none) | table, fontColor, backgroundColor |
PluginModal | modal | open() | image, video, link, math, audio, drawing, embed |
PluginBrowser | browser | open(), close() | imageGallery, videoGallery, audioGallery, fileGallery |
PluginField | field | (none) | mention |
PluginInput | input | (none) | fontSize, pageNavigator |
PluginPopup | popup | show() | anchor |
Plugin Access Pattern:
All plugins access dependencies through this.$:
import { PluginModal } from '../../interfaces'; class MyPlugin extends PluginModal { static key = 'myPlugin'; static className = 'se-btn-my-plugin'; /** * @constructor * @param {SunEditor.Kernel} kernel - The Kernel instance */ constructor(kernel, pluginOptions) { super(kernel); // KernelInjector → this.$ = kernel.$ (Deps bag) this.title = this.$.lang.myPlugin; // access via Deps this.icon = 'myPlugin'; } open(target) { const range = this.$.selection.get(); const wysiwyg = this.$.frameContext.get('wysiwyg'); const height = this.$.frameOptions.get('height'); this.$.html.insert('<p>content</p>'); this.$.history.push(false); } }
Multi-Interface Pattern (TypeScript):
A single plugin can implement multiple interfaces — combining a base plugin type with module contracts and component hooks. In TypeScript, use implements to compose these:
import { interfaces } from 'suneditor'; import type { SunEditor } from 'suneditor/types'; class MyPlugin extends interfaces.PluginModal implements interfaces.ModuleModal, interfaces.EditorComponent { static key = 'myPlugin'; _element: HTMLElement | null = null; constructor(kernel: SunEditor.Kernel) { super(kernel); } // PluginModal base open(target?: HTMLElement) { ... } // ModuleModal interface async modalAction() { return true; } modalOff(isUpdate: boolean) { ... } // EditorComponent interface static component(node: Node) { return /^IMG$/i.test(node?.nodeName) ? node : null; } componentSelect(target: HTMLElement) { ... } }
Available Contracts and Base Types (interfaces.*):
| Type | Purpose | Key Methods |
|---|---|---|
ModuleModal | Modal dialog behavior | modalAction(), modalOn(), modalOff() |
ModuleController | Floating controller | controllerAction(), controllerOn() |
ModuleColorPicker | Color picker behavior | colorPickerAction() |
ModuleHueSlider | Hue slider behavior | hueSliderAction() |
ModuleBrowser | Gallery browser | browserInit() |
EditorComponent | Component lifecycle | componentSelect(), componentDestroy() |
PluginDropdown | Plugin base class | on(), action() |
Contracts can be combined with a base plugin class via implements.
Available via this.$ (Deps bag):
options, frameOptions, context, frameContext, frameRoots, lang, iconsselection, html, format, inline, listFormat, nodeTransform, char, offsetcomponent, focusManager, pluginManager, plugins, ui, commandDispatcher, history, shortcutstoolbar, subToolbar (second Toolbar instance, only with _subMode), menu, viewereventManager, contextProvider, optionProvider, instanceCheck, storefacade (editor instance)Full reference: Custom Plugin Guide — Complete hook tables, parameter types, code examples, and multi-interface patterns.
Plugin hooks are organized into four categories:
| Category | Interfaces | Key Methods |
|---|---|---|
| Common Hooks | (all plugins) | active(), init(), retainFormat(), shortcut(), setDir() |
| Event Hooks | (all plugins) | onKeyDown, onInput, onClick, onPaste, onFocus, onBlur, +8 more |
| Module Hooks | ModuleModal, ModuleController, ModuleColorPicker, ModuleHueSlider, ModuleBrowser | modalAction(), controllerAction(), colorPickerAction(), etc. |
| Component Hooks | EditorComponent | componentSelect(), componentDeselect(), componentEdit(), componentDestroy(), componentCopy() |
Event hook execution order is controlled by eventIndex in static options (lower = earlier).
src/modules/)Architecture Pattern: ES6 classes that receive $ (Deps bag) directly — no inheritance from KernelInjector.
constructor(inst, $, ...) → receives plugin instance + Deps bag + custom params#privateField (ES2022 syntax)Module Classes:
| Module | Folder | Purpose | Constructor Pattern |
|---|---|---|---|
Modal | contract/ | Dialog windows | new Modal(inst, $, element) |
Controller | contract/ | Floating tooltips | new Controller(inst, $, element) |
Figure | contract/ | Resize/align wrapper | new Figure(inst, $, ...) |
ColorPicker | contract/ | Color palette | new ColorPicker(inst, $, ...) |
HueSlider | contract/ | HSL color wheel | new HueSlider(inst, $, ...) |
Browser | contract/ | Gallery UI | new Browser(inst, $, ...) |
FileManager | manager/ | File uploads | Instance + async |
ApiManager | manager/ | XHR requests | new ApiManager(inst, $, ...) |
SelectMenu | ui/ | Custom dropdowns | Instance + items |
ModalAnchorEditor | ui/ | Link form | Instance + form |
_DragHandle | ui/ | Drag state | Map (not class) |
src/helper/)Architecture Pattern: Pure functions, no classes or state
export function funcName() + export default { funcName }import { dom } from '../helper' → dom.check.isElement()Helper Modules:
| Module | Key Functions | Purpose |
|---|---|---|
markdown.js | jsonToMarkdown, markdownToHtml | Markdown ↔ HTML conversion (GFM) |
converter.js | htmlToEntity, htmlToJson, debounce, toFontUnit, rgb2hex | String/HTML conversion |
env.js | isMobile, isOSX_IOS, isClipboardSupported, _w, _d | Browser/device detection |
keyCodeMap.js | isEnter, isCtrl, isArrow, isComposing | Keyboard event checking |
numbers.js | is, get, isEven, isOdd | Number validation |
unicode.js | zeroWidthSpace, escapeStringRegexp | Special characters |
clipboard.js | write | Clipboard with iframe handling |
dom/domCheck.js | isElement, isText, isWysiwygFrame, isComponentContainer | Node type checking |
dom/domQuery.js | getParentElement, getChildNode, getNodePath | DOM tree navigation |
dom/domUtils.js | addClass, createElement, setStyle, removeItem | DOM operations |
Options are split into two categories:
$.options): Shared across all frames (plugins, mode, toolbar, shortcuts, events)$.frameOptions): Per-frame configuration (width, height, placeholder, iframe, statusbar)Options use Map-based storage. Some are marked 'fixed' (immutable) or resettable via editor.resetOptions().
1. Global Context ($.context)
$.context.get('toolbar')2. Frame Context ($.frameContext)
frameRoots.get(store.get('rootKey'))$.frameContext.get('wysiwyg')3. Frame Roots Storage ($.frameRoots)
Map<rootKey, FrameContext> — actual data storage for all framesnull key for single-root, custom string for multi-root4. Frame Options ($.frameOptions)
frameContext.get('options')$.frameOptions.get('height')npm run dev # Start local dev server (http://localhost:8088) npm start # Alias for npm run dev
npm run build:dev # Build for development (with source maps) npm run build:prod # Build for production (minified)
npm test # Run Jest unit tests (silent mode) npm run test:watch # Run Jest in watch mode npm run test:coverage # Run tests with coverage report npm run test:e2e # Run Playwright E2E tests (webServer starts/reuses localhost:8088) npm run test:e2e:ui # Run E2E tests with Playwright UI npm run test:e2e:headed # Run E2E tests in headed mode npm run test:all # Run all tests (Jest + Playwright)
npm run lint # All: ESLint (JS + TS) + TypeScript type check + Architecture check npm run lint:type # Run TypeScript type checking without emitting files npm run lint:fix-js # Auto-fix JavaScript issues with ESLint npm run lint:fix-ts # Auto-fix TypeScript issues with ESLint npm run lint:fix-all # Fix all lint issues (JS + TS) npm run check:arch # Check architecture dependencies with dependency-cruiser
npm run ts-build # Build TypeScript definitions from JSDoc npm run check:langs # Sync language files (requires Google API credentials) npm run check:inject # Inject plugin JSDoc types into options.js
File Naming:
selection.js, eventManager.js)Modal.js for Modal class)blockquote.js for key 'blockquote')Code Naming:
KernelInjector, Modal, CoreKernel)getRange, setContent, applyTagEffect)#privateField, #privateMethod() (ES2022)ACTION_TYPE, EVENT_TYPES)Plugin Naming:
'image', 'video', 'blockStyle')'command', 'modal', 'dropdown')Blockquote, Link, Image)CSS Naming:
se- (e.g., se-wrapper, se-component)se-component, se-flex-component, se-inline-componentDON'T:
innerHTML directly on wysiwyg frame → Use this.$.html.set(content)frameRoots directly → Use this.$.frameContextthis.$.eventManager.addEvent(element, 'click', handler)document.execCommand → Use this.$.html, this.$.format, or this.$.inline methodssrc/interfaces/plugins.jsthis.$ (the Deps bag, not the kernel itself)DO:
this.$.selection for all selection managementthis.$.html for content manipulationthis.$.format for block-level formattingthis.$.eventManager for automatic cleanupthis.$.frameContext and this.$.frameOptions instead of direct frameRoots accessdom.check methods (iframe-safe)SunEditor.Kernel for constructors, SunEditor.Deps for deps)options.plugins: [ImagePlugin, VideoPlugin, ...] // or { image: ImagePlugin, video: VideoPlugin, ... }
↓
Constructor.js: stores as class references in product.plugins
↓
CoreKernel → PluginManager: loops through plugins
↓
new Plugin(kernel, options) → super(kernel) → KernelInjector → this.$ = kernel.$ (Deps bag)
↓
Plugin events registered (_onPluginEvents Map)
Runtime Activation:
| Plugin Type | Flow |
|---|---|
| Command | button.click → commandDispatcher.run() → plugin.action() |
| Modal | button.click → commandDispatcher.run() → plugin.open() → Modal shows |
| Dropdown | button.click → menu.dropdownOn() → plugin.on() |
Key Rule: Always pass class references, not instances:
// Correct plugins: [MyPlugin]; // Wrong - Kernel cannot manage lifecycle plugins: [new MyPlugin()];
Simple Command Plugin:
src/plugins/command/blockquote.js - Minimal command pluginModal Plugin with Form:
src/plugins/modal/link.js - Link dialog with form validationsrc/plugins/modal/image/index.js - Image upload with Figure moduleDropdown Plugin:
src/plugins/dropdown/align.js - Simple dropdown menuComponent Plugin:
src/plugins/modal/image/index.js - Full component lifecyclesrc/plugins/modal/video/index.js - Component with multiple content typesCore Logic Class:
src/core/logic/dom/selection.js - Selection and range manipulationsrc/core/logic/dom/format.js - Block-level formatting operationssrc/core/logic/shell/component.js - Component lifecycle managementModule:
src/modules/contract/Modal.js - Dialog window systemsrc/modules/contract/Controller.js - Floating toolbar controllerEvent Handling:
src/core/event/handlers/handler_ww_key.js - Wysiwyg keyboard handlerssrc/core/event/reducers/keydown.reducer.js - Keydown event analysissrc/core/event/rules/keydown.rule.enter.js - Enter key rule logicsrc/core/event/actions/index.js - Action type definitions and creatorssrc/core/event/executor.js - Action dispatchersrc/core/event/effects/keydown.registry.js - Keydown effect handlerssrc/core/event/effects/common.registry.js - Common effect handlersExample Event Flow (Enter Key):
1. User presses Enter
↓
2. handler_ww_key.js captures keydown event
↓
3. keydown.reducer.js analyzes the event with current editor state
↓
4. Reducer delegates to keydown.rule.enter.js for Enter-specific logic
↓
5. Returns action list: [{t: 'enter.line.addDefault', p: {...}}, {t: 'history.push', p: {...}}]
↓
6. executor.js dispatches actions through effect registries (common + keydown)
↓
7. Effects execute:
- 'enter.line.addDefault' → calls format.addLine()
- 'history.push' → calls history.push()
↓
8. DOM updated, selection adjusted, onChange event triggered
test/unit/)@/ maps to src/test/integration/)test/e2e/)onload EventEditor initialization completes asynchronously. Use onload for operations that depend on fully initialized UI/state:
// Wrong - may fail const editor = SUNEDITOR.create('#editor'); editor.focusManager.focus(); // Correct SUNEDITOR.create('#editor', { events: { onload: ({ $ }) => { $.focusManager.focus(); $.html.set('<p>Initial content</p>'); }, }, });
Why: suneditor.create() returns immediately, but toolbar visibility, ResizeObserver registration, and history reset happen in a deferred setTimeout. Calling methods before onload may cause errors.
SunEditor supports DIV mode (default) and iframe mode (iframe: true).
SUNEDITOR.create('#editor', { iframe: true, iframe_attributes: { sandbox: 'allow-downloads', // allow-same-origin is auto-added }, });
SSR frameworks (Next.js/Nuxt): Use dynamic import with ssr: false to avoid contentDocument is null errors.
SunEditor supports a Markdown View mode alongside the existing Code View and WYSIWYG modes. The markdown view converts editor content to GitHub Flavored Markdown (GFM) for editing and converts back to HTML on exit.
Toggle: Use the markdownView button in the toolbar or call editor.viewer.markdownView() programmatically.
Supported GFM Syntax:
# ~ ######), paragraphs, line breaksinline code, ==highlight==- [x])>), fenced code blocks (```), horizontal rules (---)How it works:
converter.htmlToJson() → markdown.jsonToMarkdown() — converts the editor's HTML to a JSON tree, then to GFM stringmarkdown.markdownToHtml() — parses GFM back to HTMLKey files:
src/helper/markdown.js — Markdown ↔ HTML converter (GFM)src/core/logic/panel/viewer.js — View mode management (code view, markdown view, fullscreen, preview)Mutual exclusivity: Code View and Markdown View are mutually exclusive — activating one automatically deactivates the other.
webpack/)@babel/preset-env) with Browserslist targetsdist/suneditor.min.js and dist/suneditor.min.cssThe dist/ folder is NOT tracked in git and is built via CI/CD.
When making code changes (bug fixes, new features, improvements, security patches, etc.), always update changes.md in the project root.
This file is used to generate the demo site's changelog. Keep entries concise and user-facing.
Format:
## [Category] - YYYY-MM-DD - **tag:** Short description of the change
Categories: Fix, Feature, Improvement, Security, Breaking
Tags (examples): html, toolbar, plugin:image, selection, clipboard, core, api, etc.
Example:
## Security - 2026-03-29 - **html:** Block obfuscated `javascript:` protocol in href/src attributes (entity/URL-encoded whitespace bypass)
Rules:
changes.md does not exist yet, create it