A modern TypeScript port of the popular Medium.com-style WYSIWYG editor
Features โข Installation โข Quick Start โข Live Demos โข API โข Examples โข Contributing
- ๐ 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
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
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...' } })
<!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"> import { MediumEditor } from './node_modules/ts-medium-editor/dist/index.js' const editor = new MediumEditor('.editable', { placeholder: { text: 'Tell your story...' } }) </script> </body> </html>
Explore our comprehensive demo collection to see all features in action:
- 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
- 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
- Multi-Editor - Multiple independent editor instances
- Single Instance - Dynamic element addition to existing editors
- Nested Editable - Complex nested contenteditable layouts
- 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
For optimal TypeScript support, configure your tsconfig.json
:
{ "compilerOptions": { "lib": ["esnext", "dom", "dom.iterable"], "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, "skipLibCheck": true } }
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 }
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 }
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' } })
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' } })
// 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...' } })
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'] } })
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') })
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() } })
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` })
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">
// 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') } })
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' } } ] } })
Run the test suite:
bun test
For help, discussion about best practices, or any other conversation:
- ๐ฌ GitHub Discussions
- ๐ฎ Discord Server
- ๐ Issue Tracker
โ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 ๐
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 and support open source development.
- 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
The MIT License (MIT). Please see LICENSE for more information.
Made with ๐ by the Stacks team
โญ Star us on GitHub โข ๐ฆ Follow on Bluesky โข ๐ฌ Join Discord