A modern, powerful rich text editor built for compatibility and extensibility
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Dynamic registration system for formats, modules, themes, and blots enabling extensibility and customization. The registry system allows developers to register custom components, override existing ones, and import registered components at runtime.
Methods on the Quill class for registering and importing components.
class Quill {
/** Registry of imported components */
static imports: Record<string, unknown>;
/**
* Register component with path and target
* @param path - Registration path (e.g., 'formats/bold')
* @param target - Component to register
* @param overwrite - Whether to overwrite existing registration
*/
static register(path: string, target: any, overwrite?: boolean): void;
/**
* Register multiple components from object
* @param targets - Object with path->component mappings
* @param overwrite - Whether to overwrite existing registrations
*/
static register(targets: Record<string, any>, overwrite?: boolean): void;
/**
* Register single component (auto-detect path from blotName/attrName)
* @param target - Component with blotName or attrName property
* @param overwrite - Whether to overwrite existing registration
*/
static register(target: RegistryDefinition, overwrite?: boolean): void;
/**
* Import registered component
* @param name - Component path or name
* @returns Imported component or undefined
*/
static import(name: string): unknown;
/**
* Import core module
* @param name - 'core/module'
* @returns Module base class
*/
static import(name: 'core/module'): typeof Module;
/**
* Import theme
* @param name - Theme path (e.g., 'themes/snow')
* @returns Theme class
*/
static import(name: `themes/${string}`): typeof Theme;
/**
* Import Parchment
* @param name - 'parchment'
* @returns Parchment namespace
*/
static import(name: 'parchment'): typeof Parchment;
/**
* Import Delta
* @param name - 'delta'
* @returns Delta class
*/
static import(name: 'delta'): typeof Delta;
/**
* Find blot instance for DOM node
* @param node - DOM node to find blot for
* @param bubble - Whether to search up the DOM tree
* @returns Blot instance or null
*/
static find(node: Node, bubble?: boolean): Blot | null;
/**
* Set debug level for logging
* @param level - Debug level or boolean
*/
static debug(level: DebugLevel | boolean): void;
}
interface RegistryDefinition {
blotName?: string;
attrName?: string;
[key: string]: any;
}
type DebugLevel = 'error' | 'warn' | 'log' | 'info';Usage Examples:
import Quill from 'quill';
// Register single component with path
Quill.register('formats/highlight', HighlightBlot);
// Register multiple components
Quill.register({
'formats/mention': MentionBlot,
'modules/autoformat': AutoFormatModule,
'themes/minimal': MinimalTheme
});
// Register with auto-detection (uses blotName)
class CustomBlot extends Inline {
static blotName = 'custom';
}
Quill.register(CustomBlot); // Registers as 'formats/custom'
// Import registered components
const BoldFormat = Quill.import('formats/bold');
const ToolbarModule = Quill.import('modules/toolbar');
const SnowTheme = Quill.import('themes/snow');
// Import core components
const Module = Quill.import('core/module');
const Parchment = Quill.import('parchment');
const Delta = Quill.import('delta');Standard paths for registering different types of components.
// Format registration paths
'formats/bold' // Bold inline format
'formats/italic' // Italic inline format
'formats/header' // Header block format
'formats/list' // List block format
'formats/image' // Image embed format
'formats/link' // Link inline format
// Module registration paths
'modules/toolbar' // Toolbar module
'modules/keyboard' // Keyboard module
'modules/history' // History module
'modules/clipboard' // Clipboard module
'modules/uploader' // Uploader module
// Theme registration paths
'themes/snow' // Snow theme
'themes/bubble' // Bubble theme
// Blot registration paths
'blots/inline' // Inline blot
'blots/block' // Block blot
'blots/embed' // Embed blot
'blots/scroll' // Scroll blot
'blots/container' // Container blot
// Attributor registration paths
'attributors/attribute/direction' // Attribute-based direction
'attributors/class/color' // Class-based color
'attributors/style/font' // Style-based font
// Core component paths
'core/module' // Module base class
'core/theme' // Theme base class
'parchment' // Parchment system
'delta' // Delta class
// UI component paths
'ui/picker' // Picker component
'ui/tooltip' // Tooltip component
'ui/icons' // Icon definitionsUsage Examples:
// Register formats
Quill.register('formats/highlight', HighlightFormat);
Quill.register('formats/spoiler', SpoilerFormat);
// Register modules
Quill.register('modules/counter', WordCountModule);
Quill.register('modules/autosave', AutoSaveModule);
// Register themes
Quill.register('themes/dark', DarkTheme);
Quill.register('themes/minimal', MinimalTheme);
// Register attributors
Quill.register('attributors/style/line-height', LineHeightAttributor);
// Use registered components
const quill = new Quill('#editor', {
theme: 'dark',
formats: ['bold', 'italic', 'highlight', 'spoiler'],
modules: {
counter: { container: '#word-count' },
autosave: { interval: 5000 }
}
});Register custom text formats and blots.
// Custom inline format
class Highlight extends Inline {
static blotName = 'highlight';
static tagName = 'MARK';
static className = 'ql-highlight';
static create(value) {
const node = super.create();
if (value) {
node.setAttribute('data-color', value);
node.style.backgroundColor = value;
}
return node;
}
static formats(domNode) {
return domNode.getAttribute('data-color') || true;
}
format(name, value) {
if (name !== this.statics.blotName || !value) {
super.format(name, value);
} else {
this.domNode.setAttribute('data-color', value);
this.domNode.style.backgroundColor = value;
}
}
}
// Custom block format
class Alert extends Block {
static blotName = 'alert';
static tagName = 'DIV';
static className = 'alert';
static create(value) {
const node = super.create();
node.classList.add(`alert-${value || 'info'}`);
return node;
}
static formats(domNode) {
const classList = domNode.classList;
for (const className of classList) {
if (className.startsWith('alert-')) {
return className.replace('alert-', '');
}
}
return undefined;
}
}
// Custom embed format
class Tweet extends BlockEmbed {
static blotName = 'tweet';
static tagName = 'DIV';
static className = 'tweet-embed';
static create(value) {
const node = super.create();
node.setAttribute('data-tweet-id', value);
node.innerHTML = `<p>Loading tweet ${value}...</p>`;
// Load tweet content asynchronously
this.loadTweet(node, value);
return node;
}
static value(domNode) {
return domNode.getAttribute('data-tweet-id');
}
static loadTweet(node, tweetId) {
// Implementation for loading tweet content
}
}Usage Examples:
// Register custom formats
Quill.register('formats/highlight', Highlight);
Quill.register('formats/alert', Alert);
Quill.register('formats/tweet', Tweet);
// Use custom formats
const quill = new Quill('#editor', {
formats: ['bold', 'italic', 'highlight', 'alert', 'tweet']
});
// Apply custom formatting
quill.formatText(0, 10, 'highlight', 'yellow');
quill.formatLine(20, 1, 'alert', 'warning');
quill.insertEmbed(30, 'tweet', '1234567890');Register custom modules to extend editor functionality.
// Custom module
class AutoSave extends Module {
static DEFAULTS = {
interval: 10000, // 10 seconds
url: '/autosave',
method: 'POST'
};
constructor(quill, options) {
super(quill, options);
this.timer = null;
this.lastSaved = '';
this.setupAutoSave();
}
setupAutoSave() {
this.quill.on('text-change', () => {
this.scheduleAutoSave();
});
}
scheduleAutoSave() {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.save();
}, this.options.interval);
}
save() {
const content = JSON.stringify(this.quill.getContents());
if (content !== this.lastSaved) {
fetch(this.options.url, {
method: this.options.method,
headers: { 'Content-Type': 'application/json' },
body: content
}).then(() => {
this.lastSaved = content;
console.log('Auto-saved');
});
}
}
}
// Mention module
class Mentions extends Module {
static DEFAULTS = {
source: null,
mentionDenotationChars: ['@'],
showDenotationChar: true,
allowedChars: /^[a-zA-Z0-9_]*$/,
minChars: 0,
maxChars: 31,
offsetTop: 2,
offsetLeft: 0
};
constructor(quill, options) {
super(quill, options);
this.setupMentions();
}
setupMentions() {
this.quill.keyboard.addBinding({
key: '@'
}, this.handleMentionChar.bind(this));
}
handleMentionChar(range, context) {
// Implementation for mention handling
this.showMentionList(range);
}
showMentionList(range) {
// Show mention dropdown
}
}Usage Examples:
// Register custom modules
Quill.register('modules/autosave', AutoSave);
Quill.register('modules/mentions', Mentions);
// Use custom modules
const quill = new Quill('#editor', {
modules: {
autosave: {
interval: 5000,
url: '/api/documents/123/autosave'
},
mentions: {
source: (searchTerm, renderList) => {
// Fetch mention suggestions
fetch(`/api/users/search?q=${searchTerm}`)
.then(response => response.json())
.then(users => {
renderList(users.map(user => ({
id: user.id,
value: user.username,
label: user.displayName
})));
});
}
}
}
});Register custom themes with unique UI and behavior.
// Custom theme
class DarkTheme extends SnowTheme {
static DEFAULTS = {
...SnowTheme.DEFAULTS,
modules: {
...SnowTheme.DEFAULTS.modules,
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
[{ 'color': [] }, { 'background': [] }],
['link', 'image'],
['clean']
]
}
};
constructor(quill, options) {
super(quill, options);
this.applyDarkTheme();
}
applyDarkTheme() {
this.quill.container.classList.add('ql-dark');
this.addDarkStyles();
}
addDarkStyles() {
// Add dark theme CSS
const style = document.createElement('style');
style.textContent = `
.ql-dark .ql-editor {
background: #2d2d2d;
color: #ffffff;
}
.ql-dark .ql-toolbar {
background: #1e1e1e;
border-color: #444;
}
`;
document.head.appendChild(style);
}
}
// Minimal theme
class MinimalTheme extends Theme {
static DEFAULTS = {
modules: {}
};
constructor(quill, options) {
super(quill, options);
this.quill.container.classList.add('ql-minimal');
}
init() {
// Minimal initialization
this.quill.root.classList.add('ql-minimal-editor');
}
}Usage Examples:
// Register custom themes
Quill.register('themes/dark', DarkTheme);
Quill.register('themes/minimal', MinimalTheme);
// Use custom themes
const darkQuill = new Quill('#dark-editor', {
theme: 'dark'
});
const minimalQuill = new Quill('#minimal-editor', {
theme: 'minimal'
});Methods for inspecting and querying the registry.
// Check if component is registered
const isRegistered = Quill.import('formats/custom') !== undefined;
// Get all registered formats
const formats = Object.keys(Quill.imports)
.filter(key => key.startsWith('formats/'))
.map(key => key.replace('formats/', ''));
// Get all registered modules
const modules = Object.keys(Quill.imports)
.filter(key => key.startsWith('modules/'))
.map(key => key.replace('modules/', ''));
// Get all registered themes
const themes = Object.keys(Quill.imports)
.filter(key => key.startsWith('themes/'))
.map(key => key.replace('themes/', ''));Usage Examples:
// List available components
console.log('Available formats:',
Object.keys(Quill.imports)
.filter(key => key.startsWith('formats/'))
.map(key => key.replace('formats/', ''))
);
console.log('Available modules:',
Object.keys(Quill.imports)
.filter(key => key.startsWith('modules/'))
.map(key => key.replace('modules/', ''))
);
// Check if specific component exists
function hasFormat(formatName) {
return Quill.import(`formats/${formatName}`) !== undefined;
}
function hasModule(moduleName) {
return Quill.import(`modules/${moduleName}`) !== undefined;
}
// Dynamic component loading
function loadComponent(type, name) {
const path = `${type}/${name}`;
const component = Quill.import(path);
if (!component) {
throw new Error(`Component ${path} not found`);
}
return component;
}
// Usage
const BoldFormat = loadComponent('formats', 'bold');
const ToolbarModule = loadComponent('modules', 'toolbar');Replace built-in components with custom implementations.
// Override built-in bold format
class CustomBold extends Inline {
static blotName = 'bold';
static tagName = 'STRONG';
static create() {
const node = super.create();
node.setAttribute('data-custom', 'true');
return node;
}
}
// Override with overwrite flag
Quill.register('formats/bold', CustomBold, true);
// Override toolbar module
class CustomToolbar extends Toolbar {
constructor(quill, options) {
super(quill, options);
this.addCustomFeatures();
}
addCustomFeatures() {
// Add custom toolbar features
}
}
Quill.register('modules/toolbar', CustomToolbar, true);Usage Examples:
// Custom implementation of existing components
class EnhancedHistory extends History {
constructor(quill, options) {
super(quill, options);
this.addHistoryPersistence();
}
addHistoryPersistence() {
// Save history to localStorage
this.quill.on('text-change', () => {
localStorage.setItem('quill-history', JSON.stringify(this.stack));
});
// Restore history on load
const saved = localStorage.getItem('quill-history');
if (saved) {
this.stack = JSON.parse(saved);
}
}
}
// Override existing history module
Quill.register('modules/history', EnhancedHistory, true);
// Now all editors use enhanced history
const quill = new Quill('#editor', {
modules: {
history: true // Uses EnhancedHistory
}
});Install with Tessl CLI
npx tessl i tessl/npm-quill