{"id":29874060,"url":"https://github.com/stacksjs/ts-medium-editor","last_synced_at":"2025-07-31T00:02:17.780Z","repository":{"id":297247972,"uuid":"996164493","full_name":"stacksjs/ts-medium-editor","owner":"stacksjs","description":"A modern, minimal \u0026 performant Medium-like rich text editor.","archived":false,"fork":false,"pushed_at":"2025-07-19T15:52:06.000Z","size":1459,"stargazers_count":3,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-19T19:29:51.734Z","etag":null,"topics":["editor","lightweight","medium","rich-text-editor","typescript","wysiwyg"],"latest_commit_sha":null,"homepage":"https://ts-medium-editor.netlify.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stacksjs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["stacksjs","chrisbbreuer"],"open_collective":"stacksjs"}},"created_at":"2025-06-04T14:46:53.000Z","updated_at":"2025-07-12T04:07:44.000Z","dependencies_parsed_at":"2025-06-24T02:21:35.200Z","dependency_job_id":"4f6f0401-7151-4e30-8a6a-9a865d87fdc7","html_url":"https://github.com/stacksjs/ts-medium-editor","commit_stats":null,"previous_names":["stacksjs/ts-medium-editor"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/stacksjs/ts-medium-editor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacksjs%2Fts-medium-editor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacksjs%2Fts-medium-editor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacksjs%2Fts-medium-editor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacksjs%2Fts-medium-editor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stacksjs","download_url":"https://codeload.github.com/stacksjs/ts-medium-editor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacksjs%2Fts-medium-editor/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266017917,"owners_count":23864885,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["editor","lightweight","medium","rich-text-editor","typescript","wysiwyg"],"created_at":"2025-07-31T00:01:31.062Z","updated_at":"2025-07-31T00:02:17.754Z","avatar_url":"https://github.com/stacksjs.png","language":"TypeScript","funding_links":["https://github.com/sponsors/stacksjs","https://github.com/sponsors/chrisbbreuer","https://opencollective.com/stacksjs"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\u003cimg src=\".github/art/cover.jpg\" alt=\"TypeScript Medium Editor - A modern WYSIWYG editor\"\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/ts-medium-editor\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/ts-medium-editor?style=flat-square\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/stacksjs/ts-medium-editor/actions?query=workflow%3Aci\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/stacksjs/ts-medium-editor/ci.yml?style=flat-square\u0026branch=main\" alt=\"GitHub Actions\"\u003e\u003c/a\u003e\n  \u003ca href=\"http://commitizen.github.io/cz-cli/\"\u003e\u003cimg src=\"https://img.shields.io/badge/commitizen-friendly-brightgreen.svg\" alt=\"Commitizen friendly\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/stacksjs/ts-medium-editor/blob/main/LICENSE.md\"\u003e\u003cimg src=\"https://img.shields.io/github/license/stacksjs/ts-medium-editor?style=flat-square\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://discord.gg/5gHFD8Uk3K\"\u003e\u003cimg src=\"https://img.shields.io/discord/928731270204653568?style=flat-square\u0026label=discord\u0026color=5865F2\" alt=\"Discord\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eA modern TypeScript port of the popular Medium.com-style WYSIWYG editor\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#features\"\u003eFeatures\u003c/a\u003e •\n  \u003ca href=\"#installation\"\u003eInstallation\u003c/a\u003e •\n  \u003ca href=\"#quick-start\"\u003eQuick Start\u003c/a\u003e •\n  \u003ca href=\"#demos\"\u003eLive Demos\u003c/a\u003e •\n  \u003ca href=\"#api-reference\"\u003eAPI\u003c/a\u003e •\n  \u003ca href=\"#examples\"\u003eExamples\u003c/a\u003e •\n  \u003ca href=\"#contributing\"\u003eContributing\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Features\n\n- 📝 **Medium-like Editor** - A modern TypeScript port of the popular Medium.com-style WYSIWYG editor\n- 🔧 **Extensible Architecture** - Plugin system for custom functionality and toolbar buttons\n- 📱 **Mobile Friendly** - Touch and mobile device support with responsive design\n- 🎨 **Customizable Themes** - 7 built-in themes plus extensive styling options\n- ⚡ **Lightweight** - Zero dependencies, small bundle size\n- 🔒 **Type Safe** - Full TypeScript support with comprehensive type definitions\n- 🎯 **Auto-Link Detection** - Automatically converts URLs to clickable links\n- 📋 **Smart Paste** - Cleans up pasted content from Word, Google Docs, etc.\n- 🔄 **Event System** - Comprehensive event handling for content changes\n- 🎛️ **Flexible Toolbars** - Static, floating, or custom positioned toolbars\n\n## Installation\n\nChoose your preferred package manager:\n\n```bash\n# npm\nnpm install ts-medium-editor\n\n# yarn\nyarn add ts-medium-editor\n\n# pnpm\npnpm add ts-medium-editor\n\n# bun\nbun add ts-medium-editor\n```\n\n## Quick Start\n\n### Basic Setup\n\n```typescript\nimport { MediumEditor } from 'ts-medium-editor'\nimport 'ts-medium-editor/css/medium-editor.css'\nimport 'ts-medium-editor/css/themes/default.css'\n\n// Initialize editor\nconst editor = new MediumEditor('.editable', {\n  toolbar: {\n    buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']\n  },\n  placeholder: {\n    text: 'Tell your story...'\n  }\n})\n```\n\n### HTML Structure\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003ctitle\u003eMy Editor\u003c/title\u003e\n  \u003clink rel=\"stylesheet\" href=\"node_modules/ts-medium-editor/css/medium-editor.css\"\u003e\n  \u003clink rel=\"stylesheet\" href=\"node_modules/ts-medium-editor/css/themes/default.css\"\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv class=\"editable\"\u003e\n    \u003cp\u003eStart typing here...\u003c/p\u003e\n  \u003c/div\u003e\n\n  \u003cscript type=\"module\"\u003e\n    import { MediumEditor } from './node_modules/ts-medium-editor/dist/index.js'\n\n    const editor = new MediumEditor('.editable', {\n      placeholder: { text: 'Tell your story...' }\n    })\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Live Demos\n\nExplore our comprehensive demo collection to see all features in action:\n\n### Core Features\n- **[Basic Editor](demo/index.html)** - Simple setup with essential toolbar\n- **[Auto-Link Detection](demo/auto-link.html)** - Automatic URL to link conversion\n- **[Clean Paste](demo/clean-paste.html)** - Smart content cleaning from Word/Google Docs\n- **[Textarea Support](demo/textarea.html)** - Enhance HTML textareas with rich editing\n\n### Advanced Configurations\n- **[Custom Toolbars](demo/custom-toolbar.html)** - 5 different toolbar configurations\n- **[Static Toolbar](demo/static-toolbar.html)** - Always-visible toolbars with alignment options\n- **[Button Examples](demo/button-example.html)** - Custom button creation with Rangy integration\n- **[Extension Examples](demo/extension-example.html)** - 4 powerful extensions with Shiki syntax highlighting\n\n### Multiple Editors\n- **[Multi-Editor](demo/multi-editor.html)** - Multiple independent editor instances\n- **[Single Instance](demo/multi-one-instance.html)** - Dynamic element addition to existing editors\n- **[Nested Editable](demo/nested-editable.html)** - Complex nested contenteditable layouts\n\n### Specialized Use Cases\n- **[Multi-Paragraph](demo/multi-paragraph.html)** - Toolbar behavior with paragraph selection\n- **[Relative Toolbar](demo/relative-toolbar.html)** - Constrained toolbar positioning\n- **[Absolute Container](demo/absolute-container.html)** - Absolute positioned container examples\n- **[Custom Extensions](demo/pass-instance.html)** - Instance-aware extension development\n- **[Table Extension](demo/table-extension.html)** - Custom table insertion functionality\n\n## TypeScript Configuration\n\nFor optimal TypeScript support, configure your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n## API Reference\n\n### Constructor Options\n\n```typescript\ninterface MediumEditorOptions {\n  // Core Settings\n  activeButtonClass?: string // CSS class for active buttons\n  buttonLabels?: boolean | string | ButtonLabels // Button label configuration\n  delay?: number // Toolbar show delay (ms)\n  disableReturn?: boolean // Disable return key\n  disableDoubleReturn?: boolean // Disable double return\n  disableExtraSpaces?: boolean // Prevent extra spaces\n  disableEditing?: boolean // Make editor read-only\n  spellcheck?: boolean // Enable spellcheck\n\n  // Auto-features\n  autoLink?: boolean // Auto-convert URLs to links\n  targetBlank?: boolean // Open links in new tab\n  imageDragging?: boolean // Enable image drag-and-drop\n  fileDragging?: boolean // Enable file drag-and-drop\n\n  // DOM Configuration\n  elementsContainer?: HTMLElement // Container for editor elements\n  contentWindow?: Window // Window context\n  ownerDocument?: Document // Document context\n\n  // Extensions\n  extensions?: Record\u003cstring, Extension\u003e // Custom extensions\n\n  // Feature Modules\n  toolbar?: ToolbarOptions | false // Toolbar configuration\n  anchorPreview?: AnchorPreviewOptions | false // Link preview\n  placeholder?: PlaceholderOptions | false // Placeholder text\n  anchor?: AnchorOptions | false // Link creation\n  paste?: PasteOptions | false // Paste handling\n  keyboardCommands?: KeyboardOptions | false // Keyboard shortcuts\n}\n```\n\n### Core Methods\n\n```typescript\nclass MediumEditor {\n  // Lifecycle\n  constructor(elements: Elements, options?: MediumEditorOptions)\n  setup(): MediumEditor\n  destroy(): void\n\n  // Content Management\n  getContent(index?: number): string\n  setContent(html: string, index?: number): void\n  serialize(): Record\u003cstring, string\u003e\n  resetContent(element?: HTMLElement): void\n\n  // Element Management\n  addElements(elements: Elements): void\n  removeElements(elements: Elements): void\n\n  // Selection Management\n  exportSelection(): SelectionState | null\n  importSelection(state: SelectionState, favorLater?: boolean): void\n  saveSelection(): void\n  restoreSelection(): void\n  selectAllContents(): void\n  selectElement(element: HTMLElement): void\n\n  // Event Handling\n  subscribe(event: string, listener: EventListener): MediumEditor\n  unsubscribe(event: string, listener: EventListener): MediumEditor\n  trigger(event: string, data?: any, editable?: HTMLElement): MediumEditor\n\n  // Actions\n  execAction(action: string, opts?: any): boolean\n  queryCommandState(action: string): boolean\n}\n```\n\n## Examples\n\n### Custom Toolbar with FontAwesome\n\n```typescript\nconst editor = new MediumEditor('.editable', {\n  buttonLabels: 'fontawesome',\n  toolbar: {\n    buttons: [\n      'bold',\n      'italic',\n      'underline',\n      'strikethrough',\n      'subscript',\n      'superscript',\n      'anchor',\n      'image',\n      'quote',\n      'pre',\n      'orderedlist',\n      'unorderedlist',\n      'indent',\n      'outdent',\n      'justifyLeft',\n      'justifyCenter',\n      'justifyRight',\n      'justifyFull',\n      'h1',\n      'h2',\n      'h3',\n      'h4',\n      'h5',\n      'h6'\n    ],\n    static: true,\n    sticky: true,\n    align: 'center'\n  }\n})\n```\n\n### Auto-Link Configuration\n\n```typescript\nconst editor = new MediumEditor('.editable', {\n  autoLink: true,\n  targetBlank: true,\n  toolbar: {\n    buttons: ['bold', 'italic', 'anchor']\n  },\n  anchor: {\n    placeholderText: 'Enter a URL',\n    targetCheckbox: true,\n    targetCheckboxText: 'Open in new tab'\n  }\n})\n```\n\n### Multiple Editors with Different Configs\n\n```typescript\n// Title editor (no line breaks)\nconst titleEditor = new MediumEditor('.title', {\n  disableReturn: true,\n  disableExtraSpaces: true,\n  toolbar: {\n    buttons: ['bold', 'italic']\n  },\n  placeholder: {\n    text: 'Enter title...'\n  }\n})\n\n// Content editor (full features)\nconst contentEditor = new MediumEditor('.content', {\n  autoLink: true,\n  toolbar: {\n    buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'orderedlist', 'unorderedlist']\n  },\n  placeholder: {\n    text: 'Tell your story...'\n  }\n})\n```\n\n### Smart Paste Configuration\n\n```typescript\nconst editor = new MediumEditor('.editable', {\n  paste: {\n    forcePlainText: false,\n    cleanPastedHTML: true,\n    cleanReplacements: [\n      [/\\s*style\\s*=\\s*[\"'][^\"']*[\"']/gi, ''], // Remove inline styles\n      [/\u003co:p\\s*\\/?\u003e|\u003c\\/o:p\u003e/gi, ''], // Remove Word tags\n      [/\u003cxml\u003e[\\s\\S]*?\u003c\\/xml\u003e/gi, ''], // Remove XML\n      [/\u003c!--[\\s\\S]*?--\u003e/g, ''] // Remove comments\n    ],\n    cleanAttrs: ['class', 'style', 'dir'],\n    cleanTags: ['meta', 'style', 'script', 'object', 'embed']\n  }\n})\n```\n\n### Event Handling\n\n```typescript\nconst editor = new MediumEditor('.editable')\n\n// Content change events\neditor.subscribe('editableInput', (event, editable) =\u003e {\n  console.log('Content changed:', editable.innerHTML)\n  // Auto-save logic here\n})\n\n// Selection change events\neditor.subscribe('editableKeyup', (event, editable) =\u003e {\n  const selection = editor.exportSelection()\n  console.log('Cursor position:', selection)\n})\n\n// Focus events\neditor.subscribe('focus', (event, editable) =\u003e {\n  console.log('Editor focused')\n})\n\neditor.subscribe('blur', (event, editable) =\u003e {\n  console.log('Editor blurred')\n})\n```\n\n### Creating Custom Extensions\n\n```typescript\nimport { MediumEditorExtension } from 'ts-medium-editor'\n\nclass EmojiExtension implements MediumEditorExtension {\n  name = 'emoji'\n  private button!: HTMLButtonElement\n  private base: any\n\n  init(): void {\n    this.button = this.createButton()\n  }\n\n  getButton(): HTMLButtonElement {\n    return this.button\n  }\n\n  private createButton(): HTMLButtonElement {\n    const button = document.createElement('button')\n    button.className = 'medium-editor-action'\n    button.innerHTML = '😀'\n    button.title = 'Insert Emoji'\n    button.addEventListener('click', this.handleClick.bind(this))\n    return button\n  }\n\n  private handleClick(): void {\n    const emoji = '🎉'\n    const selection = window.getSelection()\n\n    if (selection \u0026\u0026 selection.rangeCount \u003e 0) {\n      const range = selection.getRangeAt(0)\n      range.deleteContents()\n      range.insertNode(document.createTextNode(emoji))\n      range.collapse(false)\n      selection.removeAllRanges()\n      selection.addRange(range)\n    }\n  }\n\n  destroy(): void {\n    if (this.button) {\n      this.button.removeEventListener('click', this.handleClick)\n    }\n  }\n}\n\n// Use the extension\nconst editor = new MediumEditor('.editable', {\n  toolbar: {\n    buttons: ['bold', 'italic', 'emoji']\n  },\n  extensions: {\n    emoji: new EmojiExtension()\n  }\n})\n```\n\n### Theme Switching\n\n```typescript\nconst themeSelector = document.getElementById('theme-select') as HTMLSelectElement\nconst themeLink = document.getElementById('theme-css') as HTMLLinkElement\n\nconst themes = [\n  'default',\n  'beagle',\n  'bootstrap',\n  'flat',\n  'mani',\n  'roman',\n  'tim'\n]\n\nthemeSelector.addEventListener('change', (event) =\u003e {\n  const theme = (event.target as HTMLSelectElement).value\n  themeLink.href = `./dist/css/themes/${theme}.css`\n})\n```\n\n## Available Themes\n\nThe library includes 7 beautiful themes:\n\n- **Default** - Clean, modern design\n- **Beagle** - Friendly, rounded interface\n- **Bootstrap** - Bootstrap-compatible styling\n- **Flat** - Minimalist flat design\n- **Mani** - Elegant, sophisticated look\n- **Roman** - Classic, serif-inspired\n- **Tim** - Bold, high-contrast theme\n\n```html\n\u003c!-- Include your chosen theme --\u003e\n\u003clink rel=\"stylesheet\" href=\"dist/css/themes/default.css\"\u003e\n```\n\n## Advanced Configuration\n\n### Toolbar Positioning\n\n```typescript\n// Static toolbar (always visible)\nconst editor = new MediumEditor('.editable', {\n  toolbar: {\n    static: true,\n    sticky: true,\n    align: 'center'\n  }\n})\n\n// Relative container\nconst editor = new MediumEditor('.editable', {\n  toolbar: {\n    relativeContainer: document.getElementById('toolbar-container')\n  }\n})\n```\n\n### Custom Button Configuration\n\n```typescript\nconst editor = new MediumEditor('.editable', {\n  toolbar: {\n    buttons: [\n      'bold',\n      'italic',\n      {\n        name: 'highlight',\n        action: 'highlight',\n        aria: 'Highlight text',\n        contentDefault: 'H',\n        classList: ['custom-highlight-button'],\n        attrs: {\n          'data-action': 'highlight'\n        }\n      }\n    ]\n  }\n})\n```\n\n## Testing\n\nRun the test suite:\n\n```bash\nbun test\n```\n\n## Community\n\nFor help, discussion about best practices, or any other conversation:\n\n- 💬 [GitHub Discussions](https://github.com/stacksjs/stacks/discussions)\n- 🎮 [Discord Server](https://discord.gg/5gHFD8Uk3K)\n- 🐛 [Issue Tracker](https://github.com/stacksjs/ts-medium-editor/issues)\n\n## Postcardware\n\n“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.\n\nOur address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎\n\n## Sponsors\n\nWe would like to extend our thanks to the following sponsors for funding Stacks development:\n\n- [**JetBrains**](https://www.jetbrains.com/) - Professional development tools\n- [**The Solana Foundation**](https://solana.com/) - Blockchain infrastructure\n\n_[Become a sponsor](https://github.com/sponsors/stacksjs) and support open source development._\n\n## Credits\n\n- **[Medium](https://medium.com)** - For the beautiful editor design inspiration\n- **[medium-editor](https://github.com/yabwe/medium-editor)** - The original JavaScript implementation that inspired this TypeScript port\n- **[Chris Breuer](https://github.com/chrisbbreuer)** - Primary maintainer and TypeScript port author\n- **[All Contributors](../../contributors)** - Everyone who has contributed to making this project better\n\n## License\n\nThe MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.\n\n---\n\n\u003cp align=\"center\"\u003e\n  Made with 💙 by the \u003ca href=\"https://github.com/stacksjs\"\u003eStacks team\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/stacksjs/ts-medium-editor\"\u003e⭐ Star us on GitHub\u003c/a\u003e •\n  \u003ca href=\"https://bsky.app/profile/chris-breuer.me\"\u003e🐦 Follow on Bluesky\u003c/a\u003e •\n  \u003ca href=\"https://discord.gg/5gHFD8Uk3K\"\u003e💬 Join Discord\u003c/a\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstacksjs%2Fts-medium-editor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstacksjs%2Fts-medium-editor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstacksjs%2Fts-medium-editor/lists"}