{"id":40773960,"url":"https://github.com/watzak/editable.ts","last_synced_at":"2026-04-19T23:10:36.353Z","repository":{"id":332954936,"uuid":"1135604580","full_name":"watzak/editable.ts","owner":"watzak","description":"Friendly contenteditable API in typescript","archived":false,"fork":false,"pushed_at":"2026-01-16T13:30:23.000Z","size":311,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-17T03:55:26.695Z","etag":null,"topics":["contenteditable","text-editing","typescript"],"latest_commit_sha":null,"homepage":"https://watzak.github.io/editable.ts/examples","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/watzak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-16T10:32:50.000Z","updated_at":"2026-01-16T13:30:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/watzak/editable.ts","commit_stats":null,"previous_names":["watzak/editable.ts"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/watzak/editable.ts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/watzak%2Feditable.ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/watzak%2Feditable.ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/watzak%2Feditable.ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/watzak%2Feditable.ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/watzak","download_url":"https://codeload.github.com/watzak/editable.ts/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/watzak%2Feditable.ts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28640113,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-21T18:04:35.752Z","status":"ssl_error","status_checked_at":"2026-01-21T18:03:55.054Z","response_time":86,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["contenteditable","text-editing","typescript"],"created_at":"2026-01-21T19:01:07.976Z","updated_at":"2026-04-19T23:10:36.340Z","avatar_url":"https://github.com/watzak.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# editable.ts\n\nA TypeScript library that provides a friendly and browser-consistent API for `contenteditable` elements. Built for block-level rich text editing with a clean, event-driven architecture. It started as a fork of [https://github.com/livingdocsIO/editable.js](https://github.com/livingdocsIO/editable.js) and has since been modernized around TypeScript, Vitest, Vite, and typed internal APIs.\n\n## Summary\n\n**editable.ts** is a modern TypeScript rewrite of editable.js, offering a robust abstraction layer over the browser's native `contenteditable` API. It handles cross-browser inconsistencies, provides a typed event system, and enables building rich text editors with minimal boilerplate.\n\n### Key Features\n\n- **Cross-browser compatibility** - Abstracts away browser differences in Selection and Range APIs\n- **Event-driven architecture** - Clean pub/sub system for handling user interactions\n- **Block-based editing** - Optimized for block-level elements (paragraphs, headings, blockquotes)\n- **Selection \u0026 Cursor management** - Powerful APIs for manipulating text selections and cursor positions\n- **Highlighting system** - Built-in support for text highlighting, spellcheck, and custom markers\n- **Default behaviors** - Sensible defaults for common operations (split, merge, insert blocks)\n- **TypeScript support** - Full type definitions and modern TypeScript implementation\n- **Extensible** - Easy to customize and extend with custom event handlers\n\n### Use Cases\n\n- Rich text editors\n- Content management systems\n- Comment and annotation systems\n- Collaborative editing interfaces\n- Inline editing components\n\nCheck out the [original editable.js live demo](https://livingdocsio.github.io/editable.js/) for a reference implementation (note: this is the JavaScript version, not this TypeScript fork).\n\n## What is it about?\n\nA typed API that defines a friendly and browser-consistent content editable interface.\n\nEditable is built for block level elements containing only phrasing content. This normally means `p`, `h1`-`h6`, `blockquote` etc. elements. This allows editable to be lean and mean since it is only concerned with formatting and not with layouting.\n\nWe made editable.ts to support our vision of online document editing. Have a look at [livingdocs.io](http://livingdocs.io/).\n\n## Architecture Overview\n\neditable.ts follows a layered architecture that separates concerns and provides clear extension points.\n\n### High-Level Architecture\n\n```mermaid\ngraph TB\n    subgraph PublicAPI[\"Public API Layer\"]\n        Editable[Editable Class]\n    end\n    \n    subgraph EventSystem[\"Event System Layer\"]\n        Dispatcher[Dispatcher]\n        Eventable[Eventable Mixin]\n        SelectionWatcher[SelectionWatcher]\n        Keyboard[Keyboard Handler]\n    end\n    \n    subgraph CoreComponents[\"Core Components\"]\n        Block[Block Management]\n        Content[Content Management]\n        Parser[Parser]\n        Clipboard[Clipboard Handler]\n    end\n    \n    subgraph SelectionSystem[\"Selection \u0026 Cursor\"]\n        Cursor[Cursor]\n        Selection[Selection]\n        RangeContainer[Range Container]\n    end\n    \n    subgraph Highlighting[\"Highlighting System\"]\n        HighlightSupport[Highlight Support]\n        MonitoredHighlighting[Monitored Highlighting]\n        Plugins[Highlighting Plugins]\n    end\n    \n    subgraph DOMAbstraction[\"DOM Abstraction Layer\"]\n        DOMUtils[DOM Utilities]\n        ElementUtils[Element Utilities]\n        StringUtils[String Utilities]\n    end\n    \n    Editable --\u003e Dispatcher\n    Editable --\u003e Block\n    Editable --\u003e Content\n    Editable --\u003e HighlightSupport\n    \n    Dispatcher --\u003e Eventable\n    Dispatcher --\u003e SelectionWatcher\n    Dispatcher --\u003e Keyboard\n    \n    SelectionWatcher --\u003e Cursor\n    SelectionWatcher --\u003e Selection\n    SelectionWatcher --\u003e RangeContainer\n    \n    Cursor --\u003e Content\n    Cursor --\u003e Parser\n    Selection --\u003e Cursor\n    \n    HighlightSupport --\u003e MonitoredHighlighting\n    MonitoredHighlighting --\u003e Plugins\n    \n    Content --\u003e Parser\n    Content --\u003e DOMUtils\n    Parser --\u003e ElementUtils\n    Block --\u003e DOMUtils\n```\n\n### Core Components\n\n#### 1. Editable Class (`core.ts`)\n\nThe main entry point and public API. Provides a clean, chainable interface for all operations.\n\n**Key Responsibilities:**\n- Exposes the public API for end users\n- Manages instance-specific configuration\n- Delegates to specialized modules\n- Provides cursor/selection creation utilities\n- Handles highlighting operations\n\n**Key Methods:**\n- `add()` / `remove()` - Enable/disable editable functionality\n- `enable()` / `disable()` - Control editable state\n- `on()` / `off()` - Event subscription\n- `getSelection()` - Get current selection/cursor\n- `highlight()` - Text highlighting functionality\n- `getContent()` - Extract clean content\n\n#### 2. Dispatcher (`dispatcher.ts`)\n\nCentral event coordination hub that bridges native DOM events to the internal event system.\n\n**Event Flow:**\n```\nNative DOM Event\n    ↓\nDispatcher (setupDocumentListener)\n    ↓\nEvent Handler (filter by editable block)\n    ↓\nSelectionWatcher (get current selection/cursor)\n    ↓\nDispatcher.notify() (emit internal event)\n    ↓\nEvent Handlers (user-defined callbacks)\n```\n\n#### 3. Event System (`eventable.ts`)\n\nLightweight publish/subscribe mixin implementing the Observer pattern.\n\n**API:**\n- `on(event, handler)` - Subscribe to events\n- `off(event, handler)` - Unsubscribe from events\n- `notify(event, ...args)` - Publish events\n\n#### 4. Selection \u0026 Cursor System\n\n**SelectionWatcher** - Monitors browser Selection API and converts to internal Cursor/Selection objects\n\n**Cursor** - Represents a collapsed selection (cursor position) with capabilities for:\n- Position querying (beginning, end, line detection)\n- Content insertion/manipulation\n- Tag detection (bold, italic, links, etc.)\n- Coordinate calculations\n\n**Selection** - Extends Cursor, represents a non-collapsed selection with additional capabilities:\n- Text/HTML extraction\n- Selection wrapping (links, formatting)\n- Range validation\n- Multiple rect support\n\n#### 5. Block Management (`block.ts`)\n\nManages the lifecycle and state of individual editable block elements.\n\n#### 6. Content Management (`content.ts`)\n\nHandles all content manipulation, extraction, and normalization:\n- HTML normalization\n- Content extraction (removes internal markers)\n- Fragment creation\n- Tag wrapping/unwrapping\n\n#### 7. Highlighting System\n\nComprehensive highlighting support including:\n- Spellcheck integration\n- Text search highlighting\n- Range-based highlighting\n- Highlight persistence during editing\n- Custom highlight types\n- Text diff overlays for inserted and deleted content\n\n### TypeScript Notes\n\nThe current codebase uses TypeScript types as architectural boundaries rather than just annotations:\n\n- `src/event-types.ts` centralizes public and internal event payloads\n- `src/plugin-types.ts` defines configuration contracts for highlighting, spellcheck, and text diff\n- `src/dom-compat.ts` isolates legacy DOM/jQuery-like compatibility helpers\n\nThis keeps browser-facing code flexible while making the main editing pipeline easier to evolve safely.\n\n### Data Flow Examples\n\n#### User Types Enter Key\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant Browser\n    participant Dispatcher\n    participant Keyboard\n    participant SelectionWatcher\n    participant DefaultBehavior\n    \n    User-\u003e\u003eBrowser: Presses Enter\n    Browser-\u003e\u003eDispatcher: keydown event\n    Dispatcher-\u003e\u003eKeyboard: dispatchKeyEvent()\n    Keyboard-\u003e\u003eDispatcher: 'enter' event\n    Dispatcher-\u003e\u003eSelectionWatcher: getFreshRange()\n    SelectionWatcher--\u003e\u003eDispatcher: Cursor object\n    Dispatcher-\u003e\u003eDefaultBehavior: notify('split'/'insert')\n    DefaultBehavior-\u003e\u003eBrowser: DOM updated\n    Browser--\u003e\u003eUser: Cursor positioned\n```\n\n#### User Selects Text\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant Browser\n    participant Dispatcher\n    participant SelectionWatcher\n    \n    User-\u003e\u003eBrowser: Selects text\n    Browser-\u003e\u003eDispatcher: selectionchange event\n    Dispatcher-\u003e\u003eSelectionWatcher: selectionChanged()\n    SelectionWatcher-\u003e\u003eSelectionWatcher: getFreshSelection()\n    SelectionWatcher--\u003e\u003eDispatcher: Selection object\n    Dispatcher-\u003e\u003eDispatcher: notify('selection')\n    Dispatcher--\u003e\u003eUser: User handlers execute\n```\n\nFor a detailed technical deep-dive, see [ARCHITECTURE.md](docs/ARCHITECTURE.md).\n\n## Installation\n\nVia npm:\n\n```shell\nnpm install --save editable.ts\n```\n\nYou can either `import` the module or find a prebuilt file in the npm bundle `dist/editable.umd.cjs`.\n\n```typescript\nimport { Editable } from 'editable.ts'\n```\n\n### Dateigröße (Bundle)\n\nDie npm-Paketinhalte umfassen das ESM-Build unter `lib/` und optional das vorgebaute UMD-Bundle unter `dist/`.\n\n| Artefakt | Größe (ca.) | Hinweis |\n| -------- | ------------- | ------- |\n| `dist/editable.umd.cjs` | ~67 KB (~20 KB gzip) | Einzeldatei für `\u003cscript\u003e` / Legacy-Bundler; Werte nach `npm run build` |\n| `lib/core.js` (ESM-Einstieg) | ~11 KB (~3 KB gzip) | Einstiegsmodul; der Rest liegt in weiteren Modulen unter `lib/` |\n| `lib/` (gesamt, ungepackt) | ~1 MB | Alle `.js`- und `.d.ts`-Dateien; Bundler tree-shaken typischerweise nur genutzte Teile |\n\nDie exakten Byte-Werte ändern sich mit der Version. Nach einem Build kannst du sie lokal mit `ls -la dist/ lib/core.js` bzw. `gzip -c dist/editable.umd.cjs | wc -c` prüfen.\n\n## Quick Start\n\n### Basic Usage\n\nTo make an element editable:\n\n```typescript\nimport { Editable } from 'editable.ts'\n\n// Create an instance\nconst editable = new Editable()\n\n// Make an element editable\nconst element = document.querySelector('.my-editable')\neditable.add(element)\n```\n\n### TypeScript Example\n\n```typescript\nimport { Editable } from 'editable.ts'\n\nconst editable = new Editable({\n  defaultBehavior: true,\n  browserSpellcheck: true,\n  smartQuotes: true,\n  quotes: ['“', '”'],\n  singleQuotes: ['‘', '’']\n})\n\n// Add editable functionality to elements\neditable.add('.editable-block')\n```\n\n## Examples\n\nThe interactive demo is published at [GitHub Pages](https://watzak.github.io/editable.ts/examples/). **Privacy:** that page includes a Matomo image tracker (requests to `matomo.kamod.ch`) for anonymous usage statistics. The library code published to npm does not contain analytics or tracking.\n\n### Selection Changes with Toolbar\n\nIn a `selection` event you get the editable element that triggered the event as well as a selection object. Through the selection object you can get information about the selection like coordinates or the text it contains and you can manipulate the selection.\n\nIn the following example we show a toolbar on top of the selection whenever the user has selected something inside of an editable element.\n\n```typescript\neditable.on('selection', (editableElement: HTMLElement, selection: Selection | null) =\u003e {\n  if (!selection) {\n    toolbar.hide()\n    return\n  }\n\n  // Get coordinates relative to the document (suited for absolutely positioned elements)\n  const coords = selection.getCoordinates()\n\n  // Position toolbar\n  const top = coords.top - toolbar.outerHeight()\n  const left = coords.left + (coords.width / 2) - (toolbar.outerWidth() / 2)\n  toolbar.css({top, left}).show()\n})\n```\n\n### Cursor Manipulation\n\nCreate and manipulate cursors programmatically:\n\n```typescript\n// Get current cursor/selection\nconst cursor = editable.getSelection()\n\nif (cursor \u0026\u0026 cursor.isCursor) {\n  // Check if cursor is at beginning of block\n  if (cursor.isAtBeginning()) {\n    console.log('Cursor is at the beginning')\n  }\n\n  // Insert text at cursor position\n  cursor.insert('Hello, World!')\n\n  // Create cursor at specific position\n  const newCursor = editable.createCursor(element, 'end')\n  newCursor?.insertAfter('\u003cstrong\u003eBold text\u003c/strong\u003e')\n}\n```\n\n### Content Extraction\n\nExtract clean content from editable elements:\n\n```typescript\n// Get clean HTML content (removes internal markers)\nconst content = editable.getContent(element)\nconsole.log(content) // Clean HTML string\n\n// Get selection text\nconst selection = editable.getSelection(element)\nif (selection \u0026\u0026 selection.isSelection) {\n  const selectedText = selection.text()\n  const selectedHtml = selection.html()\n  console.log('Selected text:', selectedText)\n  console.log('Selected HTML:', selectedHtml)\n}\n```\n\n### Event Handling\n\nHandle multiple events with a clean API:\n\n```typescript\n// Handle focus events\neditable.on('focus', (element: HTMLElement) =\u003e {\n  console.log('Element focused:', element)\n})\n\n// Handle content changes\neditable.on('change', (element: HTMLElement) =\u003e {\n  console.log('Content changed in:', element)\n  // Auto-save, validation, etc.\n})\n\n// Handle block splits (Enter key in middle of block)\neditable.on('split', (element: HTMLElement, before: string, after: string, cursor: Cursor) =\u003e {\n  console.log('Block split:', { before, after, cursor })\n  // Custom split behavior\n})\n\n// Handle block merges (Backspace/Delete at boundaries)\neditable.on('merge', (element: HTMLElement, direction: 'before' | 'after', cursor: Cursor) =\u003e {\n  console.log('Blocks merged:', { direction, cursor })\n  // Custom merge behavior\n})\n```\n\n### Highlighting\n\nAdd text highlighting and spellcheck:\n\n```typescript\n// Highlight specific text\nconst startIndex = editable.highlight({\n  editableHost: element,\n  text: 'search term',\n  highlightId: 'search-1',\n  type: 'search'\n})\n\n// Highlight specific range\neditable.highlight({\n  editableHost: element,\n  text: 'important',\n  highlightId: 'important-1',\n  textRange: { start: 10, end: 18 },\n  type: 'comment'\n})\n\n// Setup spellcheck\neditable.setupSpellcheck({\n  throttle: 300,\n  spellcheckService: (text: string, callback) =\u003e {\n    // Your spellcheck service\n    callback(checkSpelling(text))\n  }\n})\n\n// Setup text diff markers\neditable.setupTextDiff({\n  checkOnInit: true,\n  throttle: 0\n})\n\n// Remove highlight\neditable.removeHighlight({\n  editableHost: element,\n  highlightId: 'search-1'\n})\n```\n\n### Custom Event Handlers\n\nOverride default behaviors:\n\n```typescript\n// Disable default behavior and implement custom\nconst editable = new Editable({\n  defaultBehavior: false\n})\n\n// Custom Enter handling via semantic events\neditable.on('insert', (element: HTMLElement, direction, cursor) =\u003e {\n  console.log('Insert requested:', direction)\n  insertCustomBlock(element, direction, cursor)\n})\n```\n\n## Events Overview\n\neditable.ts emits a comprehensive set of events for all user interactions:\n\n### Core Events\n\n- **focus**  \n  Fired when an editable element gets focus.\n\n- **blur**  \n  Fired when an editable element loses focus.\n\n- **selection**  \n  Fired when the user selects some text inside an editable element.\n\n- **cursor**  \n  Fired when the cursor position changes.\n\n- **change**  \n  Fired when the user has made a change.\n\n### Content Modification Events\n\n- **insert**  \n  Fired when the user presses `ENTER` at the beginning or end of an editable (For example you can insert a new paragraph after the element if this happens).\n\n- **split**  \n  Fired when the user presses `ENTER` in the middle of an element.\n\n- **merge**  \n  Fired when the user pressed `FORWARD DELETE` at the end or `BACKSPACE` at the beginning of an element.\n\n- **newline**  \n  Fired when the user presses `SHIFT+ENTER` to insert a newline.\n\n- **switch**  \n  Fired when the user pressed an `ARROW KEY` at the top or bottom so that you may want to set the cursor into the preceding or following element.\n\n### Clipboard Events\n\n- **clipboard**  \n  Fired for `copy`, `cut` and `paste` events.\n\n- **paste**  \n  Fired specifically on paste operations.\n\n### Highlighting Events\n\n- **spellcheckUpdated**  \n  Fired when the spellcheckService has updated the spellcheck highlights.\n\n## API Reference\n\nFor detailed API documentation, see the source files:\n\n- **[core.ts](src/core.ts)** - Main Editable class and public API\n- **[cursor.ts](src/cursor.ts)** - Cursor manipulation API\n- **[selection.ts](src/selection.ts)** - Selection manipulation API\n- **[dispatcher.ts](src/dispatcher.ts)** - Event system internals\n- **[create-default-behavior.ts](src/create-default-behavior.ts)** - Default behavior implementation\n\n### Type Definitions\n\n```typescript\ninterface EditableConfig {\n  window?: Window\n  defaultBehavior?: boolean\n  mouseMoveSelectionChanges?: boolean\n  browserSpellcheck?: boolean\n  smartQuotes?: boolean\n  quotes?: string[]\n  singleQuotes?: string[]\n}\n\ninterface HighlightOptions {\n  editableHost: HTMLElement\n  text: string\n  highlightId: string\n  textRange?: { start: number; end: number }\n  raiseEvents?: boolean\n  type?: string\n}\n\ninterface TextDiffOptions {\n  enabled?: boolean\n  checkOnInit?: boolean\n  checkOnFocus?: boolean\n  markerDeleted?: string\n  markerInserted?: string\n  throttle?: number\n}\n```\n\n## Development\n\n### Setup\n\n```bash\n# Install node dependencies\nnpm install\n```\n\n### Development Tasks\n\n```bash\n# Development server with demo app (Vite dev server)\nnpm start\n\n# Run tests (Vitest)\nnpm test\n\n# Run tests in watch mode\nnpm run test:watch\n\n# Run tests with coverage\nnpm run test:coverage\n\n# Run tests with interactive UI\nnpm run test:ui\n\n# TypeScript/JavaScript linting\nnpm run lint\n\n# Build editable.ts (TypeScript → lib/, then bundle → dist/)\nnpm run build\n\n# Build TypeScript only\nnpm run build:ts\n\n# Build library bundle only\nnpm run build:dist\n\n# Build examples only\nnpm run build:docs\n```\n\n### Requirements\n\n- Node.js \u003e= 22\n- npm \u003e= 11\n\n## License\n\neditable.ts is licensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwatzak%2Feditable.ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwatzak%2Feditable.ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwatzak%2Feditable.ts/lists"}