GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - stacksjs/ts-medium-editor: A modern, minimal & performant Medium-like rich text editor.

BranchesTags
Open more actions menu

Latest commit

 

History

118 Commits
118 Commits

Folders and files

NameName
Last commit message
Last commit date
.config
.config
 
 
.github
.github
 
 
.vscode
.vscode
 
 
demo
demo
 
 
docs
docs
 
 
src
src
 
 
test
test
 
 
.editorconfig
.editorconfig
 
 
.gitattributes
.gitattributes
 
 
.gitignore
.gitignore
 
 
CHANGELOG.md
CHANGELOG.md
 
 
CLAUDE.md
CLAUDE.md
 
 
LICENSE.md
LICENSE.md
 
 
README.md
README.md
 
 
build.ts
build.ts
 
 
bun.lock
bun.lock
 
 
bunfig.toml
bunfig.toml
 
 
deps.yaml
deps.yaml
 
 
happy-dom.ts
happy-dom.ts
 
 
package.json
package.json
 
 
pantry.lock
pantry.lock
 
 
tsconfig.json
tsconfig.json
 
 

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
  • 🖼️ Published Articles - Medium-style figures, image grids, click-to-zoom and embeds for the reading side

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

Publishing Articles

The editor writes content; ts-medium-editor/article shows it to readers the way Medium does. renderArticle is a pure HTML-to-HTML transform with no DOM, so it runs on the server or at build time, over the editor's output or over whatever a markdown renderer produced:

import { mountArticle, renderArticle } from 'ts-medium-editor/article'
import 'ts-medium-editor/css/article.css'

const html = renderArticle(markdownHtml, {
  // Optional: sizes let grids line images up exactly, with nothing cropped.
  resolveImage: src => imageSizes[src],
})

// In the browser, once: click-to-zoom and auto-sizing embeds.
mountArticle()

What it does:

  • Figures. An image alone in its paragraph becomes a <figure>, and its title (![alt](src "A caption")) becomes the <figcaption>.
  • Grids. Adjacent images are laid out together: two side by side, three with the tallest on the left and the other two stacked on its right, four as a 2x2, more in rows of three and two. With sizes known, columns are proportioned so every image shows whole at one shared height.
  • Loading. loading="lazy" after the first image, decoding="async", and width and height when known, so nothing shifts as images arrive.
  • Zoom. Images get data-zoomable; mountArticle() zooms them to fit the viewport on click or Enter, and closes on click, Escape or scroll. It respects reduced motion.
  • Embeds. A link alone on its own line becomes an embed when a provider recognizes it: YouTube, Vimeo and HQ.training activities out of the box, or your own EmbedProviders. Auto-height embeds resize to the height their frame posts as { type: 'embed:height', height }.

Everything is optional (grids, zoom, lazy, embeds), and the output is stable: rendering it again changes nothing. toMarkdown reads captions back as image titles and embeds back as their links, so content round-trips between the editor, markdown and the published page.

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:

  • 💬 GitHub Discussions
  • 🎮 Discord Server
  • 🐛 Issue Tracker

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

⭐ Star us on GitHub • 🐦 Follow on Bluesky • 💬 Join Discord

About

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

ts-medium-editor.netlify.app

Topics

editorlightweightmediumrich-text-editortypescriptwysiwyg

Resources

Readme
MIT license

Code of conduct

Code of conduct

Contributing

Contributing

Security policy

Security policy
Activity
Custom properties

Stars

5 stars

Watchers

0 watching

Forks

0 forks
Report repository

Releases

Sponsor this project

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.