channelwill-editor
v1.1.103
Published
A customizable rich text editor built with Lexical
Maintainers
Readme
Channelwill Rich Text Editor
A modern rich text editor React component based on the Lexical framework, supporting multiple UI themes.
📑 Table of Contents
- Features
- Installation
- Quick Start
- Common Use Cases
- Advanced Usage
- API Documentation
- UI Theme System
- Style Customization
- Development
Features
- 🎨 Multiple UI Theme Support - Default theme and Shopify Polaris theme
- 📝 Rich Editing Features - Bold, italic, underline, strikethrough, font family, font size, etc.
- 📐 Text Alignment - Left, center, right, and justify alignment
- 📋 History Management - Undo/redo functionality
- 🖼️ Image Upload & Management - Upload, smooth 8-direction resizing, drag & drop, paste images with progress tracking
- 🧹 Clear Formatting - Remove all text formatting and inline styles with one click
- 🎯 Floating Toolbar - Context-aware toolbar that appears when text is selected
- 😊 Emoji Picker - Smart emoji picker with automatic boundary detection to stay within viewport
- 🔌 Plugin Architecture - Support for tables, tree view, custom user plugins and more
- 🚀 Full Lexical API Access - Direct access to all Lexical native APIs via
/nativesubmodule andonInitcallback - 🎯 TypeScript Support - Complete type definitions
- 🎭 Theme Customization - Support for custom themes and styles
- 💻 HTML Source Editing - Switch between rich text and HTML source code mode with CodeMirror
- 🔧 Extensible Architecture - Easy to add custom plugins and themes
Installation
npm install channelwill-editorThat's it! All core dependencies (Lexical, React) are automatically installed. You only need to install optional features if needed:
# For Emoji Picker (optional)
npm install emoji-picker-react
# For HTML Source Code Editor (optional)
npm install @codemirror/lang-html @uiw/react-codemirror
# For Shopify Polaris Theme (optional)
npm install @shopify/polaris @shopify/polaris-icons
# For AI Assistant (optional)
npm install openaiQuick Start
Basic Usage
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
const [content, setContent] = useState('');
return (
<Editor
placeholder="Start typing..."
onChange={(html) => setContent(html)}
/>
);
}Using Polaris Theme
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
import '@shopify/polaris/build/esm/styles.css';
function App() {
return (
<Editor
placeholder="Start typing..."
uiTheme="polaris"
onChange={(html) => console.log(html)}
/>
);
}Read-Only Mode
You can disable the editor to make it read-only:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
disabled={true} // Editor is read-only
value="<p>This content cannot be edited</p>"
placeholder="Read-only editor..."
/>
);
}When disabled={true}:
- ✅ All toolbar buttons are disabled
- ✅ Keyboard shortcuts (Ctrl+B, Ctrl+I, etc.) are blocked
- ✅ Text input, deletion, and paste operations are prevented
- ✅ Editor is completely read-only
- ⚠️ Lexical editor is still loaded
Pure HTML Display Mode
For simple HTML rendering without loading Lexical editor (better performance):
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
readOnly={true} // Pure HTML display mode
value="<p>HTML content <strong>rendered</strong> directly</p>"
height="400px"
width="100%"
/>
);
}When readOnly={true}:
- ✅ HTML is rendered in an isolated iframe
- ✅ No Lexical editor loaded (better performance)
- ✅ No toolbar displayed
- ✅ Complete CSS isolation
- ✅ Dangerous tags are filtered by default (script, iframe, object, etc.)
Advanced Configuration with readOnlyConfig:
import { Editor, ReadOnlyClickEvent } from 'channelwill-editor';
<Editor
readOnly={true}
value={htmlContent}
readOnlyConfig={{
useIframe: true, // Use iframe for CSS isolation
notAllowTags: ['script', 'iframe'], // Customize forbidden tags
onClick: (event: ReadOnlyClickEvent) => {
console.log('Clicked element:', event.clickNode);
console.log('Parent element:', event.parentNode);
console.log('Parent ID:', event.parentId);
// Example: Handle click on specific element
if (event.parentId === 'pw-mail-header') {
console.log('Clicked header section');
}
}
}}
/>ReadOnlyConfig Options:
useIframe- Whether to use iframe (default:true)notAllowTags- Array of forbidden HTML tagsonClick- Click event handler that provides:clickNode- The clicked HTML elementparentNode- Parent element with id attribute (or direct parent if none found)parentId- The id of the parent element (if found)
Comparison:
| Feature | disabled={true} | readOnly={true} |
|---------|------------------|-------------------|
| Lexical Editor | ✅ Loaded | ❌ Not loaded |
| Toolbar | ❌ Disabled | ❌ Hidden |
| Performance | Normal | ⚡ Faster |
| HTML Fidelity | 70-80% | 💯 100% |
| Use Case | Preview with editor | Pure HTML display |
Common Use Cases
HTML Import with Formatting Preservation
When users provide HTML content with inline styles, CSS classes, or data attributes, you can preserve these during import:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
const htmlContent = `
<p>Normal text with <span style="color: red; font-size: 18px;">custom styles</span></p>
<div class="custom-class" data-id="123">Content with classes and data attributes</div>
`;
return (
<Editor
value={htmlContent}
htmlImportOptions={{
preserveInlineStyles: true, // Preserve style attributes
preserveClassNames: true, // Preserve class attributes
preserveDataAttributes: true, // Preserve data-* attributes
}}
onChange={(html) => {
console.log('Updated HTML:', html);
}}
/>
);
}Features:
- ✅ Inline Styles: Preserves
style="color: red; font-size: 18px;"attributes - ✅ CSS Classes: Preserves
class="custom-class"attributes - ✅ Data Attributes: Preserves
data-*attributes likedata-id="123" - ✅ Automatic: Works automatically when importing HTML via the
valueprop
Use Cases:
- Import email templates with existing styling
- Preserve custom attributes from external HTML sources
- Maintain branding and design consistency when importing content
- Keep tracking IDs and metadata in data attributes
Default Behavior:
By default, all preservation options are enabled (true). You only need to configure htmlImportOptions if you want to disable certain features or specify custom behavior.
Custom Toolbar Configuration
You can customize which toolbar buttons to display and their order:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
// Only show bold, italic, and HTML source code button
toolbarItems={['bold', 'italic', 'code']}
enableHtmlSource={true}
placeholder="Start typing..."
onChange={(html) => console.log(html)}
/>
);
}Available toolbar items:
'blockFormat'- Block format selector (Normal, H1-H6, Lists, Quote, Code)'history'- Undo/Redo buttons'bold'- Bold text formatting'italic'- Italic text formatting'underline'- Underline text formatting'strikethrough'- Strikethrough text formatting'alignment'- Text alignment (left, center, right, justify)'fontSize'- Font size dropdown'fontFamily'- Font family dropdown (customizable viafontFamilyItemsprop)'lineHeight'- Line height dropdown (customizable vialineHeightItemsprop)'fontColor'- Text color picker'fontBackground'- Background color picker'link'- Insert/edit link'image'- Image upload with resize and management'emoji'- Emoji picker with smart boundary detection'clean'- Clear formatting'code'- HTML source code toggle
Custom Font Families
You can customize the available font families in the font dropdown:
import { Editor, type FontFamilyOption } from 'channelwill-editor';
function App() {
const customFonts: FontFamilyOption[] = [
['Arial', 'Arial'],
['Helvetica', 'Helvetica'],
['Georgia', 'Georgia'],
['Times New Roman', 'Times New Roman'],
['Courier New', 'Courier New'],
['Verdana', 'Verdana'],
['Comic Sans MS', 'Comic Sans MS'],
// Add your custom fonts
['Custom Font', 'Custom Font'],
['My Special Font', 'My Special Font'],
];
return (
<Editor
toolbarItems={['fontFamily', 'lineHeight', 'bold', 'italic']}
fontFamilyItems={customFonts} // Custom font list
lineHeightItems={customLineHeights} // Custom line height list
onChange={(html) => console.log(html)}
/>
);
}Custom Block Formats
Control which block formats appear in the blockFormat dropdown:
import { Editor } from 'channelwill-editor';
import type { BlockFormatType } from 'channelwill-editor';
function App() {
// Only show paragraph, h1, h2, and h3
const customBlockFormats: BlockFormatType[] = [
'paragraph',
'h1',
'h2',
'h3'
];
return (
<Editor
toolbarItems={['blockFormat', 'bold', 'italic']}
blockFormatItems={customBlockFormats} // Custom block format list
onChange={(html) => console.log(html)}
/>
);
}Default Fonts (when fontFamilyItems is not provided or empty):
- Arial
- Courier New
- Georgia
- Times New Roman
- Trebuchet MS
- Verdana
- Helvetica
- Comic Sans MS
- Impact
- Lucida Console
Custom Line Heights
You can customize the line height options using the lineHeightItems prop. Each line height is defined as [value, label].
import { Editor } from 'channelwill-editor';
import type { LineHeightOption } from 'channelwill-editor';
function MyEditor() {
const customLineHeights: LineHeightOption[] = [
['1.0', '1.0'],
['1.5', '1.5'],
['2.0', '2.0'],
['2.5', '2.5'],
// Add your custom line heights
];
return (
<Editor
toolbarItems={['lineHeight', 'bold', 'italic']}
lineHeightItems={customLineHeights} // Custom line height list
onChange={(html) => console.log(html)}
/>
);
}Default Line Heights (when lineHeightItems is not provided or empty):
- 1.0
- 1.15
- 1.5
- 1.75
- 2.0
- 2.5
- 3.0
Default Block Formats (when blockFormatItems is not provided or empty):
paragraph- Normal paragraph texth1- Heading 1h2- Heading 2h3- Heading 3h4- Heading 4h5- Heading 5h6- Heading 6bullet- Bullet listnumber- Numbered listquote- Quote blockcode- Code block
Email-Friendly HTML (Inline Styles)
⭐ NEW FEATURE - Export HTML with inline styles instead of CSS classes for email compatibility!
By default, the editor uses CSS classes for styling (requires external stylesheet). For email compatibility, use styleMode="inline" to convert all styles to inline style attributes:
import { Editor } from 'channelwill-editor';
import { useState } from 'react';
function EmailEditor() {
const [emailHtml, setEmailHtml] = useState('');
return (
<Editor
styleMode="inline" // 🎯 Email-friendly inline styles
placeholder="Compose your email..."
onChange={(html) => {
setEmailHtml(html);
console.log('Email-safe HTML:', html);
}}
### Pure HTML Output
By default, Lexical wraps text in `<span style="white-space: pre-wrap;">` to preserve formatting (trailing spaces, multiple spaces, etc.). This ensures accurate rendering but may generate verbose HTML.
If you prefer cleaner HTML without these span wrappers, use `pureHTML={true}`:
```tsx
import { Editor } from 'channelwill-editor';
function App() {
return (
<Editor
pureHTML={true} // Remove span wrappers for cleaner HTML
placeholder="Type something..."
onChange={(html) => {
console.log(html);
// With pureHTML=false (default): <p><span style="white-space: pre-wrap;">hello </span></p>
// With pureHTML=true: <p>hello</p>
// Note: Trailing/multiple spaces will be lost
}}
/>
);
}⚠️ Warning: When pureHTML={true}:
- Trailing and leading spaces will be removed by browsers
- Multiple consecutive spaces will be collapsed to one
- Only use this if you don't need to preserve whitespace formatting
When to use:
- ✅
pureHTML={false}(default): For accurate formatting preservation - ✅
pureHTML={true}: For minimal HTML when whitespace doesn't matter /> ); }
**Comparison:**
```tsx
// styleMode="outside" (default) - Requires external CSS
<p class="editor-paragraph">
<span class="editor-text-bold editor-text-underline">Hello World</span>
</p>
// styleMode="inline" - Email-friendly, no external CSS needed
<p style="margin: 0; margin-bottom: 8px;">
<span style="font-weight: bold; text-decoration: underline;">Hello World</span>
</p>Benefits for Email:
- ✅ Works in all email clients (Gmail, Outlook, Apple Mail, etc.)
- ✅ No external CSS required
- ✅ Preserves all formatting (bold, italic, underline, colors, fonts, etc.)
- ✅ No
<style>tags needed - ✅ Better compatibility with email sanitizers
Supported Styles:
- Text formatting: bold, italic, underline, strikethrough
- Text alignment: left, center, right, justify
- Font size, font family, line height
- Text color, background color
- Headings (H1-H6)
- Lists (bullet, numbered)
- Quote blocks
- Code blocks
- Links
🎯 NEW: Custom Theme Support with Auto-Detection
Now works seamlessly with custom themes! Styles are automatically detected from your CSS:
import { Editor } from 'channelwill-editor';
// Define custom theme
const customTheme = {
text: {
bold: 'my-custom-bold',
italic: 'my-custom-italic',
},
heading: {
h1: 'my-custom-h1',
},
};
function EmailComposer() {
return (
<>
<style>{`
.my-custom-bold { font-weight: 700; color: #d63384; }
.my-custom-italic { font-style: italic; color: #0d6efd; }
.my-custom-h1 { font-size: 36px; font-weight: 800; }
`}</style>
<Editor
styleMode="inline"
theme={customTheme} // ⭐ Styles automatically detected from CSS!
onChange={(html) => console.log(html)}
/>
</>
);
}Advanced Options:
<Editor
styleMode="inline"
inlineStyleOptions={{
// Auto-detect styles from document.styleSheets (default: true)
autoDetectStyles: true,
// Manual mappings (useful for cross-origin stylesheets)
customMappings: {
'my-custom-class': 'color: red; font-weight: bold;',
},
// Include default Lexical mappings as fallback (default: true)
includeDefaults: true,
}}
/>Available customMappings Class Names:
| Category | Class Name | Description | Default Style |
|----------|-----------|-------------|---------------|
| Text Formatting | editor-text-bold | Bold text | font-weight: bold; |
| | editor-text-italic | Italic text | font-style: italic; |
| | editor-text-underline | Underlined text | text-decoration: underline; |
| | editor-text-strikethrough | Strikethrough text | text-decoration: line-through; |
| | editor-text-underlineStrikethrough | Combined underline & strikethrough | text-decoration: underline line-through; |
| | editor-text-code | Inline code | font-family: Menlo, Consolas, Monaco, monospace; background-color: #f0f0f0; padding: 2px 4px; |
| | editor-text-subscript | Subscript | font-size: 0.8em; vertical-align: sub; |
| | editor-text-superscript | Superscript | font-size: 0.8em; vertical-align: super; |
| Alignment | editor-text-left | Left aligned text | text-align: left; |
| | editor-text-center | Center aligned text | text-align: center; |
| | editor-text-right | Right aligned text | text-align: right; |
| | editor-text-justify | Justified text | text-align: justify; |
| Headings | editor-heading-h1 | H1 heading | font-size: 2em; font-weight: bold; |
| | editor-heading-h2 | H2 heading | font-size: 1.5em; font-weight: bold; |
| | editor-heading-h3 | H3 heading | font-size: 1.17em; font-weight: bold; |
| | editor-heading-h4 | H4 heading | font-size: 1em; font-weight: bold; |
| | editor-heading-h5 | H5 heading | font-size: 0.83em; font-weight: bold; |
| | editor-heading-h6 | H6 heading | font-size: 0.75em; font-weight: bold; |
| Lists | editor-list-ul | Unordered list | list-style-type: disc; padding-left: 24px; |
| | editor-list-ol | Ordered list | list-style-type: decimal; padding-left: 24px; |
| | editor-listitem | List item | margin: 0; padding: 0; |
| | editor-nested-listitem | Nested list item | list-style-type: none; |
| Other | editor-quote | Quote block | border-left: 4px solid #ccc; padding-left: 16px; margin: 16px 0; color: #666; |
| | editor-code | Code block | font-family: Menlo, Consolas, Monaco, monospace; background-color: #f5f5f5; padding: 12px; |
| | editor-link | Link | (empty by default) |
| | editor-paragraph | Paragraph | (empty by default) |
Example - Customize styles for email:
<Editor
styleMode="inline"
inlineStyleOptions={{
autoDetectStyles: false, // Disable auto-detection
customMappings: {
// Only include essential styles for email
'editor-heading-h1': 'font-size: 24px; font-weight: bold; margin: 10px 0;',
'editor-heading-h2': 'font-size: 18px; font-weight: bold; margin: 8px 0;',
'editor-paragraph': 'margin: 0 0 8px 0;',
'editor-text-bold': 'font-weight: bold;',
'editor-text-italic': 'font-style: italic;',
'editor-link': 'color: #0066cc; text-decoration: none;',
}
}}
/>
includeDefaults: true,
}}
/>Detection Priority (highest to lowest):
customMappings- Manual mappings you provide- Auto-detected styles from
document.styleSheets - Default Lexical class mappings
HTML Source Editing
Enable HTML source code editing with CodeMirror:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
enableHtmlSource={true}
height="400px"
htmlSourceConfig={{
theme: 'dark',
showLineNumbers: true,
}}
placeholder="Start typing..."
onChange={(html) => console.log(html)}
/>
);
}Code Mode (HTML Editor Only)
Display only the CodeMirror HTML editor without the Lexical rich text editor:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
const [html, setHtml] = useState('<h1>Hello World</h1>');
return (
<Editor
codeMode={true}
value={html}
onChange={(html) => setHtml(html)}
height="400px"
htmlSourceConfig={{
theme: 'dark',
showLineNumbers: true,
}}
/>
);
}When codeMode={true}:
- ✅ Shows only the CodeMirror HTML editor
- ✅ No rich text editor or toolbar
- ✅ Direct HTML editing without Lexical overhead
- ✅ Supports all CodeMirror features (syntax highlighting, line numbers)
- ✅ onChange receives HTML string directly
- ✅ Respects
disabledprop for read-only mode
Note: This mode requires @codemirror/lang-html and @uiw/react-codemirror packages.
Read-only code mode example:
<Editor
codeMode={true}
disabled={true} // Makes CodeMirror read-only
value={html}
height="400px"
/>Clear Formatting
Clear all formatting from selected text or cursor position:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
toolbarItems={['bold', 'italic', 'fontColor', 'clean']}
placeholder="Format text and click clean button to remove formatting..."
onChange={(html) => console.log(html)}
/>
);
}What gets cleared:
- ✅ Text formats (bold, italic, underline, strikethrough)
- ✅ Inline styles (font size, text color, background color, font family)
- ✅ Links
Smart clearing:
- When text is selected: removes all formatting from selection
- When cursor only: clears format state so new text won't inherit formatting
Floating Toolbar
Enable a context-aware floating toolbar that appears when text is selected:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
floatToolbar={['blockFormat', 'bold', 'italic', 'underline', 'fontColor', 'clean']}
placeholder="Select text to see floating toolbar..."
onChange={(html) => console.log(html)}
/>
);
}The floating toolbar appears above selected text and provides quick access to formatting options. All toolbar items including blockFormat are supported.
Emoji Picker
Enable emoji selection with smart boundary detection:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
toolbarItems={['bold', 'italic', 'emoji']}
placeholder="Type or click emoji button..."
onChange={(html) => console.log(html)}
/>
);
}Features:
- 😊 Native Emoji Support - Uses system-native emoji rendering
- 🎯 Smart Positioning - Automatically adjusts position to stay within viewport boundaries
- 🔍 Search & Categories - Search emojis and browse by categories
- ⚡ Lazy Loading - Loads emoji picker on-demand for better performance
- 📱 Responsive - Works on both desktop and mobile devices
Smart Positioning Logic: The emoji picker uses intelligent placement with priority order:
- Below button (default) - Shows below if there's enough space
- Right side - If not enough space below, tries right side first
- Left side - If right side doesn't fit, tries left side
- Above button - If no side fits, shows above
- Best fit - If no position has full space, uses the position with most available space
This ensures the picker is always fully visible and positioned in the most natural location based on available screen space.
Note: This feature requires the emoji-picker-react package:
npm install emoji-picker-reactImage Upload
Enable image upload functionality with customizable upload handling:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
const imageUploadConfig = {
limitSize: 5 * 1024 * 1024, // 5MB limit
limitType: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
hash: true, // Hash filenames for security
onChange: (file) => {
console.log('Selected file:', file.name, file.size);
},
uploadHandler: async (file, onProgress) => {
// Your upload implementation
const formData = new FormData();
formData.append('image', file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = (event.loaded / event.total) * 100;
onProgress?.(progress);
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
resolve(response.url); // Return uploaded image URL
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.open('POST', '/api/upload/image');
xhr.send(formData);
});
}
};
return (
<Editor
toolbarItems={['image', 'bold', 'italic', 'code']}
imageUploadConfig={imageUploadConfig}
placeholder="Click image button to upload, or drag & drop images here..."
onChange={(html) => console.log(html)}
/>
);
}Features:
- 📤 Multiple Upload Methods - Click button, drag & drop, or paste (Ctrl+V)
- 📏 Smooth Interactive Resizing - 8-direction resize handles with smooth pointer events, real-time visual feedback
- 🎯 Intelligent Resizing - Corner handles maintain aspect ratio, edge handles resize freely
- 🗑️ Easy Deletion - Select image and press Delete/Backspace key
- 📊 Upload Progress - Real-time progress indicator during upload
- 🔒 File Validation - Size and type restrictions with error handling
- 🎨 Theme Support - Works with both Default and Polaris themes
Text Length Limit
Limit the maximum number of text characters (excluding HTML tags and rich media):
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
function App() {
return (
<Editor
placeholder="Type your content (max 500 characters)..."
maxLength={500}
maxLengthParams={{
alignment: 'right', // Position: 'left' | 'right'
visible: true, // Whether to show character counter
size: 'normal', // Character size: 'small' | 'normal' | 'large'
color: '#666' // Character color
}}
onChange={(html) => console.log(html)}
/>
);
}maxLengthParams Configuration:
alignment: Character counter position,'left'shows at bottom-left,'right'shows at bottom-rightvisible: Whether to show character counter,trueto show,falseto hidesize: Character size,'small'(10px),'normal'(12px),'large'(14px)color: Character color, supports any CSS color value
Smart Color Indicators:
- When character count reaches 90% of limit, counter turns orange
- When character count reaches or exceeds 100% of limit, counter turns red
Custom Element Styles
Customize styles for headings, paragraphs, quotes, lists, and other elements:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
import './custom-styles.css';
function App() {
const customTheme = {
heading: {
h4: 'my-custom-h4', // Custom CSS class
}
};
return (
<Editor
theme={customTheme}
placeholder="Type your content..."
/>
);
}custom-styles.css:
.my-custom-h4 {
font-size: 16px;
font-weight: bold;
}Documentation:
- Custom Styles Guide - Step-by-step customization guide
- Theme Properties Reference - Complete list of all customizable properties
Dynamic Theme Switching
import { useState } from 'react';
import { Editor } from 'channelwill-editor';
function App() {
const [theme, setTheme] = useState('default');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="default">Default Theme</option>
<option value="polaris">Polaris Theme</option>
</select>
<Editor
placeholder="Type your content..."
uiTheme={theme}
/>
</div>
);
}Editor Commands API
Control editor programmatically with custom buttons using a super simple API:
import { useRef, useState, useEffect, useCallback } from 'react';
import { Editor } from 'channelwill-editor';
import type { LexicalEditor, EditorCommands } from 'channelwill-editor';
function MyEditor() {
const editorRef = useRef<LexicalEditor & EditorCommands>();
const [isBold, setIsBold] = useState(false);
// Use useCallback to prevent re-initialization on every render
const handleEditorInit = useCallback((editor: LexicalEditor & EditorCommands) => {
editorRef.current = editor;
// Setup format state listener
const unsubscribe = editor.onFormatStateChange((state) => {
setIsBold(state.isBold);
// Update your UI based on current format state
});
// You can return cleanup function if needed
// Note: Currently this won't be called, consider using useEffect for cleanup
}, []);
return (
<>
{/* Your custom buttons - super simple API! */}
<button
onClick={() => editorRef.current?.bold()}
style={{ fontWeight: isBold ? 'bold' : 'normal' }}
>
Bold
</button>
<button onClick={() => editorRef.current?.formatHeading(1)}>
H1
</button>
<button onClick={() => editorRef.current?.setFontColor('#ff0000')}>
Red Text
</button>
<Editor
onInit={handleEditorInit}
toolbarItems={[]} // No built-in toolbar
/>
</>
);
}Quick Reference:
- Text:
bold(),italic(),underline(),strikethrough() - Alignment:
alignLeft(),alignCenter(),alignRight() - Block:
formatHeading(1-6),formatQuote(),formatBulletList(),formatNumberedList() - Style:
setFontSize(),setFontColor(),setBackgroundColor(),setFontFamily(),setLineHeight(),clearFormatting() - Link:
insertLink(),removeLink() - History:
undo(),redo() - State:
getFormatState(),onFormatStateChange(callback)⭐ - Content:
insertText(),getSelection()
| Command | Parameters | Description |
|---------|-----------|-------------|
| Text Formatting | | |
| bold() | - | Toggle bold formatting |
| italic() | - | Toggle italic formatting |
| underline() | - | Toggle underline formatting |
| strikethrough() | - | Toggle strikethrough formatting |
| Text Alignment | | |
| alignLeft() | - | Align text to left |
| alignCenter() | - | Align text to center |
| alignRight() | - | Align text to right |
| alignJustify() | - | Justify text |
| Block Formatting | | |
| formatParagraph() | - | Format as normal paragraph |
| formatHeading(level) | level: 1-6 | Format as heading (H1-H6) |
| formatQuote() | - | Format as block quote |
| formatCode() | - | Format as code block |
| formatBulletList() | - | Create bullet list |
| formatNumberedList() | - | Create numbered list |
| Link Management | | |
| insertLink(url?) | url?: string | Insert or update link (default: 'https://') |
| removeLink() | - | Remove link from selection |
| Styling | | |
| setFontSize(size) | size: string | Set font size (e.g., '16' or '16px') |
| setFontColor(color) | color: string | Set text color (hex, rgb, etc.) |
| setBackgroundColor(color) | color: string | Set background color |
| setFontFamily(fontFamily) | fontFamily: string | Set font family (e.g., 'Arial', 'Georgia') |
| setLineHeight(lineHeight) | lineHeight: string | Set line height (e.g., '1.5', '2.0') |
| clearFormatting() | - | Clear all formatting |
| Content Insertion | | |
| insertText(text) | text: string | Insert text at cursor position |
| History | | |
| undo() | - | Undo last action |
| redo() | - | Redo last undone action |
| Selection & State | | |
| getSelection() | - | Get current selection info (bold, italic, text, etc.) |
| getFormatState() | - | Get current format state at cursor |
| onFormatStateChange(callback) | callback: Function | Listen to format state changes (returns unsubscribe function) |
| Markdown | | |
| exportMarkdown(preserveNewLines?) | preserveNewLines?: boolean | Export content as Markdown |
| importMarkdown(markdown, ...) | markdown: string, ... | Import Markdown content |
| getMarkdown(preserveNewLines?) | preserveNewLines?: boolean | Get Markdown content |
Format State Object:
{
isBold: boolean;
isItalic: boolean;
isUnderline: boolean;
isStrikethrough: boolean;
isLink: boolean;
fontSize: string | null; // e.g., '16px'
fontColor: string | null; // e.g., '#ff0000'
backgroundColor: string | null; // e.g., 'rgb(255, 255, 0)'
fontFamily: string | null; // e.g., 'Arial', 'Georgia'
lineHeight: string | null; // e.g., '1.5', '2.0'
}Alternative Syntax:
You can also use editor.commands.bold() instead of editor.bold() - both work identically!
For complete API reference and examples, see Editor Commands API Documentation.
Markdown Support
Import/export Markdown and use Markdown shortcuts while typing:
import { Editor, MarkdownShortcutPlugin } from 'channelwill-editor';
import { useRef } from 'react';
function MarkdownEditor() {
const editorRef = useRef();
const handleExport = async () => {
const markdown = await editorRef.current?.commands.exportMarkdown();
console.log(markdown);
};
const handleImport = async () => {
const markdown = '# Hello\n\nThis is **bold** text';
await editorRef.current?.commands.importMarkdown(markdown);
};
return (
<>
<button onClick={handleExport}>Export Markdown</button>
<button onClick={handleImport}>Import Markdown</button>
<Editor
onInit={(editor) => { editorRef.current = editor; }}
placeholder="Type Markdown shortcuts like # for heading..."
>
<MarkdownShortcutPlugin />
</Editor>
</>
);
}Markdown Features:
- Shortcuts: Type
#for heading,-for bullet list,**bold**for bold text - Import/Export: Convert between editor content and Markdown format
- Supported Syntax: Headings, lists, bold, italic, links, quotes, code blocks
- Commands API:
exportMarkdown(),importMarkdown(),getMarkdown()
Markdown Shortcuts Examples:
#→ Heading 1##→ Heading 2-or*→ Bullet list1.→ Numbered list**text**→ Bold_text_→ Italic`code`→inline code>→ Quote block
For complete guide, see Markdown Support Documentation.
User Plugins
Create custom toolbar plugins to extend the editor functionality:
import { Editor, UserPlugin, PopoverPlugin } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
// Custom button component
const MyCustomButton = ({ context }) => {
const handleClick = () => {
// Access editor and utilities from context
const { editor, $getSelection, INSERT_TEXT_COMMAND } = context;
editor.update(() => {
const selection = $getSelection();
if (selection) {
editor.dispatchCommand(INSERT_TEXT_COMMAND, '🎉 Custom text!');
}
});
};
return (
<button onClick={handleClick} className="toolbar-item">
Custom Button
</button>
);
};
// Define custom plugin
const myPlugin: UserPlugin = {
id: 'my-plugin',
name: 'My Custom Plugin',
buttons: [
{
id: 'custom-action',
label: (context) => <MyCustomButton context={context} />,
onClick: () => {}, // Handled by the button component
tooltip: 'Insert custom text',
}
],
};
function App() {
return (
<Editor
userPlugins={[myPlugin]}
placeholder="Try your custom plugin..."
onChange={(html) => console.log(html)}
/>
);
}Plugin Context API:
The context object provides access to:
editor- Lexical editor instance- Lexical utilities -
$getSelection,$isRangeSelection, etc. - Lexical commands -
INSERT_TEXT_COMMAND,FORMAT_TEXT_COMMAND, etc.
For detailed documentation, see User Plugin Development Guide.
Advanced Usage
Accessing Lexical Native APIs
The editor provides two ways to access Lexical's native APIs for advanced customization:
1. Using onInit Callback
The onInit callback is triggered when the editor is fully initialized, providing direct access to the Lexical editor instance:
import { useRef } from 'react';
import { Editor } from 'channelwill-editor';
import { LexicalEditor, $getRoot, $createParagraphNode, $createTextNode } from 'channelwill-editor/native';
function App() {
const editorRef = useRef<LexicalEditor | null>(null);
return (
<Editor
placeholder="Start typing..."
onInit={(editor) => {
// Save editor instance for later use
editorRef.current = editor;
// Programmatically insert content
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
paragraph.append($createTextNode('Hello World!'));
root.append(paragraph);
});
// Register custom commands
editor.registerCommand(
MY_CUSTOM_COMMAND,
(payload) => {
// Handle command
return true;
},
COMMAND_PRIORITY_EDITOR
);
}}
onChange={(html) => console.log(html)}
/>
);
}2. Importing Lexical Native APIs
All Lexical native APIs are re-exported through the /native submodule for your convenience:
import { Editor } from 'channelwill-editor';
import 'channelwill-editor/dist/style.css';
// Import Lexical native APIs from /native
import {
// Core APIs
LexicalEditor,
$getRoot,
$getSelection,
$createParagraphNode,
$createTextNode,
// Commands
createCommand,
COMMAND_PRIORITY_EDITOR,
FORMAT_TEXT_COMMAND,
// Selection utilities
$isRangeSelection,
$patchStyleText,
// React hooks
useLexicalComposerContext,
// Node types
HeadingNode,
QuoteNode,
ListNode,
LinkNode,
TableNode,
// And many more...
} from 'channelwill-editor/native';Available API Categories:
- ✅ Core Lexical APIs (
$getRoot,$createParagraphNode,$getSelection, etc.) - ✅ Commands and Events (
FORMAT_TEXT_COMMAND,PASTE_COMMAND, etc.) - ✅ Selection utilities (
$patchStyleText,$setBlocksType, etc.) - ✅ HTML conversion (
$generateHtmlFromNodes,$generateNodesFromDOM) - ✅ React hooks (
useLexicalComposerContext, etc.) - ✅ React components (
LexicalComposer,ContentEditable, etc.) - ✅ All node types (Rich text, List, Link, Code, Table nodes)
- ✅ Utility functions (
$wrapNodeInElement,mergeRegister, etc.)
Benefits:
- 📦 Single import source - no need to install or import from multiple
lexicalpackages - 🔒 Version consistency - ensures Lexical APIs match the editor's version
- 📘 Full TypeScript support - complete type definitions included
Use Cases
Programmatic Content Insertion:
function insertCustomContent() {
if (editorRef.current) {
editorRef.current.update(() => {
const root = $getRoot();
const heading = $createHeadingNode('h1');
heading.append($createTextNode('Welcome!'));
root.append(heading);
});
}
}Custom Commands:
const INSERT_PRODUCT_COMMAND = createCommand();
// In onInit:
editor.registerCommand(
INSERT_PRODUCT_COMMAND,
(product) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertText(`Product: ${product.name}`);
}
});
return true;
},
COMMAND_PRIORITY_EDITOR
);
// Later, dispatch the command:
editor.dispatchCommand(INSERT_PRODUCT_COMMAND, { name: 'iPhone' });Reading Editor State:
function getPlainText() {
if (editorRef.current) {
editorRef.current.read(() => {
const root = $getRoot();
const text = root.getTextContent();
console.log('Plain text:', text);
});
}
}API Documentation
Editor Props
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| placeholder | string | "Type your content..." | Placeholder text |
| value | string | - | Editor content (HTML string) |
| onChange | (html: string, editor: LexicalEditor) => void | - | Content change callback |
| onInit | (editor: LexicalEditor) => void | - | Editor initialization callback, provides access to Lexical editor instance |
| showTreeView | boolean | false | Whether to show debug tree view |
| enableTable | boolean | true | Whether to enable table functionality |
| disabled | boolean | false | Whether to disable the editor (read-only mode, Lexical editor still loaded) |
| readOnly | boolean | false | Pure HTML display mode (no Lexical editor, better performance, 100% HTML fidelity) |
| readOnlyConfig | ReadOnlyConfig | - | Configuration for readOnly mode (useIframe, notAllowTags, onClick) |
| className | string | "" | Custom CSS class name |
| theme | EditorTheme | - | Lexical editor theme |
| uiTheme | 'default' \| 'polaris' | 'default' | UI theme |
| uiConfig | EditorUIConfig | - | Custom UI configuration |
| toolbar | ReactNode | - | Custom toolbar component |
| onError | (error: Error) => void | - | Error handling function |
| toolbarItems | ToolbarItem[] | All available items | Toolbar buttons configuration |
| floatToolbar | ToolbarItem[] | undefined | Floating toolbar items (shown on text selection) |
| userPlugins | UserPlugin[] | [] | Custom user plugins for extending toolbar |
| fontFamilyItems | FontFamilyOption[] | Default fonts | Custom font family options for fontFamily dropdown |
| lineHeightItems | LineHeightOption[] | Default line heights | Custom line height options for lineHeight dropdown |
| blockFormatItems | BlockFormatType[] | All formats | Block format items to display in blockFormat dropdown |
| styleMode | 'outside' \| 'inline' | 'outside' | HTML output style mode: 'outside' (CSS classes) or 'inline' (inline styles for email) |
| inlineStyleOptions | InlineStyleOptions | {} | Options for inline style conversion (auto-detection, custom mappings, etc.) |
| pureHTML | boolean | false | Whether to output pure HTML without Lexical-generated span wrappers (see Pure HTML Output) |
| enableHtmlSource | boolean | false | Enable HTML source editing mode |
| htmlSourceConfig | HtmlSourceConfig | - | HTML source editor configuration |
| codeMode | boolean | false | Display only CodeMirror HTML editor without Lexical rich text editor (see Code Mode) |
| imageUploadConfig | ImageUploadConfig | - | Image upload configuration |
| height | string \| number | '350px' | Editor height |
| maxLength | number | - | Maximum text length limit (text only, excludes HTML tags and rich media) |
| maxLengthParams | MaxLengthParams | - | Character counter display configuration |
| htmlImportOptions | HTMLImportOptions | - | HTML import configuration for preserving formatting |
RichTextEditor Props
Simplified editor with fewer configuration options:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| placeholder | string | "Type your content..." | Placeholder text |
| value | string | - | Editor content (HTML string) |
| onChange | (html: string) => void | - | Content change callback |
| className | string | "" | Custom CSS class name |
| uiTheme | 'default' \| 'polaris' | 'default' | UI theme |
Type Definitions
type ToolbarItem =
| 'blockFormat'
| 'history'
| 'bold'
| 'italic'
| 'underline'
| 'strikethrough'
| 'alignment'
| 'fontSize'
| 'fontFamily'
| 'lineHeight'
| 'fontColor'
| 'fontBackgound'
| 'link'
| 'image'
| 'emoji'
| 'clean'
| 'code'
| 'ai'
| string; // Support for custom user plugin button IDs
type FontFamilyOption = [string, string]; // [value, label]
type LineHeightOption = [string, string]; // [value, label]
type BlockFormatType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'bullet' | 'number' | 'quote' | 'code';
type StyleMode = 'outside' | 'inline'; // 'outside' uses CSS classes, 'inline' uses inline styles
interface InlineStyleOptions {
customMappings?: Record<string, string>; // Manual class-to-style mappings
autoDetectStyles?: boolean; // Auto-detect from document.styleSheets (default: true)
includeDefaults?: boolean; // Include default Lexical mappings (default: true)
}
// Available customMappings class names:
// Text Formatting: editor-text-bold, editor-text-italic, editor-text-underline, editor-text-strikethrough,
// editor-text-underlineStrikethrough, editor-text-code, editor-text-subscript, editor-text-superscript
// Alignment: editor-text-left, editor-text-center, editor-text-right, editor-text-justify
// Headings: editor-heading-h1, editor-heading-h2, editor-heading-h3, editor-heading-h4, editor-heading-h5, editor-heading-h6
// Lists: editor-list-ul, editor-list-ol, editor-listitem, editor-nested-listitem
// Other: editor-quote, editor-code, editor-link, editor-paragraph
interface HtmlSourceConfig {
onModeChange?: (isSourceMode: boolean) => void;
isSourceMode?: boolean;
theme?: 'light' | 'dark';
showLineNumbers?: boolean;
}
interface ImageUploadConfig {
limitSize?: number; // File size limit in bytes
limitType?: string[]; // Allowed file types (default: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])
hash?: boolean; // Whether to hash filenames
onChange?: (file: File) => void; // File selection callback
uploadHandler: ( // Upload function (required)
file: File,
onProgress?: (progress: number) => void
) => Promise<string>; // Must return uploaded image URL
}
interface HTMLImportOptions {
preserveInlineStyles?: boolean; // Preserve inline styles (style attribute), default: true
preserveClassNames?: boolean; // Preserve CSS classes (class attribute), default: true
preserveDataAttributes?: boolean; // Preserve data-* attributes, default: true
preservedStyles?: string[]; // Specific styles to preserve
preserveUnknownHTML?: boolean; // Preserve unknown HTML elements, default: false
}
interface MaxLengthParams {
alignment?: 'left' | 'right'; // Character counter position
visible?: boolean; // Whether to show character counter
size?: 'small' | 'normal' | 'large'; // Character counter size
color?: string; // Character counter color
}UI Theme System
Supported Themes
- Default Theme - Uses built-in CSS icon system
- Polaris Theme - Uses Shopify Polaris design system
Custom Themes
import { UIThemeProvider } from 'channelwill-editor';
const myCustomTheme: UIThemeProvider = {
icons: {
bold: ({ className }) => <MyBoldIcon className={className} />,
italic: ({ className }) => <MyItalicIcon className={className} />,
// ... other icons
},
components: {
Button: ({ onClick, active, children }) => (
<MyButton onClick={onClick} isActive={active}>
{children}
</MyButton>
),
Divider: () => <MyDivider />,
},
};
<Editor
uiConfig={{
customProvider: myCustomTheme
}}
/>For detailed theme system documentation, see UI_THEMES.md.
Extended Editor Methods
When image upload is configured, the editor instance is automatically extended with additional methods:
// Get editor instance from onChange callback
const handleChange = (html: string, editor: LexicalEditor) => {
// Insert image programmatically
editor.insertImage('https://example.com/image.jpg', 'Alt text');
// Update upload progress (for custom upload UI)
editor.setImageUploadProgress('node-key', 75);
// Set upload success
editor.setImageUploadSuccess('node-key', 'https://uploaded-url.jpg');
// Set upload failure
editor.setImageUploadFailed('node-key', 'Upload failed message');
};
<Editor
imageUploadConfig={config}
onChange={handleChange}
/>Style Customization
/* Custom editor styles */
.editor-container {
border: 2px solid #e1e5e9;
border-radius: 8px;
}
.editor-input {
min-height: 150px;
font-size: 16px;
line-height: 1.6;
}
.toolbar {
background-color: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
}Development
# Clone the project
git clone https://github.com/your-username/channelwill-editor.git
cd channelwill-editor
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build library
pnpm build:lib
# Publish
pnpm releaseDependencies
Required Peer Dependencies
- React ≥ 16.8.0
- React DOM ≥ 16.8.0
Note: All Lexical dependencies are bundled with the editor and installed automatically.
Optional Dependencies
emoji-picker-react- For emoji picker functionality@shopify/polaris&@shopify/polaris-icons- For Shopify Polaris theme@uiw/react-codemirror&@codemirror/lang-html- For HTML source code editingopenai- For AI assistant features
License
MIT
Contributing
Issues and Pull Requests are welcome!
