Skip to content

stacksjs/ts-medium-editor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

38 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

TypeScript Medium Editor - A modern WYSIWYG editor

npm version GitHub Actions Commitizen friendly License Discord

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">
    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

Advanced Configurations

Multiple Editors

Specialized Use Cases

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:

bun test

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:

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

โญ Star us on GitHub โ€ข ๐Ÿฆ Follow on Bluesky โ€ข ๐Ÿ’ฌ Join Discord

About

A modern, minimal & performant Medium-like rich text editor.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors 2

  •  
  •