@molecule/app-rich-text-quill

Provider bond · rich-text · App (browser) · v1.0.5 · Apache-2.0

Quill v2 rich text editor provider for molecule.dev

npm install @molecule/app-rich-text-quill

npm · Source on GitHub · Implements @molecule/app-rich-text

How it works

@molecule/app-rich-text-quill is a provider bond on the app (browser) side: it implements the rich-text core interface (@molecule/app-rich-text) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider } from '@molecule/app-rich-text'
import { createQuillProvider } from '@molecule/app-rich-text-quill'

setProvider(createQuillProvider())

Works with: @molecule/app-rich-text

Live demos

Running apps built with Molecule that install this package — open the app or study the template.

Reference

Quill v2 rich text editor provider for molecule.dev.

Implements the RichTextProvider interface using Quill v2.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider } from '@molecule/app-rich-text'
import { createQuillProvider } from '@molecule/app-rich-text-quill'

setProvider(createQuillProvider())

Type

provider

Installation

npm install @molecule/app-rich-text-quill @molecule/app-rich-text dompurify quill quill-delta

API

Interfaces

QuillOptions

Quill-specific configuration options.

interface QuillOptions extends EditorOptions {
  /**
   * Quill theme ('snow' or 'bubble').
   */
  theme?: 'snow' | 'bubble'

  /**
   * Enable/disable specific formats.
   */
  formats?: FormatType[]

  /**
   * Quill module configurations.
   */
  modules?: {
    toolbar?: boolean | string | unknown[] | Record<string, unknown>
    history?: { delay?: number; maxStack?: number; userOnly?: boolean }
    clipboard?: { matchVisual?: boolean }
    keyboard?: Record<string, unknown>
    [key: string]: unknown
  }

  /**
   * Scroll container element.
   * (Quill v1 option — ignored by the pinned Quill v2.)
   */
  scrollingContainer?: HTMLElement | string | null

  /**
   * Strict mode (limit user input to editor's capabilities).
   * (Quill v1 option — ignored by the pinned Quill v2.)
   */
  strict?: boolean

  /**
   * Debug mode.
   */
  debug?: 'error' | 'warn' | 'log' | false
}

RichTextValue

Rich text content value.

interface RichTextValue {
  /**
   * Plain text content.
   */
  text: string
  /**
   * HTML content.
   */
  html: string
  /**
   * Delta/JSON representation (if supported by the editor).
   */
  delta?: unknown
}

ToolbarConfig

Rich text editor toolbar layout (named preset with grouped format buttons).

interface ToolbarConfig {
  /**
   * Toolbar name/key.
   */
  name: string
  /**
   * Toolbar groups - each group is an array of format buttons.
   */
  groups: ToolbarGroup[]
}

SelectionRange

Selection range in the editor.

interface SelectionRange {
  /**
   * Start index.
   */
  index: number
  /**
   * Length of selection.
   */
  length: number
}

TextChangeData

Data emitted when the editor content changes.

interface TextChangeData {
  /**
   * New value.
   */
  value: RichTextValue
  /**
   * Previous value.
   */
  previousValue: RichTextValue
  /**
   * Source of the change.
   */
  source: 'user' | 'api' | 'silent'
}

SelectionChangeData

Data emitted when the editor selection changes.

interface SelectionChangeData {
  /**
   * New selection range.
   */
  range: SelectionRange | null
  /**
   * Previous selection range.
   */
  previousRange: SelectionRange | null
  /**
   * Source of the change.
   */
  source: 'user' | 'api' | 'silent'
}

RichTextEditor

Rich text editor instance with methods for content manipulation, formatting, and event handling.

interface RichTextEditor {
  /**
   * Gets the current content value.
   */
  getValue(): RichTextValue
  /**
   * Sets the content value.
   */
  setValue(value: RichTextValue): void
  /**
   * Gets the plain text content.
   */
  getText(): string
  /**
   * Gets the HTML content.
   */
  getHTML(): string
  /**
   * Gets the content length.
   */
  getLength(): number
  /**
   * Inserts text at the current cursor position.
   */
  insertText(text: string, index?: number): void
  /**
   * Inserts HTML at the current cursor position.
   */
  insertHTML(html: string, index?: number): void
  /**
   * Inserts an embed (image, video, etc.).
   */
  insertEmbed(type: string, value: unknown, index?: number): void
  /**
   * Deletes content.
   */
  deleteText(index: number, length: number): void
  /**
   * Formats text.
   */
  format(format: FormatType, value?: unknown): void
  /**
   * Formats text at a specific range.
   */
  formatText(index: number, length: number, format: FormatType, value?: unknown): void
  /**
   * Removes formatting.
   */
  removeFormat(index: number, length: number): void
  /**
   * Gets the current selection.
   */
  getSelection(): SelectionRange | null
  /**
   * Sets the selection.
   */
  setSelection(index: number, length?: number): void
  /**
   * Focuses the editor.
   */
  focus(): void
  /**
   * Blurs the editor.
   */
  blur(): void
  /**
   * Checks if the editor has focus.
   */
  hasFocus(): boolean
  /**
   * Enables the editor.
   */
  enable(): void
  /**
   * Disables the editor.
   */
  disable(): void
  /**
   * Checks if the editor is enabled.
   */
  isEnabled(): boolean
  /**
   * Subscribes to editor events.
   */
  on<T>(event: EditorEvent, handler: EditorEventHandler<T>): () => void
  /**
   * Removes event handler.
   */
  off<T>(event: EditorEvent, handler: EditorEventHandler<T>): void
  /**
   * Gets the underlying editor instance (for advanced usage).
   */
  getEditorInstance(): unknown
  /**
   * Destroys the editor.
   */
  destroy(): void
}

EditorOptions

Configuration options for creating a new rich text editor instance.

interface EditorOptions {
  /**
   * Container element.
   */
  container: HTMLElement
  /**
   * Initial value.
   */
  value?: RichTextValue
  /**
   * Placeholder text.
   */
  placeholder?: string
  /**
   * Toolbar configuration key or custom config.
   */
  toolbar?: string | ToolbarConfig
  /**
   * Read-only mode.
   */
  readOnly?: boolean
  /**
   * Custom formats to register.
   */
  formats?: FormatType[]
  /**
   * Theme name.
   */
  theme?: string
  /**
   * Additional modules/plugins configuration.
   */
  modules?: Record<string, unknown>
}

RichTextProvider

Rich text editor provider interface for creating editors and converting content formats.

interface RichTextProvider {
  /**
   * Create a new rich text editor instance attached to a container element.
   * @returns A RichTextEditor instance for controlling the editor.
   */
  createEditor(options: EditorOptions): RichTextEditor
  /**
   * Get the name of this rich text provider (e.g., 'quill', 'tiptap', 'slate').
   * @returns The provider name string.
   */
  getName(): string
  /**
   * Get the available toolbar configuration presets (e.g., 'minimal', 'standard', 'full').
   * @returns A map of preset names to their ToolbarConfig definitions.
   */
  getToolbarPresets(): Record<string, ToolbarConfig>
  /**
   * Convert an HTML string to the editor's internal rich text format.
   * @returns A RichTextValue in the provider's internal format.
   */
  htmlToValue(html: string): RichTextValue
  /**
   * Convert a plain text string to the editor's internal rich text format.
   * @returns A RichTextValue in the provider's internal format.
   */
  textToValue(text: string): RichTextValue
  /**
   * Create an empty rich text value suitable for initializing an editor.
   * @returns An empty RichTextValue.
   */
  createEmptyValue(): RichTextValue
}

Types

FormatType

Editor format types.

type FormatType =
  | 'bold'
  | 'italic'
  | 'underline'
  | 'strike'
  | 'link'
  | 'image'
  | 'video'
  | 'blockquote'
  | 'code-block'
  | 'header'
  | 'list'
  | 'indent'
  | 'align'
  | 'color'
  | 'background'
  | 'font'
  | 'size'
  | 'script'
  | 'direction'
  | 'clean'

EditorEvent

Editor event types.

type EditorEvent = 'text-change' | 'selection-change' | 'focus' | 'blur'

EditorEventHandler

Event handler for editor events.

type EditorEventHandler<T = unknown> = (data: T) => void
  • data — The data.

Classes

Quill

declare class Quill {
  static DEFAULTS: {
    bounds: null
    modules: {
      clipboard: boolean
      keyboard: boolean
      history: boolean
      uploader: boolean
    }
    placeholder: string
    readOnly: false
    registry: Parchment.Registry
    theme: string
  }
  static events: {
    readonly EDITOR_CHANGE: 'editor-change'
    readonly SCROLL_BEFORE_UPDATE: 'scroll-before-update'
    readonly SCROLL_BLOT_MOUNT: 'scroll-blot-mount'
    readonly SCROLL_BLOT_UNMOUNT: 'scroll-blot-unmount'
    readonly SCROLL_OPTIMIZE: 'scroll-optimize'
    readonly SCROLL_UPDATE: 'scroll-update'
    readonly SCROLL_EMBED_UPDATE: 'scroll-embed-update'
    readonly SELECTION_CHANGE: 'selection-change'
    readonly TEXT_CHANGE: 'text-change'
    readonly COMPOSITION_BEFORE_START: 'composition-before-start'
    readonly COMPOSITION_START: 'composition-start'
    readonly COMPOSITION_BEFORE_END: 'composition-before-end'
    readonly COMPOSITION_END: 'composition-end'
  }
  static sources: {
    readonly API: 'api'
    readonly SILENT: 'silent'
    readonly USER: 'user'
  }
  static version: string
  static imports: Record<string, unknown>
  static debug(limit: DebugLevel | boolean): void
  static find(node: Node, bubble?: boolean): Parchment.Blot | Quill | null
  static import(name: 'core/module'): typeof Module
  static import(name: `themes/${string}`): typeof Theme
  static import(name: 'parchment'): typeof Parchment
  static import(name: 'delta'): typeof Delta
  static import(name: string): unknown
  static register(
    targets: Record<
      string,
      Parchment.RegistryDefinition | Record<string, unknown> | Theme | Module | Function
    >,
    overwrite?: boolean,
  ): void
  static register(target: Parchment.RegistryDefinition, overwrite?: boolean): void
  static register(path: string, target: any, overwrite?: boolean): void
  container: HTMLElement
  root: HTMLDivElement
  scroll: Scroll
  emitter: Emitter
  protected allowReadOnlyEdits: boolean
  editor: Editor
  composition: Composition
  selection: Selection
  theme: Theme
  keyboard: Keyboard
  clipboard: Clipboard
  history: History
  uploader: Uploader
  options: ExpandedQuillOptions
  constructor(container: HTMLElement | string, options?: QuillOptions)
  addContainer(container: string, refNode?: Node | null): HTMLDivElement
  addContainer(container: HTMLElement, refNode?: Node | null): HTMLElement
  blur(): void
  deleteText(range: Range, source?: EmitterSource): Delta
  deleteText(index: number, length: number, source?: EmitterSource): Delta
  disable(): void
  editReadOnly<T>(modifier: () => T): T
  enable(enabled?: boolean): void
  focus(options?: { preventScroll?: boolean }): void
  format(name: string, value: unknown, source?: EmitterSource): Delta
  formatLine(
    index: number,
    length: number,
    formats: Record<string, unknown>,
    source?: EmitterSource,
  ): Delta
  formatLine(
    index: number,
    length: number,
    name: string,
    value?: unknown,
    source?: EmitterSource,
  ): Delta
  formatText(range: Range, name: string, value: unknown, source?: EmitterSource): Delta
  formatText(
    index: number,
    length: number,
    name: string,
    value: unknown,
    source?: EmitterSource,
  ): Delta
  formatText(
    index: number,
    length: number,
    formats: Record<string, unknown>,
    source?: EmitterSource,
  ): Delta
  getBounds(index: number | Range, length?: number): Bounds | null
  getContents(index?: number, length?: number): Delta
  getFormat(
    index?: number,
    length?: number,
  ): {
    [format: string]: unknown
  }
  getFormat(range?: Range): {
    [format: string]: unknown
  }
  getIndex(blot: Parchment.Blot): number
  getLength(): number
  getLeaf(index: number): [Parchment.LeafBlot | null, number]
  getLine(index: number): [Block | BlockEmbed | null, number]
  getLines(range: Range): (Block | BlockEmbed)[]
  getLines(index?: number, length?: number): (Block | BlockEmbed)[]
  getModule(name: string): unknown
  getSelection(focus: true): Range
  getSelection(focus?: boolean): Range | null
  getSemanticHTML(range: Range): string
  getSemanticHTML(index?: number, length?: number): string
  getText(range?: Range): string
  getText(index?: number, length?: number): string
  hasFocus(): boolean
  insertEmbed(index: number, embed: string, value: unknown, source?: EmitterSource): Delta
  insertText(index: number, text: string, source?: EmitterSource): Delta
  insertText(
    index: number,
    text: string,
    formats: Record<string, unknown>,
    source?: EmitterSource,
  ): Delta
  insertText(
    index: number,
    text: string,
    name: string,
    value: unknown,
    source?: EmitterSource,
  ): Delta
  isEnabled(): boolean
  off(...args: Parameters<(typeof Emitter)['prototype']['off']>): Emitter
  on(
    event: (typeof Emitter)['events']['TEXT_CHANGE'],
    handler: (delta: Delta, oldContent: Delta, source: EmitterSource) => void,
  ): Emitter
  on(
    event: (typeof Emitter)['events']['SELECTION_CHANGE'],
    handler: (range: Range, oldRange: Range, source: EmitterSource) => void,
  ): Emitter
  on(
    event: (typeof Emitter)['events']['EDITOR_CHANGE'],
    handler: (
      ...args:
        | [(typeof Emitter)['events']['TEXT_CHANGE'], Delta, Delta, EmitterSource]
        | [(typeof Emitter)['events']['SELECTION_CHANGE'], Range, Range, EmitterSource]
    ) => void,
  ): Emitter
  on(event: string, ...args: unknown[]): Emitter
  once(...args: Parameters<(typeof Emitter)['prototype']['once']>): Emitter
  removeFormat(index: number, length: number, source?: EmitterSource): Delta
  scrollRectIntoView(rect: Rect): void
  /**
   * @deprecated Use Quill#scrollSelectionIntoView() instead.
   */
  scrollIntoView(): void
  /**
   * Scroll the current selection into the visible area.
   * If the selection is already visible, no scrolling will occur.
   */
  scrollSelectionIntoView(): void
  setContents(delta: Delta | Op[], source?: EmitterSource): Delta
  setSelection(range: Range | null, source?: EmitterSource): void
  setSelection(index: number, source?: EmitterSource): void
  setSelection(index: number, length?: number, source?: EmitterSource): void
  setSelection(index: number, source?: EmitterSource): void
  setText(text: string, source?: EmitterSource): Delta
  update(source?: EmitterSource): void
  updateContents(delta: Delta | Op[], source?: EmitterSource): Delta
}

Functions

createQuillEditor(quill, container)

Wraps a Quill instance in a molecule RichTextEditor interface. Sets up event listeners for text changes, selection changes, focus, and blur, and exposes a unified API for content manipulation, formatting, and selection management.

function createQuillEditor(quill: Quill, container: HTMLElement): RichTextEditor
  • quill — The initialized Quill editor instance.
  • container — The DOM element containing the Quill editor (used for cleanup on destroy).

Returns: A RichTextEditor with getValue/setValue, formatting, selection, and event methods.

createQuillProvider(defaultOptions)

Creates a Quill-based rich text provider implementing the molecule RichTextProvider interface. Supports toolbar presets (minimal, standard, full), HTML/text/delta value conversion, and Quill themes (snow, bubble).

function createQuillProvider(defaultOptions?: Partial<QuillOptions>): RichTextProvider
  • defaultOptions — Default Quill options applied to every editor created by this provider.

Returns: A RichTextProvider backed by Quill.

toolbarConfigToQuill(config)

Converts a molecule ToolbarConfig to Quill's native toolbar array format. Each group becomes a sub-array; string items pass through, objects become { type: options } entries.

function toolbarConfigToQuill(config: ToolbarConfig): unknown[][]
  • config — A molecule toolbar config with named groups of toolbar items.

Returns: A nested array in Quill's toolbar module format.

Constants

provider

Default Quill rich text provider instance.

const provider: RichTextProvider

quillToolbars

Default toolbar presets for Quill.

const quillToolbars: Record<string, ToolbarConfig>

Core Interface

Implements @molecule/app-rich-text interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/app-rich-text'
import { provider } from '@molecule/app-rich-text-quill'

export function setupRichTextQuill(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-rich-text ^1.0.2

Runtime Dependencies

  • @molecule/app-rich-text

  • dompurify

  • quill

  • quill-delta

  • Import Quill's theme stylesheet yourself — this package does not: import 'quill/dist/quill.snow.css' (or quill.bubble.css for theme: 'bubble'). Without it the editor renders unstyled and the toolbar is a broken list.

  • Call setProvider(...) explicitly. If you forget, @molecule/app-rich-text silently falls back to its built-in contentEditable provider — the app appears to work but without Quill's toolbar presets, delta support, or theming. No error is thrown.

  • Browser-only. createEditor({ container }) requires a mounted DOM element and htmlToValue() touches document — in SSR frameworks construct editors in a client-only effect.

  • htmlToValue() returns delta: undefined (delta conversion needs a live editor); initialize editors from value.html or convert via an editor instance.

  • Untrusted HTML is sanitized on the way IN (DOMPurify in htmlToValue, Quill's clipboard allowlist for stored HTML) — but editor.getHTML() is raw editor DOM: still sanitize server-side before persisting/re-serving user content.

  • Toolbar: pass a preset name ('minimal' | 'standard' | 'full'), a custom ToolbarConfig, or toolbar: false; presets live in quillToolbars.

  • Custom modules merge with the toolbar preset. A modules object you pass (history/clipboard/keyboard/…) is merged UNDER the derived toolbar, so it never drops the toolbar; set modules.toolbar only when you want to override the preset.

  • editor.destroy() fully tears down — it also removes the snow theme's sibling .ql-toolbar element, so re-mounting on the same node won't stack duplicate toolbars.