import { PropertyValues, TemplateResult } from 'lit'; import { Editor, JSONContent } from '@tiptap/core'; import { LuxenFormAssociatedElement } from '../../shared/luxen-form-associated-element.js'; /** Fired when the editor content changes. Bubbles; not composed. */ export declare class EditorChangeEvent extends Event { readonly html: string; readonly json: JSONContent; constructor(htmlValue: string, json: JSONContent); } /** Fired when the attachment toolbar button is clicked. */ export declare class AddFileEvent extends Event { constructor(); } interface ProseEditorEventMap { change: EditorChangeEvent; } declare global { interface GlobalEventHandlersEventMap { 'add-file': AddFileEvent; } } type ToolbarCommandName = 'heading-1' | 'heading-2' | 'heading-3' | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'bulletlist' | 'orderedlist' | 'blockquote' | 'code-block' | 'horizontal-rule' | 'link' | 'emoji' | 'attachment' | 'undo' | 'redo' | 'divider'; /** * @summary A rich text editor built on Tiptap (ProseMirror). Form-associated: its value is the editor HTML. * * ### Keyboard — APG Toolbar pattern * The generated toolbar buttons follow the * [APG Toolbar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/): * exactly one button holds `tabindex="0"` at a time (roving tabindex). * **ArrowRight** / **ArrowLeft** move focus to the next / previous button, * wrapping around. **Home** / **End** jump to the first / last button. * A single Tab enters and leaves the toolbar without stepping through every * button. Slotted `toolbar-start` / `toolbar-end` focusables are excluded * from roving management — they are consumer-controlled content and their * tab order is handled by the consumer. * * @customElement l-prose-editor * * @slot toolbar-start - Content placed before the generated toolbar buttons. * @slot toolbar-end - Content placed after the generated toolbar buttons. * * @event change - Fired when the content changes. Bubbles. Properties: `html: string`, `json: JSONContent`. * @event add-file - Fired when the attachment toolbar button is clicked. * * @csspart wrapper - The editor frame wrapping the toolbar and content. * @csspart toolbar - The toolbar row. * @csspart toolbar-button - Any toolbar button. * @csspart divider - A toolbar divider. * @csspart editor - The container around the editable content. * * @cssproperty --border-color - Color of the editor frame border. * @cssproperty --border-width - Width of the editor frame border. * @cssproperty --border-radius - Corner radius of the editor frame. * @cssproperty --background - Background color of the editor. * @cssproperty --color - Text color of the editor. * @cssproperty --toolbar-background - Background color of the toolbar. * @cssproperty --toolbar-padding - Padding around the toolbar. * @cssproperty --toolbar-gap - Gap between toolbar buttons. * @cssproperty --toolbar-divider-color - Color of toolbar dividers. * @cssproperty --toolbar-button-size - Size of toolbar buttons. * @cssproperty --toolbar-button-radius - Corner radius of toolbar buttons. * @cssproperty --toolbar-button-color - Icon color of inactive toolbar buttons. * @cssproperty --toolbar-button-color-active - Icon color of hovered/active toolbar buttons. * @cssproperty --toolbar-button-background-hover - Background of hovered toolbar buttons. * @cssproperty --toolbar-button-background-active - Background of active toolbar buttons. * @cssproperty --content-padding - Padding inside the editable content region. Default `0.75rem 1rem`. * @cssproperty --content-min-height - Minimum height of the editable content region. Default `8rem`. * @cssproperty --placeholder-color - Placeholder text color. */ export declare class ProseEditor extends LuxenFormAssociatedElement { static styles: import('lit').CSSResult[]; private _localize; /** The Tiptap editor instance. Available after the first render. */ editor: Editor; /** Initial HTML content. */ accessor initialHtml: string; /** Initial content as a serialized ProseMirror JSON string. */ accessor initialJson: string; /** Class applied to the `.ProseMirror` editable element (e.g. for Tailwind Typography `prose`). */ accessor editorClass: string; /** Explicit list of toolbar commands. Overrides `toolbar-preset` when set. */ accessor toolbar: ToolbarCommandName[]; /** Built-in toolbar layout used when `toolbar` is not set. */ accessor toolbarPreset: 'default' | 'minimal'; /** Where the toolbar sits relative to the content. */ accessor toolbarPlacement: 'top' | 'bottom'; /** Focus the editor on creation. */ accessor autofocus: boolean; /** Placeholder shown when the editor is empty. */ accessor placeholder: string; /** * URL the emoji picker fetches its data from. Point this at a locally served * `emojibase-data` JSON to run fully offline (no CDN). Defaults to the * picker's bundled CDN source. */ accessor emojiDataSource: string; private _editorRoot?; private _emojiPicker?; private _emojiPickerPromise?; private _emojiOpenAtPointerDown; private _emojiAutoUpdateCleanup?; /** * Index (within the generated button list) that currently holds tabindex="0". * Initialised to 0 (first button). Survives Lit re-renders because tabindex * is computed from this field inside `_renderButton`; no post-hoc DOM patching * needed (which re-render would overwrite). */ private _rovingIndex; /** * Set to `true` while the toolbar is driving focus via arrow-key navigation. * `_onFocus` checks this flag and skips its redirect-to-editor logic so that * focus stays on the toolbar button the user navigated to. */ private _rovingFocusActive; get validationTarget(): HTMLElement | undefined; private get _toolbar(); firstUpdated(): void; private _initEditor; updated(changed: PropertyValues): void; disconnectedCallback(): void; /** Get the current content as an HTML string. Empty paragraph resolves to `''`. */ getHTML(): string; /** Get the current content as ProseMirror JSON. */ getJSON(): JSONContent; /** Remove all content. */ clear(): void; focus(): void; blur(): void; toggleBold(): void; toggleItalic(): void; toggleUnderline(): void; toggleStrike(): void; toggleHighlight(): void; toggleHeading(level: 1 | 2 | 3): void; toggleBulletList(): void; toggleOrderedList(): void; toggleBlockquote(): void; toggleCodeBlock(): void; setHorizontalRule(): void; undo(): void; redo(): void; toggleLink(): void; formResetCallback(): void; /** Sync the form value and validity from the current content. Returns the HTML. */ private _syncValue; private _emitChange; private _initialContent; /** * The editable element is created in light DOM (slotted), not the shadow root. * Firefox and WebKit have long-standing bugs with `contenteditable` carets and * DOM selections inside a shadow tree, so ProseMirror must live outside it. * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1496769 * @see https://bugs.webkit.org/show_bug.cgi?id=163921 */ private _createEditorRoot; private _onFocus; /** * Delegated click handler on the toolbar container — updates the roving * tabindex owner when a generated button is clicked so that subsequent * Tab-in / Tab-out cycles return to the last-interacted button. */ private _onToolbarClick; /** * APG Toolbar keyboard handler — roving tabindex navigation. * Targets only the generated [part="toolbar-button"] set; slotted * toolbar-start / toolbar-end focusables are left to the consumer. */ private _onToolbarKeyDown; private _onKeyDown; /** * Capture-phase outside-click dismissal — the robust fallback the platform's * native light-dismiss can't be trusted to provide (see `firstUpdated`). * Dismiss when the picker is open and the pointer landed on neither the * picker nor the emoji toolbar button (re-clicking the button is handled by * the toggle in `_onEmojiButtonClick`). `composedPath()` crosses shadow * boundaries, so a click inside the picker's shadow tree still resolves to * the host element here. */ private _onDocumentPointerDown; /** * The picker is a `popover="auto"` parented into the top layer; outside-click * dismissal is owned by `_onDocumentPointerDown` (capture phase), which * survives `stopPropagation()`/`preventDefault()` from an ancestor (modal * ``, ProseMirror, Vue delegation). (`Escape` is handled by * `_onKeyDown` instead — ProseMirror `preventDefault()`s it, which would * cancel the native close.) * * We can't wire the button as a native popover invoker (`popoverTargetElement` * / `popovertarget`) because the picker is parented into another shadow tree * (the open `` — see `_topLayerContainer`) while the button lives in * this element's shadow root; a cross-tree invoker reference doesn't resolve. * So the toggle is hand-rolled here. The catch: a pointer click on the button * is "outside" the popover, so native light-dismiss (where it works) has * already closed an open picker by the time this `click` fires (light-dismiss * runs on `pointerup`). We therefore read the open state captured at * `pointerdown` to know whether this click should re-open or stay closed. * Keyboard activation (`detail === 0`) has no pointer light-dismiss, so it * reads the live state and toggles directly. */ private _onEmojiButtonPointerDown; private _onEmojiButtonClick; private _ensureEmojiPicker; /** * The element the emoji picker is appended to. Inside a modal `` the * picker must live within the dialog's flat-tree subtree, otherwise the modal's * inertness makes it unclickable. Walk the flat tree (crossing shadow * boundaries via `assignedSlot`) to the nearest open ``; fall back to * `` when the editor is not inside a modal. */ private _topLayerContainer; private get _emojiButton(); /** * Keep the picker pinned to the emoji button while it is open. Floating UI's * `autoUpdate` re-runs `_positionEmojiPicker` on scroll (including ancestor * scroll), resize, and layout shifts, so the picker stays anchored to the * button — the cross-tree equivalent of CSS anchor positioning, which can't be * used here because the button lives in this element's shadow root while the * picker is parented to the top-layer container (a different tree scope). */ private _startEmojiAutoUpdate; private _stopEmojiAutoUpdate; private _positionEmojiPicker; private _renderButton; private _renderToolbarItem; render(): TemplateResult; } export interface ProseEditor { addEventListener(type: K, listener: (this: ProseEditor, ev: ProseEditorEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: K, listener: (this: ProseEditor, ev: ProseEditorEventMap[K]) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } export {}; //# sourceMappingURL=prose-editor.d.ts.map