A modern TypeScript port of the popular Medium.com-style WYSIWYG editor
Features • Installation • Quick Start • Live Demos • API • Examples • Contributing
Features
- 📝 Medium-like Editor - A modern TypeScript port of the popular Medium.com-style WYSIWYG editor
- 🔧 Extensible Architecture - Plugin system for custom functionality and toolbar buttons
- 📱 Mobile Friendly - Touch and mobile device support with responsive design
- 🎨 Customizable Themes - 7 built-in themes plus extensive styling options
- ⚡ Lightweight - Zero dependencies, small bundle size
- 🔒 Type Safe - Full TypeScript support with comprehensive type definitions
- 🎯 Auto-Link Detection - Automatically converts URLs to clickable links
- 📋 Smart Paste - Cleans up pasted content from Word, Google Docs, etc.
- 🔄 Event System - Comprehensive event handling for content changes
- 🎛️ Flexible Toolbars - Static, floating, or custom positioned toolbars
Installation
Choose your preferred package manager:
# npm npm install ts-medium-editor # yarn yarn add ts-medium-editor # pnpm pnpm add ts-medium-editor # bun bun add ts-medium-editor
Quick Start
Basic Setup
import { MediumEditor } from 'ts-medium-editor' import 'ts-medium-editor/css/medium-editor.css' import 'ts-medium-editor/css/themes/default.css' // Initialize editor const editor = new MediumEditor('.editable', { toolbar: { buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'] }, placeholder: { text: 'Tell your story...' } })
HTML Structure
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Editor</title> <link rel="stylesheet" href="node_modules/ts-medium-editor/css/medium-editor.css"> <link rel="stylesheet" href="node_modules/ts-medium-editor/css/themes/default.css"> </head> <body> <div class="editable"> <p>Start typing here...</p> </div> <script type="module"> /_ eslint-disable markdown/reference-links-images, markdown/blanks-around-tables, markdown/table-pipe-style, markdown/no-reversed-links, markdown/single-title, markdown/link-image-reference-definitions, markdown/table-column-count _/ import { MediumEditor } from './node_modules/ts-medium-editor/dist/index.js' const editor = new MediumEditor('.editable', { placeholder: { text: 'Tell your story...' } }) </script> </body> </html>
Live Demos
Explore our comprehensive demo collection to see all features in action:
Core Features
- Basic Editor - Simple setup with essential toolbar
- Auto-Link Detection - Automatic URL to link conversion
- Clean Paste - Smart content cleaning from Word/Google Docs
- Textarea Support - Enhance HTML textareas with rich editing
Advanced Configurations
- Custom Toolbars - 5 different toolbar configurations
- Static Toolbar - Always-visible toolbars with alignment options
- Button Examples - Custom button creation with Rangy integration
- Extension Examples - 4 powerful extensions with Shiki syntax highlighting
Multiple Editors
- Multi-Editor - Multiple independent editor instances
- Single Instance - Dynamic element addition to existing editors
- Nested Editable - Complex nested contenteditable layouts
Specialized Use Cases
- Multi-Paragraph - Toolbar behavior with paragraph selection
- Relative Toolbar - Constrained toolbar positioning
- Absolute Container - Absolute positioned container examples
- Custom Extensions - Instance-aware extension development
- Table Extension - Custom table insertion functionality
TypeScript Configuration
For optimal TypeScript support, configure your tsconfig.json:
{
"compilerOptions": {
"lib": ["esnext", "dom", "dom.iterable"],
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}API Reference
Constructor Options
interface MediumEditorOptions { // Core Settings activeButtonClass?: string // CSS class for active buttons buttonLabels?: boolean | string | ButtonLabels // Button label configuration delay?: number // Toolbar show delay (ms) disableReturn?: boolean // Disable return key disableDoubleReturn?: boolean // Disable double return disableExtraSpaces?: boolean // Prevent extra spaces disableEditing?: boolean // Make editor read-only spellcheck?: boolean // Enable spellcheck // Auto-features autoLink?: boolean // Auto-convert URLs to links targetBlank?: boolean // Open links in new tab imageDragging?: boolean // Enable image drag-and-drop fileDragging?: boolean // Enable file drag-and-drop // DOM Configuration elementsContainer?: HTMLElement // Container for editor elements contentWindow?: Window // Window context ownerDocument?: Document // Document context // Extensions extensions?: Record<string, Extension> // Custom extensions // Feature Modules toolbar?: ToolbarOptions | false // Toolbar configuration anchorPreview?: AnchorPreviewOptions | false // Link preview placeholder?: PlaceholderOptions | false // Placeholder text anchor?: AnchorOptions | false // Link creation paste?: PasteOptions | false // Paste handling keyboardCommands?: KeyboardOptions | false // Keyboard shortcuts }
Core Methods
class MediumEditor { // Lifecycle constructor(elements: Elements, options?: MediumEditorOptions) setup(): MediumEditor destroy(): void // Content Management getContent(index?: number): string setContent(html: string, index?: number): void serialize(): Record<string, string> resetContent(element?: HTMLElement): void // Element Management addElements(elements: Elements): void removeElements(elements: Elements): void // Selection Management exportSelection(): SelectionState | null importSelection(state: SelectionState, favorLater?: boolean): void saveSelection(): void restoreSelection(): void selectAllContents(): void selectElement(element: HTMLElement): void // Event Handling subscribe(event: string, listener: EventListener): MediumEditor unsubscribe(event: string, listener: EventListener): MediumEditor trigger(event: string, data?: any, editable?: HTMLElement): MediumEditor // Actions execAction(action: string, opts?: any): boolean queryCommandState(action: string): boolean }
Examples
Custom Toolbar with FontAwesome
const editor = new MediumEditor('.editable', { buttonLabels: 'fontawesome', toolbar: { buttons: [ 'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'anchor', 'image', 'quote', 'pre', 'orderedlist', 'unorderedlist', 'indent', 'outdent', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], static: true, sticky: true, align: 'center' } })
Auto-Link Configuration
const editor = new MediumEditor('.editable', { autoLink: true, targetBlank: true, toolbar: { buttons: ['bold', 'italic', 'anchor'] }, anchor: { placeholderText: 'Enter a URL', targetCheckbox: true, targetCheckboxText: 'Open in new tab' } })
Multiple Editors with Different Configs
// Title editor (no line breaks) const titleEditor = new MediumEditor('.title', { disableReturn: true, disableExtraSpaces: true, toolbar: { buttons: ['bold', 'italic'] }, placeholder: { text: 'Enter title...' } }) // Content editor (full features) const contentEditor = new MediumEditor('.content', { autoLink: true, toolbar: { buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'orderedlist', 'unorderedlist'] }, placeholder: { text: 'Tell your story...' } })
Smart Paste Configuration
const editor = new MediumEditor('.editable', { paste: { forcePlainText: false, cleanPastedHTML: true, cleanReplacements: [ [/\s_style\s_=\s_["'][^"']_["']/gi, ''], // Remove inline styles [/<o:p\s_\/?>|<\/o:p>/gi, ''], // Remove Word tags [/<xml>[\s\S]_?<\/xml>/gi, ''], // Remove XML [/<!--[\s\S]*?-->/g, ''] // Remove comments ], cleanAttrs: ['class', 'style', 'dir'], cleanTags: ['meta', 'style', 'script', 'object', 'embed'] } })
Event Handling
const editor = new MediumEditor('.editable') // Content change events editor.subscribe('editableInput', (event, editable) => { console.log('Content changed:', editable.innerHTML) // Auto-save logic here }) // Selection change events editor.subscribe('editableKeyup', (event, editable) => { const selection = editor.exportSelection() console.log('Cursor position:', selection) }) // Focus events editor.subscribe('focus', (event, editable) => { console.log('Editor focused') }) editor.subscribe('blur', (event, editable) => { console.log('Editor blurred') })
Creating Custom Extensions
import { MediumEditorExtension } from 'ts-medium-editor' class EmojiExtension implements MediumEditorExtension { name = 'emoji' private button!: HTMLButtonElement private base: any init(): void { this.button = this.createButton() } getButton(): HTMLButtonElement { return this.button } private createButton(): HTMLButtonElement { const button = document.createElement('button') button.className = 'medium-editor-action' button.innerHTML = '😀' button.title = 'Insert Emoji' button.addEventListener('click', this.handleClick.bind(this)) return button } private handleClick(): void { const emoji = '🎉' const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) range.deleteContents() range.insertNode(document.createTextNode(emoji)) range.collapse(false) selection.removeAllRanges() selection.addRange(range) } } destroy(): void { if (this.button) { this.button.removeEventListener('click', this.handleClick) } } } // Use the extension const editor = new MediumEditor('.editable', { toolbar: { buttons: ['bold', 'italic', 'emoji'] }, extensions: { emoji: new EmojiExtension() } })
Theme Switching
const themeSelector = document.getElementById('theme-select') as HTMLSelectElement const themeLink = document.getElementById('theme-css') as HTMLLinkElement const themes = [ 'default', 'beagle', 'bootstrap', 'flat', 'mani', 'roman', 'tim' ] themeSelector.addEventListener('change', (event) => { const theme = (event.target as HTMLSelectElement).value themeLink.href = `./dist/css/themes/${theme}.css` })
Available Themes
The library includes 7 beautiful themes:
- Default - Clean, modern design
- Beagle - Friendly, rounded interface
- Bootstrap - Bootstrap-compatible styling
- Flat - Minimalist flat design
- Mani - Elegant, sophisticated look
- Roman - Classic, serif-inspired
- Tim - Bold, high-contrast theme
<!-- Include your chosen theme --> <link rel="stylesheet" href="dist/css/themes/default.css">
Advanced Configuration
Toolbar Positioning
// Static toolbar (always visible) const editor = new MediumEditor('.editable', { toolbar: { static: true, sticky: true, align: 'center' } }) // Relative container const editor = new MediumEditor('.editable', { toolbar: { relativeContainer: document.getElementById('toolbar-container') } })
Custom Button Configuration
const editor = new MediumEditor('.editable', { toolbar: { buttons: [ 'bold', 'italic', { name: 'highlight', action: 'highlight', aria: 'Highlight text', contentDefault: 'H', classList: ['custom-highlight-button'], attrs: { 'data-action': 'highlight' } } ] } })
Testing
Run the test suite:
Community
For help, discussion about best practices, or any other conversation:
Postcardware
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
Sponsors
We would like to extend our thanks to the following sponsors for funding Stacks development:
- JetBrains - Professional development tools
- The Solana Foundation - Blockchain infrastructure
Become a Sponsor
Become a sponsor and support open source development.
Credits
- Medium - For the beautiful editor design inspiration
- medium-editor - The original JavaScript implementation that inspired this TypeScript port
- Chris Breuer - Primary maintainer and TypeScript port author
- All Contributors - Everyone who has contributed to making this project better
License
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙 by the Stacks team
