npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

channelwill-editor

v1.1.103

Published

A customizable rich text editor built with Lexical

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

  • 🎨 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 /native submodule and onInit callback
  • 🎯 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-editor

That'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 openai

Quick 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 tags
  • onClick - Click event handler that provides:
    • clickNode - The clicked HTML element
    • parentNode - 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 like data-id="123"
  • Automatic: Works automatically when importing HTML via the value prop

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 via fontFamilyItems prop)
  • 'lineHeight' - Line height dropdown (customizable via lineHeightItems prop)
  • '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 text
  • h1 - Heading 1
  • h2 - Heading 2
  • h3 - Heading 3
  • h4 - Heading 4
  • h5 - Heading 5
  • h6 - Heading 6
  • bullet - Bullet list
  • number - Numbered list
  • quote - Quote block
  • code - 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):

  1. customMappings - Manual mappings you provide
  2. Auto-detected styles from document.styleSheets
  3. 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 disabled prop 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:

  1. Below button (default) - Shows below if there's enough space
  2. Right side - If not enough space below, tries right side first
  3. Left side - If right side doesn't fit, tries left side
  4. Above button - If no side fits, shows above
  5. 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-react

Image 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-right
  • visible: Whether to show character counter, true to show, false to hide
  • size: 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:

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 list
  • 1. → 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 lexical packages
  • 🔒 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

  1. Default Theme - Uses built-in CSS icon system
  2. 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 release

Dependencies

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 editing
  • openai - For AI assistant features

License

MIT

Contributing

Issues and Pull Requests are welcome!