@bikdotai/bik-component-library 0.0.811-beta.2 → 0.0.811
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/feature-announcements/hooks/useFeatureAnnouncements.js +1 -1
- package/dist/cjs/components/feature-announcements/hooks/useFeatureAnnouncements.js.map +1 -1
- package/dist/cjs/editor/BikEditor.js +1 -1
- package/dist/cjs/editor/BikEditor.js.map +1 -1
- package/dist/cjs/editor/BikEditor.styles.js +12 -22
- package/dist/cjs/editor/BikEditor.styles.js.map +1 -1
- package/dist/cjs/editor/BikEditor.types.js.map +1 -1
- package/dist/cjs/editor/BikEditor.utils.js +1 -1
- package/dist/cjs/editor/BikEditor.utils.js.map +1 -1
- package/dist/cjs/editor/extensions/buildExtensions.js +1 -1
- package/dist/cjs/editor/extensions/buildExtensions.js.map +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/pasteUtils.js +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/pasteUtils.js.map +1 -1
- package/dist/cjs/src/components/QueryBuilder/Triggers/EVENTS/components/EventsTrigger.d.ts +1 -1
- package/dist/cjs/src/components/QueryBuilder/Triggers/EVENTS/selectors/useIGTriggerNameCacheSelector.d.ts +1 -1
- package/dist/cjs/src/components/QueryBuilder/Triggers/IG/components/IGTrigger.d.ts +1 -1
- package/dist/cjs/src/components/QueryBuilder/Triggers/IG/selectors/useIGTriggerNameCacheSelector.d.ts +1 -1
- package/dist/cjs/src/components/QueryBuilder/Triggers/components/BaseTriggerQueryBuilderNode.d.ts +2 -2
- package/dist/cjs/src/components/bik-layout/MockMenus.d.ts +0 -1
- package/dist/cjs/src/editor/BikEditor.styles.d.ts +1 -1
- package/dist/cjs/src/editor/BikEditor.types.d.ts +2 -0
- package/dist/cjs/src/editor/BikEditor.utils.d.ts +8 -14
- package/dist/cjs/src/editor/extensions/buildExtensions.d.ts +2 -1
- package/dist/cjs/src/editor/extensions/plainClipboard/PasteNormalizationExtension.d.ts +4 -22
- package/dist/cjs/src/editor/extensions/plainClipboard/pasteUtils.d.ts +5 -28
- package/dist/esm/components/feature-announcements/hooks/useFeatureAnnouncements.js +1 -1
- package/dist/esm/components/feature-announcements/hooks/useFeatureAnnouncements.js.map +1 -1
- package/dist/esm/editor/BikEditor.js +1 -1
- package/dist/esm/editor/BikEditor.js.map +1 -1
- package/dist/esm/editor/BikEditor.styles.js +12 -22
- package/dist/esm/editor/BikEditor.styles.js.map +1 -1
- package/dist/esm/editor/BikEditor.types.js.map +1 -1
- package/dist/esm/editor/BikEditor.utils.js +1 -1
- package/dist/esm/editor/BikEditor.utils.js.map +1 -1
- package/dist/esm/editor/extensions/buildExtensions.js +1 -1
- package/dist/esm/editor/extensions/buildExtensions.js.map +1 -1
- package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
- package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
- package/dist/esm/editor/extensions/plainClipboard/pasteUtils.js +1 -1
- package/dist/esm/editor/extensions/plainClipboard/pasteUtils.js.map +1 -1
- package/dist/esm/src/components/QueryBuilder/Triggers/EVENTS/components/EventsTrigger.d.ts +1 -1
- package/dist/esm/src/components/QueryBuilder/Triggers/EVENTS/selectors/useIGTriggerNameCacheSelector.d.ts +1 -1
- package/dist/esm/src/components/QueryBuilder/Triggers/IG/components/IGTrigger.d.ts +1 -1
- package/dist/esm/src/components/QueryBuilder/Triggers/IG/selectors/useIGTriggerNameCacheSelector.d.ts +1 -1
- package/dist/esm/src/components/QueryBuilder/Triggers/components/BaseTriggerQueryBuilderNode.d.ts +2 -2
- package/dist/esm/src/components/bik-layout/MockMenus.d.ts +0 -1
- package/dist/esm/src/editor/BikEditor.styles.d.ts +1 -1
- package/dist/esm/src/editor/BikEditor.types.d.ts +2 -0
- package/dist/esm/src/editor/BikEditor.utils.d.ts +8 -14
- package/dist/esm/src/editor/extensions/buildExtensions.d.ts +2 -1
- package/dist/esm/src/editor/extensions/plainClipboard/PasteNormalizationExtension.d.ts +4 -22
- package/dist/esm/src/editor/extensions/plainClipboard/pasteUtils.d.ts +5 -28
- package/package.json +1 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BikEditor.types.js","sources":["../../../src/editor/BikEditor.types.ts"],"sourcesContent":["/**\n * ┌─────────────────────────────────────────────────────────────────────────┐\n * │ PUBLIC API LAYER — zero editor-framework imports │\n * │ │\n * │ This file is the consumer contract for BikEditor. It has NO dependency │\n * │ on TipTap, Lexical, Slate, or any other rich-text library. │\n * │ │\n * │ If the underlying editor is ever swapped, ONLY the implementation files │\n * │ change — this file, src/editor/index.ts, and all consumer code stay as- │\n * │ is. The files that would change are: │\n * │ • BikEditor.tsx — wires props into the editor instance │\n * │ • BikEditor.utils.ts — HTML / document-state adapters │\n * │ • extensions/ — extension / plugin layer │\n * └─────────────────────────────────────────────────────────────────────────┘\n */\nimport React from 'react';\n\n/**\n * Opt in to specific editor capabilities. Both default to `false`.\n *\n * @example\n * // Email editor with full rich text support\n * <BikEditor features={{ richPaste: true, richTypography: true }} />\n */\nexport interface EditorFeatures {\n\t/** Preserve HTML formatting on paste. Default: `false`. */\n\trichPaste?: boolean;\n\t/** Enable font family, font size, text alignment, highlight, subscript, superscript, and image. Default: `false`. */\n\trichTypography?: boolean;\n\t/**\n\t * Restrict which inline marks are active in the editor.\n\t * When set, only the listed marks are registered — all others are disabled\n\t * (no rendering, no keyboard shortcuts, no paste preservation).\n\t * Omit to allow all marks (default).\n\t *\n\t * @example\n\t * // LiveChat: bold only, no italic / underline / strike\n\t * features={{ allowedMarks: ['bold'] }}\n\t */\n\tallowedMarks?: Array<'bold' | 'italic' | 'strike' | 'underline' | 'code'>;\n}\n\n/**\n * A named content section placed below the main body, separated by an invisible divider.\n *\n * Sections let you split a single editor instance into independently readable\n * and writable zones — for example: body / signature / forwarded mail.\n *\n * @example\n * sections={[\n * { id: 'signature', content: '<p>Best regards,<br/>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- Forwarded message ---</p>' },\n * ]}\n */\nexport interface EditorSection {\n\t/**\n\t * Unique identifier. Used in ref methods: `getSectionContent(id)`,\n\t * `setSectionContent(id, html)`, `focusSection(id)`, `clearSection(id)`.\n\t * The string `'body'` is reserved for the main content above all dividers.\n\t */\n\tid: string;\n\t/** Initial HTML content of this section. */\n\tcontent: string;\n}\n\n/**\n * Data passed to the `onPaste` callback.\n * Return `true` from your handler to suppress BikEditor's default paste handling\n * (rich-paste and plain-text stripping won't run for that event).\n */\nexport interface PasteData {\n\t/** All files in the clipboard (images, PDFs, etc.). */\n\tfiles: File[];\n\t/** Image files only — a filtered subset of `files`. */\n\timages: File[];\n\t/** Plain text from the clipboard. */\n\ttext: string;\n\t/** HTML from the clipboard (empty string if the source doesn't provide HTML). */\n\thtml: string;\n\t/** The raw browser ClipboardEvent — for advanced use. */\n\tevent: ClipboardEvent;\n}\n\n/**\n * A custom keyboard shortcut registered with the editor.\n *\n * @example\n * shortcuts={[\n * // Mod = Cmd on Mac, Ctrl on Windows/Linux\n * { key: 'Enter', modifiers: ['mod'], handler: handleSend },\n * { key: 's', modifiers: ['mod'], handler: handleSave },\n * { key: 'k', modifiers: ['mod', 'shift'], handler: openLinkModal },\n * ]}\n */\nexport interface KeyboardShortcut {\n\t/** Key name. Use the same strings as KeyboardEvent.key, e.g. `'Enter'`, `'s'`, `'k'`. */\n\tkey: string;\n\t/**\n\t * Modifier keys.\n\t * `'mod'` maps to Cmd on macOS and Ctrl on Windows/Linux — prefer it over\n\t * `'ctrl'` for cross-platform shortcuts.\n\t */\n\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t/** Called when the shortcut fires. Returning nothing is fine. */\n\thandler: () => void;\n}\n\n/** Content object returned by onChange, onSend, getContent(), and getSectionContent(). */\nexport interface EditorSnapshot {\n\t/** Serialised HTML. Safe to pass back to setContent() or setSectionContent(). */\n\thtml: string;\n\t/** Plain text, stripped of all marks and block nodes. */\n\ttext: string;\n\t/** True when the document contains no user-visible content. */\n\tisEmpty: boolean;\n\t/** Character count at this point in time. */\n\tcharacterCount: number;\n}\n\n/** One item shown in the @agent or #team mention dropdown. */\nexport interface MentionItem {\n\t/** Unique identifier stored as a node attribute. Used to look up the item later. */\n\tid: string | number;\n\t/** Display name shown in the dropdown and inserted into the document. */\n\tlabel: string;\n\t/** Avatar image URL. Shown in the built-in row when renderMentionItem is not provided. */\n\tavatarUrl?: string;\n\t/** When true, a green presence dot appears in the built-in row. */\n\tisOnline?: boolean;\n\t/** Any extra data your app needs. Passed back as-is in onMentionSelected. */\n\tmeta?: Record<string, unknown>;\n}\n\n/** One item shown in the / slash-command dropdown. */\nexport interface SlashCommandItem {\n\t/** Unique identifier. */\n\tid: string;\n\t/** Short command name shown as the primary label in the menu. */\n\tlabel: string;\n\t/** Optional one-line description shown below the label in the built-in row. */\n\tdescription?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Feature config objects — pass one object per feature instead of N flat props\n// ---------------------------------------------------------------------------\n\n/**\n * Props passed to the `renderDropdown` function in `MentionConfig`.\n * `activeIndex` is the keyboard-navigated row — highlight it in your UI.\n * Call `onSelect(item)` when the user picks one.\n */\nexport interface MentionDropdownRenderProps {\n\titems: MentionItem[];\n\t/** The text the user typed after the `@` or `#` trigger. */\n\tquery: string;\n\t/**\n\t * Index of the currently highlighted row (arrow-key navigation).\n\t * The editor manages this automatically — just use it for visual highlighting.\n\t */\n\tactiveIndex: number;\n\t/** Call this when the user selects an item (click, touch, etc.). */\n\tonSelect: (item: MentionItem) => void;\n}\n\n/**\n * Everything related to @ and # mention dropdowns, in one place.\n *\n * @example\n * mentions={{\n * agents: [{ id: 1, label: 'Alice', avatarUrl: '...' }],\n * teams: [{ id: 't1', label: 'Support' }],\n * onSelect: (item, char) => console.log(char, item),\n * }}\n */\nexport interface MentionConfig {\n\t/** Items shown when the user types `@`. */\n\tagents?: MentionItem[];\n\t/** Items shown when the user types `#`. */\n\tteams?: MentionItem[];\n\t/** Called when the user picks an item. `char` is `'@'` or `'#'`. */\n\tonSelect?: (item: MentionItem, char: '@' | '#') => void;\n\t/**\n\t * Custom renderer for each row in the dropdown.\n\t * `isActive` is `true` when that row is currently highlighted by keyboard\n\t * navigation (arrows move it, Enter selects it). Use it to apply a highlight\n\t * style so the user can see which item they're about to select.\n\t * Omit to use the built-in avatar + name row.\n\t *\n\t * Use `renderDropdown` instead when you need a completely different structure.\n\t *\n\t * @example\n\t * renderItem={(item, isActive) => (\n\t * <div style={{ background: isActive ? '#f0f0f0' : undefined }}>\n\t * {item.label}\n\t * </div>\n\t * )}\n\t */\n\trenderItem?: (item: MentionItem, isActive: boolean) => React.ReactNode;\n\t/**\n\t * Render the entire dropdown yourself instead of individual rows.\n\t * The editor still manages keyboard navigation — `activeIndex` tells you\n\t * which row is highlighted, and `onSelect(item)` confirms the selection.\n\t * Takes priority over `renderItem` when both are provided.\n\t *\n\t * @example\n\t * renderDropdown={({ items, query, activeIndex, onSelect }) => (\n\t * <MyDropdown\n\t * items={items}\n\t * highlightedIndex={activeIndex}\n\t * onPick={onSelect}\n\t * />\n\t * )}\n\t */\n\trenderDropdown?: (props: MentionDropdownRenderProps) => React.ReactNode;\n}\n\n/**\n * Props passed to the `renderDropdown` function in `SlashCommandConfig`.\n */\nexport interface SlashCommandDropdownRenderProps {\n\titems: SlashCommandItem[];\n\t/** The text the user typed after `/`. */\n\tquery: string;\n\t/** Index of the currently highlighted row. Use for visual highlighting. */\n\tactiveIndex: number;\n\t/** Call this when the user selects an item. */\n\tonSelect: (item: SlashCommandItem) => void;\n}\n\n/**\n * Everything related to the `/` slash-command menu, in one place.\n *\n * @example\n * slashCommands={{\n * items: [{ id: 'ai-reply', label: 'AI Reply', description: 'Generate a reply' }],\n * onSelect: (cmd) => triggerAI(cmd.id),\n * }}\n */\nexport interface SlashCommandConfig {\n\t/** The list of commands. Filtered client-side as the user types after `/`. */\n\titems: SlashCommandItem[];\n\t/** Called when the user selects a command. The `/` trigger is removed from the doc. */\n\tonSelect?: (command: SlashCommandItem) => void;\n\t/**\n\t * Custom renderer for each row.\n\t * `isActive` is `true` when that row is highlighted by keyboard navigation.\n\t * Omit to use the built-in label + description row.\n\t *\n\t * Use `renderDropdown` instead when you need a completely different structure.\n\t */\n\trenderItem?: (item: SlashCommandItem, isActive: boolean) => React.ReactNode;\n\t/**\n\t * Render the entire slash-command dropdown yourself.\n\t * The editor manages keyboard navigation — use `activeIndex` to highlight\n\t * the active row and call `onSelect(item)` to confirm selection.\n\t * Takes priority over `renderItem` when both are provided.\n\t *\n\t * @example\n\t * renderDropdown={({ items, activeIndex, onSelect }) => (\n\t * <MyCmdPalette commands={items} active={activeIndex} onPick={onSelect} />\n\t * )}\n\t */\n\trenderDropdown?: (props: SlashCommandDropdownRenderProps) => React.ReactNode;\n}\n\n/**\n * Configuration for how hyperlinks behave in the editor.\n *\n * @example\n * link={{ renderTooltip: ({ href, text, onOpen, onRemove }) => (\n * <MyLinkPopover href={href} onOpen={onOpen} onRemove={onRemove} />\n * )}}\n */\nexport interface LinkConfig {\n\t/**\n\t * Custom tooltip rendered when the user clicks a link.\n\t * Receives the href, display text, bounding rect (for positioning),\n\t * and ready-to-call action handlers.\n\t * Omit to use the minimal built-in tooltip.\n\t */\n\trenderTooltip?: (props: LinkTooltipProps) => React.ReactNode;\n}\n\n/**\n * Snapshot of all active marks and block types at the current cursor or selection.\n * Returned by onSelectionChange and ref.getActiveFormats().\n *\n * Keep this in component state and pass it to BikEditorToolbar (or your own toolbar)\n * to keep button active-states in sync.\n *\n * @example\n * const [formats, setFormats] = useState(DEFAULT_ACTIVE_FORMATS);\n * <BikEditor onSelectionChange={setFormats} ... />\n * <BikEditorToolbar activeFormats={formats} ... />\n */\nexport interface FormatState {\n\tbold: boolean;\n\titalic: boolean;\n\tunderline: boolean;\n\tstrike: boolean;\n\tbulletList: boolean;\n\torderedList: boolean;\n\tblockquote: boolean;\n\tcodeBlock: boolean;\n\t/** Non-null when the cursor is inside (or selection spans) a hyperlink. */\n\tlink: { href: string } | null;\n\ttextAlign: 'left' | 'center' | 'right' | 'justify' | null;\n\tfontFamily: string | null;\n\tfontSize: string | null;\n\t/** Active text colour as a CSS colour string, or null if none. */\n\tcolor: string | null;\n\t/** Active highlight (background) colour, or null if none. */\n\thighlight: string | null;\n\t/** True when the cursor/selection is in a superscript. */\n\tsuperscript: boolean;\n\t/** True when the cursor/selection is in a subscript. */\n\tsubscript: boolean;\n}\n\nexport const DEFAULT_FORMAT_STATE: FormatState = {\n\tbold: false,\n\titalic: false,\n\tunderline: false,\n\tstrike: false,\n\tbulletList: false,\n\torderedList: false,\n\tblockquote: false,\n\tcodeBlock: false,\n\tlink: null,\n\ttextAlign: null,\n\tfontFamily: null,\n\tfontSize: null,\n\tcolor: null,\n\thighlight: null,\n\tsuperscript: false,\n\tsubscript: false,\n};\n\n/**\n * All formatting and insertion commands. Access via `editorRef.current.actions`.\n *\n * Every method focuses the editor internally — no need to call `.focus()` first.\n *\n * @example\n * editorRef.current?.actions.toggleBold();\n * editorRef.current?.actions.setLink('https://example.com', 'Visit example');\n */\nexport interface EditorActions {\n\t// ── Basic marks ───────────────────────────────────────────────────────────\n\ttoggleBold: () => void;\n\ttoggleItalic: () => void;\n\ttoggleUnderline: () => void;\n\ttoggleStrike: () => void;\n\n\t// ── Blocks ────────────────────────────────────────────────────────────────\n\ttoggleBulletList: () => void;\n\ttoggleOrderedList: () => void;\n\ttoggleBlockquote: () => void;\n\ttoggleCodeBlock: () => void;\n\n\t// ── Alignment & typography ────────────────────────────────────────────────\n\tsetTextAlign: (align: 'left' | 'center' | 'right' | 'justify') => void;\n\t/** Pass a CSS font-family value, e.g. `'Inter, sans-serif'`. Only works when richTypography is on. */\n\tsetFontFamily: (font: string) => void;\n\t/** Pass a CSS size value, e.g. `'16px'` or `'1.25rem'`. Only works when richTypography is on. */\n\tsetFontSize: (size: string) => void;\n\n\t// ── Colour ────────────────────────────────────────────────────────────────\n\t/** Sets the text colour. Pass any CSS colour value, e.g. `'#e53e3e'`. */\n\tsetColor: (color: string) => void;\n\t/** Sets the highlight (background) colour. */\n\tsetHighlight: (color: string) => void;\n\t/** Removes the text colour mark from the current selection. */\n\tunsetColor: () => void;\n\t/** Removes the highlight mark from the current selection. */\n\tunsetHighlight: () => void;\n\n\t// ── Links ─────────────────────────────────────────────────────────────────\n\t/**\n\t * Applies a hyperlink to the current selection, or updates the link the\n\t * cursor is already inside.\n\t *\n\t * @param href The URL.\n\t * @param text Optional display text. When provided, the visible text of the\n\t * linked range is replaced atomically in the same undo step.\n\t *\n\t * @example\n\t * actions.setLink('https://example.com');\n\t * actions.setLink('https://example.com', 'Visit example');\n\t */\n\tsetLink: (href: string, text?: string) => void;\n\t/** Updates only the href of the link at the current cursor. */\n\tupdateLink: (href: string) => void;\n\t/** Removes the link mark but keeps the visible text. */\n\tremoveLink: () => void;\n\n\t// ── Insertions ────────────────────────────────────────────────────────────\n\t/** Inserts an emoji character at the cursor. */\n\tinsertEmoji: (emoji: string) => void;\n\t/**\n\t * Inserts a `{{variableName}}` token at the cursor.\n\t * The token renders with a distinct highlight inside the editor.\n\t */\n\tinsertVariable: (variableName: string) => void;\n\t/** Inserts arbitrary HTML at the current cursor position. */\n\tinsertHtml: (html: string) => void;\n\n\t// ── History ───────────────────────────────────────────────────────────────\n\tundo: () => void;\n\tredo: () => void;\n\tcanUndo: () => boolean;\n\tcanRedo: () => boolean;\n\n\t// ── Rich typography (richTypography feature required) ─────────────────────\n\t/** Toggles superscript on the selection. Only works when richTypography is on. */\n\ttoggleSuperscript: () => void;\n\t/** Toggles subscript on the selection. Only works when richTypography is on. */\n\ttoggleSubscript: () => void;\n\t/** Inserts an inline image at the cursor. Only works when richTypography is on. */\n\tinsertImage: (src: string) => void;\n}\n\n/** Props passed to your renderLinkTooltip render prop. */\nexport interface LinkTooltipProps {\n\t/** The href of the active link. */\n\thref: string;\n\t/** The display text of the link. */\n\ttext: string;\n\t/** Bounding rect of the link element — use this to position your popover. */\n\trect: DOMRect;\n\t/** Opens the link in a new tab. */\n\tonOpen: () => void;\n\t/** Removes the link mark but keeps the visible text. */\n\tonRemove: () => void;\n\t/** Hides the bubble menu. Call this before opening an edit modal so the tooltip doesn't linger. */\n\tonHide: () => void;\n}\n\n/**\n * Imperative handle exposed by BikEditor.\n *\n * Obtain via `useRef<BikEditorRef>()` and pass to `<BikEditor ref={...} />`.\n * All methods are safe to call after the first render.\n *\n * @example\n * const ref = useRef<BikEditorRef>(null);\n * <BikEditor ref={ref} ... />\n * ref.current?.clearContent();\n * ref.current?.actions.toggleBold();\n */\nexport interface BikEditorRef {\n\t// ── Focus ─────────────────────────────────────────────────────────────────\n\t/** Moves focus into the editor. Optionally pass a position: 'start', 'end', 'all', or a numeric offset. */\n\tfocus: (position?: 'start' | 'end' | 'all' | number | boolean | null) => void;\n\t/** Removes focus from the editor. */\n\tblur: () => void;\n\n\t// ── Content ───────────────────────────────────────────────────────────────\n\t/** Clears the entire document and fires onChange. */\n\tclearContent: () => void;\n\t/**\n\t * Replaces the full document with the given HTML and fires onChange.\n\t * Overwrites all sections. Use setSectionContent() / setBodyContent() to\n\t * update a single section without touching the others.\n\t */\n\tsetContent: (html: string) => void;\n\t/** Inserts HTML at the current cursor position without replacing anything. */\n\tinsertContent: (html: string) => void;\n\t/**\n\t * Inserts HTML inline at the current cursor position, unwrapping block\n\t * wrappers (e.g. `<p>`) so the content merges into the surrounding paragraph\n\t * instead of creating new blocks. Use for macros, quick replies, etc.\n\t */\n\tinsertInlineContent: (html: string) => void;\n\t/** Inserts HTML at the very start of the document. */\n\tinsertAtStart: (html: string) => void;\n\t/** Inserts HTML as a new block at the end of the document. */\n\tinsertBlock: (html: string) => void;\n\t/**\n\t * Appends HTML inline inside the last paragraph of the document.\n\t * Ideal for streaming — call repeatedly with each text chunk.\n\t * Use `appendBodyContent` instead if the editor has named sections.\n\t */\n\tappendInline: (html: string) => void;\n\t/** @deprecated Use `insertBlock` */\n\tinsertAtEnd: (html: string) => void;\n\t/** @deprecated Use `appendInline` */\n\tappendContent: (html: string) => void;\n\t/** Returns the full document as `{ html, text, isEmpty, characterCount }`. */\n\tgetContent: () => EditorSnapshot;\n\t/**\n\t * Returns the raw TipTap document as a plain JSON object.\n\t * Returns `null` when the editor is not yet mounted.\n\t */\n\tgetJSON: () => Record<string, unknown> | null;\n\t/**\n\t * Walks the document and returns all mention IDs currently in the editor,\n\t * split by type. IDs are deduplicated and returned as arrays.\n\t *\n\t * @example\n\t * const { agentIds, teamIds } = ref.current?.getMentions() ?? { agentIds: [], teamIds: [] };\n\t */\n\tgetMentions: () => { agentIds: string[]; teamIds: string[] };\n\n\t// ── Selection ─────────────────────────────────────────────────────────────\n\t/**\n\t * Returns the current cursor / selection position as a `{ from, to }` pair.\n\t * When nothing is selected, `from === to` (a collapsed cursor).\n\t * Pass the returned value to `insertAtPosition()` to insert at an exact spot.\n\t *\n\t * @example\n\t * const { from } = ref.current?.getCursorPosition() ?? { from: 0, to: 0 };\n\t * ref.current?.insertAtPosition(from, '<strong>inserted</strong>');\n\t */\n\tgetCursorPosition: () => { from: number; to: number };\n\t/**\n\t * Inserts HTML at an explicit document position rather than at the current cursor.\n\t * Use `getCursorPosition()` to get the position, or any value from\n\t * `findSectionStartPos` / `findSectionEndPos` if you need section-relative insertion.\n\t *\n\t * @example\n\t * const { from } = ref.current!.getCursorPosition();\n\t * ref.current?.insertAtPosition(from, '<em>hello</em>');\n\t */\n\tinsertAtPosition: (pos: number, html: string) => void;\n\t/**\n\t * Returns the currently selected plain text.\n\t * When the cursor is inside a link with nothing selected, returns the full\n\t * link text — useful for pre-filling a link modal's text field.\n\t */\n\tgetSelectedText: () => string;\n\n\t// ── Character count ───────────────────────────────────────────────────────\n\t/**\n\t * Returns the current character count and the configured limit (or null).\n\t * Use this to render your own character counter anywhere you like.\n\t *\n\t * @example\n\t * const { count, limit } = ref.current?.getCharacterCount() ?? { count: 0, limit: null };\n\t * <span style={{ color: count >= (limit ?? Infinity) ? 'red' : 'inherit' }}>\n\t * {count}{limit != null ? ` / ${limit}` : ''}\n\t * </span>\n\t */\n\tgetCharacterCount: () => { count: number; limit: number | null };\n\n\t// ── Sections ─────────────────────────────────────────────────────────────\n\t/**\n\t * Returns content for a named section.\n\t * Use `'body'` to get the main content above all section dividers.\n\t *\n\t * @example\n\t * const sig = ref.current?.getSectionContent('signature');\n\t */\n\tgetSectionContent: (sectionId: string) => EditorSnapshot;\n\t/**\n\t * Replaces the content of a named section without touching other sections.\n\t * If the section doesn't exist yet it is appended at the end.\n\t *\n\t * @example\n\t * ref.current?.setSectionContent('signature', '<p>New signature</p>');\n\t * ref.current?.setSectionContent('body', rephrased); // same as setBodyContent\n\t */\n\tsetSectionContent: (sectionId: string, html: string) => void;\n\t/**\n\t * Moves the cursor to the start of a named section.\n\t * Use `'body'` to focus the main content area.\n\t */\n\tfocusSection: (sectionId: string) => void;\n\t/** Clears all content in a named section (replaces with an empty paragraph). */\n\tclearSection: (sectionId: string) => void;\n\n\t// ── Convenience body shorthands ───────────────────────────────────────────\n\t/** Shorthand for `getSectionContent('body')`. */\n\tgetBodyContent: () => EditorSnapshot;\n\t/** Shorthand for `setSectionContent('body', html)`. */\n\tsetBodyContent: (html: string) => void;\n\t/**\n\t * Atomically sets the body AND all sections in a single editor update.\n\t * Sections are laid out in the exact order supplied — use this instead of\n\t * multiple `setSectionContent()` calls whenever order matters (e.g. forward\n\t * mail where signature must appear before the forwarded thread).\n\t *\n\t * @example\n\t * ref.current?.setBodyAndSections('<p></p>', [\n\t * { id: 'signature', content: '<p>Alice</p>' },\n\t * { id: 'forwarded', content: '<p>--- Forwarded ---</p>' },\n\t * ]);\n\t */\n\tsetBodyAndSections: (\n\t\tbody: string,\n\t\tsections: Array<{ id: string; content: string }>,\n\t) => void;\n\t/**\n\t * Appends HTML to the end of the body section without moving the cursor.\n\t * Ideal for streaming AI-generated content — call repeatedly with each chunk.\n\t * Does NOT clear existing content; call `setBodyContent('')` first if needed.\n\t *\n\t * @example\n\t * // Stream AI response chunk-by-chunk\n\t * ref.current?.setBodyContent('');\n\t * for await (const chunk of aiStream) {\n\t * ref.current?.appendBodyContent(chunk);\n\t * }\n\t */\n\tappendBodyContent: (html: string) => void;\n\n\t/**\n\t * Inserts HTML at the start of a named section (or `'body'`).\n\t * Inserts at the very start of the document when `sectionId` is `'body'`.\n\t */\n\tinsertAtSectionStart: (sectionId: string, html: string) => void;\n\t/**\n\t * Inserts HTML at the end of a named section (or `'body'`).\n\t * For `'body'`, inserts just before the first section divider (or at end of doc).\n\t */\n\tinsertAtSectionEnd: (sectionId: string, html: string) => void;\n\n\t// ── Formats & actions ─────────────────────────────────────────────────────\n\t/** Returns a snapshot of all active marks at the current cursor or selection. */\n\tgetActiveFormats: () => FormatState;\n\t/** All formatting and insertion commands. See EditorActions. */\n\tactions: EditorActions;\n}\n\n/**\n * Props for `<BikEditor />`. All props are optional.\n */\nexport interface BikEditorProps {\n\t// ── Features ──────────────────────────────────────────────────────────────\n\t/**\n\t * Opt in to specific editor capabilities. Both default to `false`.\n\t *\n\t * @example\n\t * <BikEditor features={{ richPaste: true, richTypography: true }} />\n\t */\n\tfeatures?: EditorFeatures;\n\n\t// ── Content ───────────────────────────────────────────────────────────────\n\t/**\n\t * HTML loaded on mount. Ignored after first render.\n\t * Use `ref.setContent()` to change content imperatively later.\n\t */\n\tinitialContent?: string;\n\t/**\n\t * Named sections placed below the main body.\n\t * Each section is separated from the previous one by an invisible divider node.\n\t * The array order determines the visual order in the document.\n\t *\n\t * These are only the INITIAL values — use `ref.setSectionContent()` to update\n\t * a section after mount without re-mounting the editor.\n\t *\n\t * @example\n\t * sections={[\n\t * { id: 'signature', content: '<p>Best regards, Alice</p>' },\n\t * { id: 'forwarded', content: '<p>--- Forwarded message ---</p>' },\n\t * ]}\n\t */\n\tsections?: EditorSection[];\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n\n\t// ── Behaviour ─────────────────────────────────────────────────────────────\n\t/** Makes the editor read-only. The toolbar still renders but actions are no-ops. */\n\tdisabled?: boolean;\n\t/**\n\t * Hard character limit. The editor refuses to accept input beyond this point.\n\t * The limit is enforced silently — no UI is rendered. Use\n\t * `ref.getCharacterCount()` or `onChange.characterCount` to display it yourself.\n\t */\n\tmaxCharacters?: number;\n\n\t// ── Keyboard shortcuts ────────────────────────────────────────────────────\n\t/**\n\t * Custom keyboard shortcuts. BikEditor registers no shortcuts of its own —\n\t * every shortcut must be explicitly listed here.\n\t *\n\t * @example\n\t * shortcuts={[\n\t * { key: 'Enter', modifiers: ['mod'], handler: handleSend },\n\t * { key: 's', modifiers: ['mod', 'shift'], handler: handleDraft },\n\t * ]}\n\t */\n\tshortcuts?: KeyboardShortcut[];\n\t/**\n\t * Shortcut key that fires `onSend`. No key is bound to `onSend` unless you\n\t * set this explicitly. Requires `onSend` to also be provided.\n\t *\n\t * `'mod'` maps to Cmd on macOS and Ctrl on Windows/Linux.\n\t *\n\t * @example\n\t * // Single shortcut\n\t * sendShortcut={{ key: 'Enter', modifiers: ['mod'] }}\n\t * // Multiple shortcuts\n\t * sendShortcut={[\n\t * { key: 'Enter', modifiers: ['mod'] },\n\t * { key: 's', modifiers: ['mod'] },\n\t * ]}\n\t */\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\n\t// ── Mentions ─────────────────────────────────────────────────────────────\n\t/**\n\t * Enables `@` (agent) and `#` (team) mention dropdowns.\n\t * Bundle all mention-related config in one object so it's easy to find.\n\t *\n\t * @example\n\t * mentions={{\n\t * agents: myAgents,\n\t * teams: myTeams,\n\t * onSelect: (item, char) => track(char, item.id),\n\t * }}\n\t */\n\tmentions?: MentionConfig;\n\n\t// ── Slash commands ────────────────────────────────────────────────────────\n\t/**\n\t * Enables the `/` slash-command menu.\n\t * Bundle all slash-command config in one object.\n\t *\n\t * @example\n\t * slashCommands={{\n\t * items: MY_COMMANDS,\n\t * onSelect: (cmd) => handleCommand(cmd.id),\n\t * }}\n\t */\n\tslashCommands?: SlashCommandConfig;\n\n\t// ── Link ─────────────────────────────────────────────────────────────────\n\t/**\n\t * Link-related configuration — currently just the click tooltip.\n\t *\n\t * @example\n\t * link={{ renderTooltip: ({ href, onOpen, onRemove }) => (\n\t * <MyTooltip href={href} onOpen={onOpen} onRemove={onRemove} />\n\t * )}}\n\t */\n\tlink?: LinkConfig;\n\n\t// ── Callbacks ─────────────────────────────────────────────────────────────\n\t/**\n\t * Called once, immediately after the editor finishes initializing.\n\t * Any imperative ref methods called before this fires are queued internally\n\t * and executed automatically once the editor is ready — consumers do not\n\t * need timers or readiness guards.\n\t *\n\t * @example\n\t * <BikEditor\n\t * ref={editorRef}\n\t * onReady={() => editorRef.current?.setContent(savedDraft)}\n\t * />\n\t */\n\tonReady?: () => void;\n\t/** Called on every document change. */\n\tonChange?: (content: EditorSnapshot) => void;\n\t/**\n\t * Called when the user presses the key combo defined by `sendShortcut`.\n\t * Only fires when both `onSend` and `sendShortcut` are provided.\n\t */\n\tonSend?: (content: EditorSnapshot) => void;\n\t/** Called when the editor gains focus. */\n\tonFocus?: () => void;\n\t/** Called when the editor loses focus. */\n\tonBlur?: () => void;\n\t/**\n\t * Called on every paste event. Return `true` to fully suppress BikEditor's\n\t * own paste handling (useful when you want to handle images or HTML yourself).\n\t *\n\t * @example\n\t * onPaste={({ images }) => {\n\t * if (images.length) {\n\t * uploadImages(images).then(urls => {\n\t * urls.forEach(url => ref.current?.actions.insertHtml(`<img src=\"${url}\" />`));\n\t * });\n\t * return true; // suppress default — don't embed the raw image data\n\t * }\n\t * }}\n\t */\n\tonPaste?: (data: PasteData) => boolean | void;\n\t/**\n\t * Called on every cursor move or selection change with a snapshot of all\n\t * active marks. Keep in state and pass to your toolbar to sync button states.\n\t */\n\tonSelectionChange?: (activeFormats: FormatState) => void;\n\n\t// ── Styling ───────────────────────────────────────────────────────────────\n\t// Outer wrapper — use these to control the shell (border, border-radius,\n\t// background, shadow, etc.).\n\t/** Extra class name applied to the outer wrapper div. */\n\tclassName?: string;\n\t/** Inline styles applied to the outer wrapper div. */\n\tstyle?: React.CSSProperties;\n\n\t// Inner editable area — use these to control the content div itself\n\t// (padding, font, line-height, etc.).\n\t/** Extra class name applied to the content-editable area (`.ProseMirror` in TipTap). */\n\teditorClassName?: string;\n\t/** Inline styles applied to the content-editable area. */\n\teditorStyle?: React.CSSProperties;\n\n\t// Sizing — applied to the content-editable area.\n\t/** Minimum height of the editable area. CSS value, e.g. `'80px'`. */\n\tminHeight?: string;\n\t/** Maximum height. The editor scrolls internally when exceeded. CSS value, e.g. `'400px'`. */\n\tmaxHeight?: string;\n}\n"],"names":["DEFAULT_FORMAT_STATE","bold","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","textAlign","fontFamily","fontSize","color","highlight","superscript","subscript"],"mappings":"AAgUO,MAAMA,EAAoC,CAChDC,MAAM,EACNC,QAAQ,EACRC,WAAW,EACXC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,YAAY,EACZC,WAAW,EACXC,KAAM,KACNC,UAAW,KACXC,WAAY,KACZC,SAAU,KACVC,MAAO,KACPC,UAAW,KACXC,aAAa,EACbC,WAAW"}
|
|
1
|
+
{"version":3,"file":"BikEditor.types.js","sources":["../../../src/editor/BikEditor.types.ts"],"sourcesContent":["/**\n * ┌─────────────────────────────────────────────────────────────────────────┐\n * │ PUBLIC API LAYER — zero editor-framework imports │\n * │ │\n * │ This file is the consumer contract for BikEditor. It has NO dependency │\n * │ on TipTap, Lexical, Slate, or any other rich-text library. │\n * │ │\n * │ If the underlying editor is ever swapped, ONLY the implementation files │\n * │ change — this file, src/editor/index.ts, and all consumer code stay as- │\n * │ is. The files that would change are: │\n * │ • BikEditor.tsx — wires props into the editor instance │\n * │ • BikEditor.utils.ts — HTML / document-state adapters │\n * │ • extensions/ — extension / plugin layer │\n * └─────────────────────────────────────────────────────────────────────────┘\n */\nimport React from 'react';\n\n/**\n * Opt in to specific editor capabilities. Both default to `false`.\n *\n * @example\n * // Email editor with full rich text support\n * <BikEditor features={{ richPaste: true, richTypography: true }} />\n */\nexport interface EditorFeatures {\n\t/** Preserve HTML formatting on paste. Default: `false`. */\n\trichPaste?: boolean;\n\t/** Enable font family, font size, text alignment, highlight, subscript, superscript, and image. Default: `false`. */\n\trichTypography?: boolean;\n\t/**\n\t * Restrict which inline marks are active in the editor.\n\t * When set, only the listed marks are registered — all others are disabled\n\t * (no rendering, no keyboard shortcuts, no paste preservation).\n\t * Omit to allow all marks (default).\n\t *\n\t * @example\n\t * // LiveChat: bold only, no italic / underline / strike\n\t * features={{ allowedMarks: ['bold'] }}\n\t */\n\tallowedMarks?: Array<'bold' | 'italic' | 'strike' | 'underline' | 'code'>;\n}\n\n/**\n * A named content section placed below the main body, separated by an invisible divider.\n *\n * Sections let you split a single editor instance into independently readable\n * and writable zones — for example: body / signature / forwarded mail.\n *\n * @example\n * sections={[\n * { id: 'signature', content: '<p>Best regards,<br/>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- Forwarded message ---</p>' },\n * ]}\n */\nexport interface EditorSection {\n\t/**\n\t * Unique identifier. Used in ref methods: `getSectionContent(id)`,\n\t * `setSectionContent(id, html)`, `focusSection(id)`, `clearSection(id)`.\n\t * The string `'body'` is reserved for the main content above all dividers.\n\t */\n\tid: string;\n\t/** Initial HTML content of this section. */\n\tcontent: string;\n}\n\n/**\n * Data passed to the `onPaste` callback.\n * Return `true` from your handler to suppress BikEditor's default paste handling\n * (rich-paste and plain-text stripping won't run for that event).\n */\nexport interface PasteData {\n\t/** All files in the clipboard (images, PDFs, etc.). */\n\tfiles: File[];\n\t/** Image files only — a filtered subset of `files`. */\n\timages: File[];\n\t/** Plain text from the clipboard. */\n\ttext: string;\n\t/** HTML from the clipboard (empty string if the source doesn't provide HTML). */\n\thtml: string;\n\t/** The raw browser ClipboardEvent — for advanced use. */\n\tevent: ClipboardEvent;\n}\n\n/**\n * A custom keyboard shortcut registered with the editor.\n *\n * @example\n * shortcuts={[\n * // Mod = Cmd on Mac, Ctrl on Windows/Linux\n * { key: 'Enter', modifiers: ['mod'], handler: handleSend },\n * { key: 's', modifiers: ['mod'], handler: handleSave },\n * { key: 'k', modifiers: ['mod', 'shift'], handler: openLinkModal },\n * ]}\n */\nexport interface KeyboardShortcut {\n\t/** Key name. Use the same strings as KeyboardEvent.key, e.g. `'Enter'`, `'s'`, `'k'`. */\n\tkey: string;\n\t/**\n\t * Modifier keys.\n\t * `'mod'` maps to Cmd on macOS and Ctrl on Windows/Linux — prefer it over\n\t * `'ctrl'` for cross-platform shortcuts.\n\t */\n\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t/** Called when the shortcut fires. Returning nothing is fine. */\n\thandler: () => void;\n}\n\n/** Content object returned by onChange, onSend, getContent(), and getSectionContent(). */\nexport interface EditorSnapshot {\n\t/** Serialised HTML. Safe to pass back to setContent() or setSectionContent(). */\n\thtml: string;\n\t/** Plain text, stripped of all marks and block nodes. */\n\ttext: string;\n\t/** True when the document contains no user-visible content. */\n\tisEmpty: boolean;\n\t/** Character count at this point in time. */\n\tcharacterCount: number;\n}\n\n/** One item shown in the @agent or #team mention dropdown. */\nexport interface MentionItem {\n\t/** Unique identifier stored as a node attribute. Used to look up the item later. */\n\tid: string | number;\n\t/** Display name shown in the dropdown and inserted into the document. */\n\tlabel: string;\n\t/** Avatar image URL. Shown in the built-in row when renderMentionItem is not provided. */\n\tavatarUrl?: string;\n\t/** When true, a green presence dot appears in the built-in row. */\n\tisOnline?: boolean;\n\t/** Any extra data your app needs. Passed back as-is in onMentionSelected. */\n\tmeta?: Record<string, unknown>;\n}\n\n/** One item shown in the / slash-command dropdown. */\nexport interface SlashCommandItem {\n\t/** Unique identifier. */\n\tid: string;\n\t/** Short command name shown as the primary label in the menu. */\n\tlabel: string;\n\t/** Optional one-line description shown below the label in the built-in row. */\n\tdescription?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Feature config objects — pass one object per feature instead of N flat props\n// ---------------------------------------------------------------------------\n\n/**\n * Props passed to the `renderDropdown` function in `MentionConfig`.\n * `activeIndex` is the keyboard-navigated row — highlight it in your UI.\n * Call `onSelect(item)` when the user picks one.\n */\nexport interface MentionDropdownRenderProps {\n\titems: MentionItem[];\n\t/** The text the user typed after the `@` or `#` trigger. */\n\tquery: string;\n\t/**\n\t * Index of the currently highlighted row (arrow-key navigation).\n\t * The editor manages this automatically — just use it for visual highlighting.\n\t */\n\tactiveIndex: number;\n\t/** Call this when the user selects an item (click, touch, etc.). */\n\tonSelect: (item: MentionItem) => void;\n}\n\n/**\n * Everything related to @ and # mention dropdowns, in one place.\n *\n * @example\n * mentions={{\n * agents: [{ id: 1, label: 'Alice', avatarUrl: '...' }],\n * teams: [{ id: 't1', label: 'Support' }],\n * onSelect: (item, char) => console.log(char, item),\n * }}\n */\nexport interface MentionConfig {\n\t/** Items shown when the user types `@`. */\n\tagents?: MentionItem[];\n\t/** Items shown when the user types `#`. */\n\tteams?: MentionItem[];\n\t/** Called when the user picks an item. `char` is `'@'` or `'#'`. */\n\tonSelect?: (item: MentionItem, char: '@' | '#') => void;\n\t/**\n\t * Custom renderer for each row in the dropdown.\n\t * `isActive` is `true` when that row is currently highlighted by keyboard\n\t * navigation (arrows move it, Enter selects it). Use it to apply a highlight\n\t * style so the user can see which item they're about to select.\n\t * Omit to use the built-in avatar + name row.\n\t *\n\t * Use `renderDropdown` instead when you need a completely different structure.\n\t *\n\t * @example\n\t * renderItem={(item, isActive) => (\n\t * <div style={{ background: isActive ? '#f0f0f0' : undefined }}>\n\t * {item.label}\n\t * </div>\n\t * )}\n\t */\n\trenderItem?: (item: MentionItem, isActive: boolean) => React.ReactNode;\n\t/**\n\t * Render the entire dropdown yourself instead of individual rows.\n\t * The editor still manages keyboard navigation — `activeIndex` tells you\n\t * which row is highlighted, and `onSelect(item)` confirms the selection.\n\t * Takes priority over `renderItem` when both are provided.\n\t *\n\t * @example\n\t * renderDropdown={({ items, query, activeIndex, onSelect }) => (\n\t * <MyDropdown\n\t * items={items}\n\t * highlightedIndex={activeIndex}\n\t * onPick={onSelect}\n\t * />\n\t * )}\n\t */\n\trenderDropdown?: (props: MentionDropdownRenderProps) => React.ReactNode;\n}\n\n/**\n * Props passed to the `renderDropdown` function in `SlashCommandConfig`.\n */\nexport interface SlashCommandDropdownRenderProps {\n\titems: SlashCommandItem[];\n\t/** The text the user typed after `/`. */\n\tquery: string;\n\t/** Index of the currently highlighted row. Use for visual highlighting. */\n\tactiveIndex: number;\n\t/** Call this when the user selects an item. */\n\tonSelect: (item: SlashCommandItem) => void;\n}\n\n/**\n * Everything related to the `/` slash-command menu, in one place.\n *\n * @example\n * slashCommands={{\n * items: [{ id: 'ai-reply', label: 'AI Reply', description: 'Generate a reply' }],\n * onSelect: (cmd) => triggerAI(cmd.id),\n * }}\n */\nexport interface SlashCommandConfig {\n\t/** The list of commands. Filtered client-side as the user types after `/`. */\n\titems: SlashCommandItem[];\n\t/** Called when the user selects a command. The `/` trigger is removed from the doc. */\n\tonSelect?: (command: SlashCommandItem) => void;\n\t/**\n\t * Custom renderer for each row.\n\t * `isActive` is `true` when that row is highlighted by keyboard navigation.\n\t * Omit to use the built-in label + description row.\n\t *\n\t * Use `renderDropdown` instead when you need a completely different structure.\n\t */\n\trenderItem?: (item: SlashCommandItem, isActive: boolean) => React.ReactNode;\n\t/**\n\t * Render the entire slash-command dropdown yourself.\n\t * The editor manages keyboard navigation — use `activeIndex` to highlight\n\t * the active row and call `onSelect(item)` to confirm selection.\n\t * Takes priority over `renderItem` when both are provided.\n\t *\n\t * @example\n\t * renderDropdown={({ items, activeIndex, onSelect }) => (\n\t * <MyCmdPalette commands={items} active={activeIndex} onPick={onSelect} />\n\t * )}\n\t */\n\trenderDropdown?: (props: SlashCommandDropdownRenderProps) => React.ReactNode;\n}\n\n/**\n * Configuration for how hyperlinks behave in the editor.\n *\n * @example\n * link={{ renderTooltip: ({ href, text, onOpen, onRemove }) => (\n * <MyLinkPopover href={href} onOpen={onOpen} onRemove={onRemove} />\n * )}}\n */\nexport interface LinkConfig {\n\t/**\n\t * Custom tooltip rendered when the user clicks a link.\n\t * Receives the href, display text, bounding rect (for positioning),\n\t * and ready-to-call action handlers.\n\t * Omit to use the minimal built-in tooltip.\n\t */\n\trenderTooltip?: (props: LinkTooltipProps) => React.ReactNode;\n}\n\n/**\n * Snapshot of all active marks and block types at the current cursor or selection.\n * Returned by onSelectionChange and ref.getActiveFormats().\n *\n * Keep this in component state and pass it to BikEditorToolbar (or your own toolbar)\n * to keep button active-states in sync.\n *\n * @example\n * const [formats, setFormats] = useState(DEFAULT_ACTIVE_FORMATS);\n * <BikEditor onSelectionChange={setFormats} ... />\n * <BikEditorToolbar activeFormats={formats} ... />\n */\nexport interface FormatState {\n\tbold: boolean;\n\titalic: boolean;\n\tunderline: boolean;\n\tstrike: boolean;\n\tbulletList: boolean;\n\torderedList: boolean;\n\tblockquote: boolean;\n\tcodeBlock: boolean;\n\t/** Non-null when the cursor is inside (or selection spans) a hyperlink. */\n\tlink: { href: string } | null;\n\ttextAlign: 'left' | 'center' | 'right' | 'justify' | null;\n\tfontFamily: string | null;\n\tfontSize: string | null;\n\t/** Active text colour as a CSS colour string, or null if none. */\n\tcolor: string | null;\n\t/** Active highlight (background) colour, or null if none. */\n\thighlight: string | null;\n\t/** True when the cursor/selection is in a superscript. */\n\tsuperscript: boolean;\n\t/** True when the cursor/selection is in a subscript. */\n\tsubscript: boolean;\n}\n\nexport const DEFAULT_FORMAT_STATE: FormatState = {\n\tbold: false,\n\titalic: false,\n\tunderline: false,\n\tstrike: false,\n\tbulletList: false,\n\torderedList: false,\n\tblockquote: false,\n\tcodeBlock: false,\n\tlink: null,\n\ttextAlign: null,\n\tfontFamily: null,\n\tfontSize: null,\n\tcolor: null,\n\thighlight: null,\n\tsuperscript: false,\n\tsubscript: false,\n};\n\n/**\n * All formatting and insertion commands. Access via `editorRef.current.actions`.\n *\n * Every method focuses the editor internally — no need to call `.focus()` first.\n *\n * @example\n * editorRef.current?.actions.toggleBold();\n * editorRef.current?.actions.setLink('https://example.com', 'Visit example');\n */\nexport interface EditorActions {\n\t// ── Basic marks ───────────────────────────────────────────────────────────\n\ttoggleBold: () => void;\n\ttoggleItalic: () => void;\n\ttoggleUnderline: () => void;\n\ttoggleStrike: () => void;\n\n\t// ── Blocks ────────────────────────────────────────────────────────────────\n\ttoggleBulletList: () => void;\n\ttoggleOrderedList: () => void;\n\ttoggleBlockquote: () => void;\n\ttoggleCodeBlock: () => void;\n\n\t// ── Alignment & typography ────────────────────────────────────────────────\n\tsetTextAlign: (align: 'left' | 'center' | 'right' | 'justify') => void;\n\t/** Pass a CSS font-family value, e.g. `'Inter, sans-serif'`. Only works when richTypography is on. */\n\tsetFontFamily: (font: string) => void;\n\t/** Pass a CSS size value, e.g. `'16px'` or `'1.25rem'`. Only works when richTypography is on. */\n\tsetFontSize: (size: string) => void;\n\n\t// ── Colour ────────────────────────────────────────────────────────────────\n\t/** Sets the text colour. Pass any CSS colour value, e.g. `'#e53e3e'`. */\n\tsetColor: (color: string) => void;\n\t/** Sets the highlight (background) colour. */\n\tsetHighlight: (color: string) => void;\n\t/** Removes the text colour mark from the current selection. */\n\tunsetColor: () => void;\n\t/** Removes the highlight mark from the current selection. */\n\tunsetHighlight: () => void;\n\n\t// ── Links ─────────────────────────────────────────────────────────────────\n\t/**\n\t * Applies a hyperlink to the current selection, or updates the link the\n\t * cursor is already inside.\n\t *\n\t * @param href The URL.\n\t * @param text Optional display text. When provided, the visible text of the\n\t * linked range is replaced atomically in the same undo step.\n\t *\n\t * @example\n\t * actions.setLink('https://example.com');\n\t * actions.setLink('https://example.com', 'Visit example');\n\t */\n\tsetLink: (href: string, text?: string) => void;\n\t/** Updates only the href of the link at the current cursor. */\n\tupdateLink: (href: string) => void;\n\t/** Removes the link mark but keeps the visible text. */\n\tremoveLink: () => void;\n\n\t// ── Insertions ────────────────────────────────────────────────────────────\n\t/** Inserts an emoji character at the cursor. */\n\tinsertEmoji: (emoji: string) => void;\n\t/**\n\t * Inserts a `{{variableName}}` token at the cursor.\n\t * The token renders with a distinct highlight inside the editor.\n\t */\n\tinsertVariable: (variableName: string) => void;\n\t/** Inserts arbitrary HTML at the current cursor position. */\n\tinsertHtml: (html: string) => void;\n\n\t// ── History ───────────────────────────────────────────────────────────────\n\tundo: () => void;\n\tredo: () => void;\n\tcanUndo: () => boolean;\n\tcanRedo: () => boolean;\n\n\t// ── Rich typography (richTypography feature required) ─────────────────────\n\t/** Toggles superscript on the selection. Only works when richTypography is on. */\n\ttoggleSuperscript: () => void;\n\t/** Toggles subscript on the selection. Only works when richTypography is on. */\n\ttoggleSubscript: () => void;\n\t/** Inserts an inline image at the cursor. Only works when richTypography is on. */\n\tinsertImage: (src: string) => void;\n}\n\n/** Props passed to your renderLinkTooltip render prop. */\nexport interface LinkTooltipProps {\n\t/** The href of the active link. */\n\thref: string;\n\t/** The display text of the link. */\n\ttext: string;\n\t/** Bounding rect of the link element — use this to position your popover. */\n\trect: DOMRect;\n\t/** Opens the link in a new tab. */\n\tonOpen: () => void;\n\t/** Removes the link mark but keeps the visible text. */\n\tonRemove: () => void;\n\t/** Hides the bubble menu. Call this before opening an edit modal so the tooltip doesn't linger. */\n\tonHide: () => void;\n}\n\n/**\n * Imperative handle exposed by BikEditor.\n *\n * Obtain via `useRef<BikEditorRef>()` and pass to `<BikEditor ref={...} />`.\n * All methods are safe to call after the first render.\n *\n * @example\n * const ref = useRef<BikEditorRef>(null);\n * <BikEditor ref={ref} ... />\n * ref.current?.clearContent();\n * ref.current?.actions.toggleBold();\n */\nexport interface BikEditorRef {\n\t// ── Focus ─────────────────────────────────────────────────────────────────\n\t/** Moves focus into the editor. Optionally pass a position: 'start', 'end', 'all', or a numeric offset. */\n\tfocus: (position?: 'start' | 'end' | 'all' | number | boolean | null) => void;\n\t/** Removes focus from the editor. */\n\tblur: () => void;\n\n\t// ── Content ───────────────────────────────────────────────────────────────\n\t/** Clears the entire document and fires onChange. */\n\tclearContent: () => void;\n\t/**\n\t * Replaces the full document with the given HTML and fires onChange.\n\t * Overwrites all sections. Use setSectionContent() / setBodyContent() to\n\t * update a single section without touching the others.\n\t */\n\tsetContent: (html: string) => void;\n\t/** Inserts HTML at the current cursor position without replacing anything. */\n\tinsertContent: (html: string) => void;\n\t/**\n\t * Inserts HTML inline at the current cursor position, unwrapping block\n\t * wrappers (e.g. `<p>`) so the content merges into the surrounding paragraph\n\t * instead of creating new blocks. Use for macros, quick replies, etc.\n\t */\n\tinsertInlineContent: (html: string) => void;\n\t/** Inserts HTML at the very start of the document. */\n\tinsertAtStart: (html: string) => void;\n\t/** Inserts HTML as a new block at the end of the document. */\n\tinsertBlock: (html: string) => void;\n\t/**\n\t * Appends HTML inline inside the last paragraph of the document.\n\t * Ideal for streaming — call repeatedly with each text chunk.\n\t * Use `appendBodyContent` instead if the editor has named sections.\n\t */\n\tappendInline: (html: string) => void;\n\t/** @deprecated Use `insertBlock` */\n\tinsertAtEnd: (html: string) => void;\n\t/** @deprecated Use `appendInline` */\n\tappendContent: (html: string) => void;\n\t/** Returns the full document as `{ html, text, isEmpty, characterCount }`. */\n\tgetContent: () => EditorSnapshot;\n\t/**\n\t * Returns the raw TipTap document as a plain JSON object.\n\t * Returns `null` when the editor is not yet mounted.\n\t */\n\tgetJSON: () => Record<string, unknown> | null;\n\t/**\n\t * Walks the document and returns all mention IDs currently in the editor,\n\t * split by type. IDs are deduplicated and returned as arrays.\n\t *\n\t * @example\n\t * const { agentIds, teamIds } = ref.current?.getMentions() ?? { agentIds: [], teamIds: [] };\n\t */\n\tgetMentions: () => { agentIds: string[]; teamIds: string[] };\n\n\t// ── Selection ─────────────────────────────────────────────────────────────\n\t/**\n\t * Returns the current cursor / selection position as a `{ from, to }` pair.\n\t * When nothing is selected, `from === to` (a collapsed cursor).\n\t * Pass the returned value to `insertAtPosition()` to insert at an exact spot.\n\t *\n\t * @example\n\t * const { from } = ref.current?.getCursorPosition() ?? { from: 0, to: 0 };\n\t * ref.current?.insertAtPosition(from, '<strong>inserted</strong>');\n\t */\n\tgetCursorPosition: () => { from: number; to: number };\n\t/**\n\t * Inserts HTML at an explicit document position rather than at the current cursor.\n\t * Use `getCursorPosition()` to get the position, or any value from\n\t * `findSectionStartPos` / `findSectionEndPos` if you need section-relative insertion.\n\t *\n\t * @example\n\t * const { from } = ref.current!.getCursorPosition();\n\t * ref.current?.insertAtPosition(from, '<em>hello</em>');\n\t */\n\tinsertAtPosition: (pos: number, html: string) => void;\n\t/**\n\t * Returns the currently selected plain text.\n\t * When the cursor is inside a link with nothing selected, returns the full\n\t * link text — useful for pre-filling a link modal's text field.\n\t */\n\tgetSelectedText: () => string;\n\n\t// ── Character count ───────────────────────────────────────────────────────\n\t/**\n\t * Returns the current character count and the configured limit (or null).\n\t * Use this to render your own character counter anywhere you like.\n\t *\n\t * @example\n\t * const { count, limit } = ref.current?.getCharacterCount() ?? { count: 0, limit: null };\n\t * <span style={{ color: count >= (limit ?? Infinity) ? 'red' : 'inherit' }}>\n\t * {count}{limit != null ? ` / ${limit}` : ''}\n\t * </span>\n\t */\n\tgetCharacterCount: () => { count: number; limit: number | null };\n\n\t// ── Sections ─────────────────────────────────────────────────────────────\n\t/**\n\t * Returns content for a named section.\n\t * Use `'body'` to get the main content above all section dividers.\n\t *\n\t * @example\n\t * const sig = ref.current?.getSectionContent('signature');\n\t */\n\tgetSectionContent: (sectionId: string) => EditorSnapshot;\n\t/**\n\t * Replaces the content of a named section without touching other sections.\n\t * If the section doesn't exist yet it is appended at the end.\n\t *\n\t * @example\n\t * ref.current?.setSectionContent('signature', '<p>New signature</p>');\n\t * ref.current?.setSectionContent('body', rephrased); // same as setBodyContent\n\t */\n\tsetSectionContent: (sectionId: string, html: string) => void;\n\t/**\n\t * Moves the cursor to the start of a named section.\n\t * Use `'body'` to focus the main content area.\n\t */\n\tfocusSection: (sectionId: string) => void;\n\t/** Clears all content in a named section (replaces with an empty paragraph). */\n\tclearSection: (sectionId: string) => void;\n\n\t// ── Convenience body shorthands ───────────────────────────────────────────\n\t/** Shorthand for `getSectionContent('body')`. */\n\tgetBodyContent: () => EditorSnapshot;\n\t/** Shorthand for `setSectionContent('body', html)`. */\n\tsetBodyContent: (html: string) => void;\n\t/**\n\t * Atomically sets the body AND all sections in a single editor update.\n\t * Sections are laid out in the exact order supplied — use this instead of\n\t * multiple `setSectionContent()` calls whenever order matters (e.g. forward\n\t * mail where signature must appear before the forwarded thread).\n\t *\n\t * @example\n\t * ref.current?.setBodyAndSections('<p></p>', [\n\t * { id: 'signature', content: '<p>Alice</p>' },\n\t * { id: 'forwarded', content: '<p>--- Forwarded ---</p>' },\n\t * ]);\n\t */\n\tsetBodyAndSections: (\n\t\tbody: string,\n\t\tsections: Array<{ id: string; content: string }>,\n\t) => void;\n\t/**\n\t * Appends HTML to the end of the body section without moving the cursor.\n\t * Ideal for streaming AI-generated content — call repeatedly with each chunk.\n\t * Does NOT clear existing content; call `setBodyContent('')` first if needed.\n\t *\n\t * @example\n\t * // Stream AI response chunk-by-chunk\n\t * ref.current?.setBodyContent('');\n\t * for await (const chunk of aiStream) {\n\t * ref.current?.appendBodyContent(chunk);\n\t * }\n\t */\n\tappendBodyContent: (html: string) => void;\n\n\t/**\n\t * Inserts HTML at the start of a named section (or `'body'`).\n\t * Inserts at the very start of the document when `sectionId` is `'body'`.\n\t */\n\tinsertAtSectionStart: (sectionId: string, html: string) => void;\n\t/**\n\t * Inserts HTML at the end of a named section (or `'body'`).\n\t * For `'body'`, inserts just before the first section divider (or at end of doc).\n\t */\n\tinsertAtSectionEnd: (sectionId: string, html: string) => void;\n\n\t// ── Formats & actions ─────────────────────────────────────────────────────\n\t/** Returns a snapshot of all active marks at the current cursor or selection. */\n\tgetActiveFormats: () => FormatState;\n\t/** All formatting and insertion commands. See EditorActions. */\n\tactions: EditorActions;\n}\n\n/**\n * Props for `<BikEditor />`. All props are optional.\n */\nexport interface BikEditorProps {\n\t// ── Features ──────────────────────────────────────────────────────────────\n\t/**\n\t * Opt in to specific editor capabilities. Both default to `false`.\n\t *\n\t * @example\n\t * <BikEditor features={{ richPaste: true, richTypography: true }} />\n\t */\n\tfeatures?: EditorFeatures;\n\n\t// ── Content ───────────────────────────────────────────────────────────────\n\t/**\n\t * HTML loaded on mount. Ignored after first render.\n\t * Use `ref.setContent()` to change content imperatively later.\n\t */\n\tinitialContent?: string;\n\t/**\n\t * Named sections placed below the main body.\n\t * Each section is separated from the previous one by an invisible divider node.\n\t * The array order determines the visual order in the document.\n\t *\n\t * These are only the INITIAL values — use `ref.setSectionContent()` to update\n\t * a section after mount without re-mounting the editor.\n\t *\n\t * @example\n\t * sections={[\n\t * { id: 'signature', content: '<p>Best regards, Alice</p>' },\n\t * { id: 'forwarded', content: '<p>--- Forwarded message ---</p>' },\n\t * ]}\n\t */\n\tsections?: EditorSection[];\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n\n\t// ── Behaviour ─────────────────────────────────────────────────────────────\n\t/** Makes the editor read-only. The toolbar still renders but actions are no-ops. */\n\tdisabled?: boolean;\n\t/**\n\t * Hard character limit. The editor refuses to accept input beyond this point.\n\t * The limit is enforced silently — no UI is rendered. Use\n\t * `ref.getCharacterCount()` or `onChange.characterCount` to display it yourself.\n\t */\n\tmaxCharacters?: number;\n\n\t// ── Keyboard shortcuts ────────────────────────────────────────────────────\n\t/**\n\t * Custom keyboard shortcuts. BikEditor registers no shortcuts of its own —\n\t * every shortcut must be explicitly listed here.\n\t *\n\t * @example\n\t * shortcuts={[\n\t * { key: 'Enter', modifiers: ['mod'], handler: handleSend },\n\t * { key: 's', modifiers: ['mod', 'shift'], handler: handleDraft },\n\t * ]}\n\t */\n\tshortcuts?: KeyboardShortcut[];\n\t/**\n\t * Shortcut key that fires `onSend`. No key is bound to `onSend` unless you\n\t * set this explicitly. Requires `onSend` to also be provided.\n\t *\n\t * `'mod'` maps to Cmd on macOS and Ctrl on Windows/Linux.\n\t *\n\t * @example\n\t * // Single shortcut\n\t * sendShortcut={{ key: 'Enter', modifiers: ['mod'] }}\n\t * // Multiple shortcuts\n\t * sendShortcut={[\n\t * { key: 'Enter', modifiers: ['mod'] },\n\t * { key: 's', modifiers: ['mod'] },\n\t * ]}\n\t */\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\n\t// ── Mentions ─────────────────────────────────────────────────────────────\n\t/**\n\t * Enables `@` (agent) and `#` (team) mention dropdowns.\n\t * Bundle all mention-related config in one object so it's easy to find.\n\t *\n\t * @example\n\t * mentions={{\n\t * agents: myAgents,\n\t * teams: myTeams,\n\t * onSelect: (item, char) => track(char, item.id),\n\t * }}\n\t */\n\tmentions?: MentionConfig;\n\n\t// ── Slash commands ────────────────────────────────────────────────────────\n\t/**\n\t * Enables the `/` slash-command menu.\n\t * Bundle all slash-command config in one object.\n\t *\n\t * @example\n\t * slashCommands={{\n\t * items: MY_COMMANDS,\n\t * onSelect: (cmd) => handleCommand(cmd.id),\n\t * }}\n\t */\n\tslashCommands?: SlashCommandConfig;\n\n\t// ── Link ─────────────────────────────────────────────────────────────────\n\t/**\n\t * Link-related configuration — currently just the click tooltip.\n\t *\n\t * @example\n\t * link={{ renderTooltip: ({ href, onOpen, onRemove }) => (\n\t * <MyTooltip href={href} onOpen={onOpen} onRemove={onRemove} />\n\t * )}}\n\t */\n\tlink?: LinkConfig;\n\n\t// ── Callbacks ─────────────────────────────────────────────────────────────\n\t/**\n\t * Called once, immediately after the editor finishes initializing.\n\t * Any imperative ref methods called before this fires are queued internally\n\t * and executed automatically once the editor is ready — consumers do not\n\t * need timers or readiness guards.\n\t *\n\t * @example\n\t * <BikEditor\n\t * ref={editorRef}\n\t * onReady={() => editorRef.current?.setContent(savedDraft)}\n\t * />\n\t */\n\tonReady?: () => void;\n\t/** Called on every document change. */\n\tonChange?: (content: EditorSnapshot) => void;\n\t/**\n\t * Called when the user presses the key combo defined by `sendShortcut`.\n\t * Only fires when both `onSend` and `sendShortcut` are provided.\n\t */\n\tonSend?: (content: EditorSnapshot) => void;\n\t/** Called when the editor gains focus. */\n\tonFocus?: () => void;\n\t/** Called when the editor loses focus. */\n\tonBlur?: () => void;\n\t/**\n\t * Called on every paste event. Return `true` to fully suppress BikEditor's\n\t * own paste handling (useful when you want to handle images or HTML yourself).\n\t *\n\t * @example\n\t * onPaste={({ images }) => {\n\t * if (images.length) {\n\t * uploadImages(images).then(urls => {\n\t * urls.forEach(url => ref.current?.actions.insertHtml(`<img src=\"${url}\" />`));\n\t * });\n\t * return true; // suppress default — don't embed the raw image data\n\t * }\n\t * }}\n\t */\n\tonPaste?: (data: PasteData) => boolean | void;\n\t/**\n\t * Called on every cursor move or selection change with a snapshot of all\n\t * active marks. Keep in state and pass to your toolbar to sync button states.\n\t */\n\tonSelectionChange?: (activeFormats: FormatState) => void;\n\n\t// ── Styling ───────────────────────────────────────────────────────────────\n\t// Outer wrapper — use these to control the shell (border, border-radius,\n\t// background, shadow, etc.).\n\t/** Extra class name applied to the outer wrapper div. */\n\tclassName?: string;\n\t/** Inline styles applied to the outer wrapper div. */\n\tstyle?: React.CSSProperties;\n\n\t// Inner editable area — use these to control the content div itself\n\t// (padding, font, line-height, etc.).\n\t/** Extra class name applied to the content-editable area (`.ProseMirror` in TipTap). */\n\teditorClassName?: string;\n\t/** Inline styles applied to the content-editable area. */\n\teditorStyle?: React.CSSProperties;\n\n\t// Sizing — applied to the content-editable area.\n\t/** Minimum height of the editable area. CSS value, e.g. `'80px'`. */\n\tminHeight?: string;\n\t/** Maximum height. The editor scrolls internally when exceeded. CSS value, e.g. `'400px'`. */\n\tmaxHeight?: string;\n\t/** Gap between consecutive paragraphs. CSS value, defaults to `'4px'`. */\n\tparagraphGap?: string;\n}\n"],"names":["DEFAULT_FORMAT_STATE","bold","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","textAlign","fontFamily","fontSize","color","highlight","superscript","subscript"],"mappings":"AAgUO,MAAMA,EAAoC,CAChDC,MAAM,EACNC,QAAQ,EACRC,WAAW,EACXC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,YAAY,EACZC,WAAW,EACXC,KAAM,KACNC,UAAW,KACXC,WAAY,KACZC,SAAU,KACVC,MAAO,KACPC,UAAW,KACXC,aAAa,EACbC,WAAW"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{DOMSerializer as t,DOMParser as e}from"../node_modules/prosemirror-model/dist/index.js";import{normalizeHardBreaks as n}from"./extensions/plainClipboard/pasteUtils.js";function i(t){
|
|
1
|
+
import{DOMSerializer as t,DOMParser as e}from"../node_modules/prosemirror-model/dist/index.js";import{normalizeHardBreaks as n}from"./extensions/plainClipboard/pasteUtils.js";function i(t){return t.replace(/<\/?span[^>]*>/gi,"").replace(/(<p[^>]*>)\s*<br\s*\/?>\s*(<\/p>)/gi,"$1$2")}function r(t){const e=document.createElement("div");e.innerHTML=t;const n=e.querySelectorAll("p");for(const t of Array.from(n)){if(0===t.childNodes.length){t.appendChild(document.createElement("br"));continue}t.lastChild instanceof HTMLBRElement&&t.childNodes.length>1&&t.appendChild(document.createElement("br"))}return e.innerHTML}function o(t,n){const r=i(n),o=document.createElement("div");o.innerHTML=r;const s=e.fromSchema(t.schema).parseSlice(o);if(0===s.size)return;const{from:c,to:l}=t.state.selection,d=t.state.tr.replaceRange(c,l,s);t.view.dispatch(d)}const s=t=>`<div data-section-divider="${t}" style="display:none;height:0;line-height:0;overflow:hidden;"></div>`;function c(t,e){let n=t;for(const t of e)n+=s(t.id)+t.content;return n}function l(t){var e;if(!t)return"";return null!==(e=u(t).get("body"))&&void 0!==e?e:t}function d(t){if(!t)return"";const e=l(t);return t.substring(e.length)}function u(t){const e=new Map,n=/<div[^>]*data-section-divider="([^"]*)[^>]*>\s*<\/div>/g,i=[];let r;for(;null!==(r=n.exec(t));)i.push({id:r[1],index:r.index,endIndex:r.index+r[0].length});if(0===i.length)return e.set("body",t),e;e.set("body",t.slice(0,i[0].index));for(let n=0;n<i.length;n++){const r=i[n].endIndex,o=n+1<i.length?i[n+1].index:t.length;e.set(i[n].id,t.slice(r,o))}return e}function a(e,n){var i;const o=p(e,n);if(-1===o)return{html:"",text:"",isEmpty:!0,characterCount:0};const s=g(e,n),c=e.state.doc.slice(o,s),l=document.createElement("div"),d=t.fromSchema(e.schema);l.appendChild(d.serializeFragment(c.content));const u=r(l.innerHTML),a=null!==(i=l.textContent)&&void 0!==i?i:"";return{html:u,text:a,isEmpty:!a.trim(),characterCount:a.length}}function f(t,n,i){const r=p(t,n);if(-1===r){const e=t.state.doc.content.size,r=s(n)+i;return void t.commands.insertContentAt(e,r,{updateSelection:!1})}const o=g(t,n),c=function(t,n){const i=document.createElement("div");return i.innerHTML=n,e.fromSchema(t.schema).parse(i).content}(t,i),{tr:l}=t.state,d="body"===n?0:r;l.replaceWith(d,o,c),l.setMeta("addToHistory",!1),t.view.dispatch(l)}function h(t){return a(t,"body")}function m(t){var e,n,i,r;return{bold:t.isActive("bold"),italic:t.isActive("italic"),underline:t.isActive("underline"),strike:t.isActive("strike"),bulletList:t.isActive("bulletList"),orderedList:t.isActive("orderedList"),blockquote:t.isActive("blockquote"),codeBlock:t.isActive("codeBlock"),link:(()=>{var e,n,i;if(t.isActive("link"))return{href:null!==(e=t.getAttributes("link").href)&&void 0!==e?e:""};const{$from:r}=t.state.selection;if(r.pos>0){const t=r.doc.resolve(r.pos).marks().find((t=>"link"===t.type.name));if(t)return{href:null!==(i=t.attrs.href)&&void 0!==i?i:""};{const t=r.nodeBefore,e=null==t?void 0:t.marks.find((t=>"link"===t.type.name));if(e)return{href:null!==(n=e.attrs.href)&&void 0!==n?n:""}}}return null})(),textAlign:t.isActive({textAlign:"center"})?"center":t.isActive({textAlign:"right"})?"right":t.isActive({textAlign:"justify"})?"justify":t.isActive({textAlign:"left"})?"left":null,fontFamily:null!==(e=t.getAttributes("textStyle").fontFamily)&&void 0!==e?e:null,fontSize:null!==(n=t.getAttributes("textStyle").fontSize)&&void 0!==n?n:null,color:null!==(i=t.getAttributes("textStyle").color)&&void 0!==i?i:null,highlight:t.isActive("highlight")?null!==(r=t.getAttributes("highlight").color)&&void 0!==r?r:"default":null,superscript:t.isActive("superscript"),subscript:t.isActive("subscript")}}function v(t){const e=r(t.getHTML()),i=n(t.state.doc.content),o=t.schema.node("doc",null,i),s=o.textBetween(0,o.content.size,"\n","\n");return{html:e,text:s,isEmpty:t.isEmpty,characterCount:s.length}}function p(t,e){if("body"===e)return 1;let n=-1;return t.state.doc.descendants(((t,i)=>{if(-1!==n)return!1;"sectionDivider"===t.type.name&&t.attrs.sectionId===e&&(n=i+t.nodeSize)})),n}function g(t,e){const n=t.state.doc,i=n.content.size;if("body"===e){let t=-1;return n.descendants(((e,n)=>{if(-1!==t)return!1;"sectionDivider"===e.type.name&&(t=n)})),-1!==t?t:i}let r=!1,o=-1;return n.descendants(((t,n)=>-1===o&&(r?void("sectionDivider"===t.type.name&&(o=n)):("sectionDivider"===t.type.name&&t.attrs.sectionId===e&&(r=!0),!1)))),-1!==o?o:i}export{s as SECTION_DIVIDER_HTML,c as buildSectionedContent,m as extractActiveFormats,u as extractAllSectionsFromHtml,h as extractBodyContent,v as extractContent,a as extractSectionContent,g as findSectionEndPos,p as findSectionStartPos,l as getBodyHtml,d as getSectionsHtml,o as insertInlineHtml,i as normalizeHtml,f as setSectionContentInEditor,r as toPortableHtml};
|
|
2
2
|
//# sourceMappingURL=BikEditor.utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BikEditor.utils.js","sources":["../../../src/editor/BikEditor.utils.ts"],"sourcesContent":["import { Editor } from '@tiptap/core';\nimport { DOMParser, DOMSerializer } from 'prosemirror-model';\nimport { EditorSnapshot, FormatState } from './BikEditor.types';\nimport { normalizeHardBreaks } from './extensions/plainClipboard/pasteUtils';\n\n// ---------------------------------------------------------------------------\n// HTML normalisation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalise HTML before passing it to TipTap.\n *\n * 1. Unwrap style-less `<span>` wrappers (plain spans, and editor-cursor spans\n * like Quill's `<span class=\"ql-cursor\">`). Spans that carry inline styling\n * (`color` / `font-family` / `font-size` render as styled spans) or that\n * represent a `{{variable}}` token are PRESERVED — otherwise a setContent\n * round-trip would silently drop colour/font formatting that paste keeps.\n * 2. Collapse placeholder paragraphs (`<p><br></p>`) to `<p></p>`. ProseMirror\n * parses `<p></p>` as an empty paragraph and adds its own DOM `<br>` for the\n * cursor — one blank line, correct height. Leaving the source `<br>` would\n * create a `hard_break` child AND the cursor `<br>` → double-height blank line.\n *\n * Uses the DOM (regex cannot selectively pair span tags). `normalizeHtml` runs\n * in the component body for `initialContent`, so it is guarded for SSR: the\n * editor is created lazily on the client (`immediatelyRender: false`), meaning\n * the value is only parsed client-side and passing through on the server is safe.\n */\nexport function normalizeHtml(html: string): string {\n\tif (!html || typeof document === 'undefined') return html;\n\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\n\tcontainer.querySelectorAll('span').forEach((span) => {\n\t\tif (span.hasAttribute('style') || span.hasAttribute('data-variable'))\n\t\t\treturn;\n\t\tconst parent = span.parentNode;\n\t\tif (!parent) return;\n\t\twhile (span.firstChild) parent.insertBefore(span.firstChild, span);\n\t\tparent.removeChild(span);\n\t});\n\n\tcontainer.querySelectorAll('p').forEach((p) => {\n\t\tif (\n\t\t\tp.querySelectorAll('br').length === 1 &&\n\t\t\t(p.textContent ?? '').trim() === ''\n\t\t) {\n\t\t\tp.innerHTML = '';\n\t\t}\n\t});\n\n\treturn container.innerHTML;\n}\n\n/**\n * Make editor HTML portable across email clients and chat renderers.\n *\n * 1. Double a trailing `<br>` in content paragraphs so the blank line is\n * visible. A single trailing `<br>` before `</p>` is swallowed by most\n * email clients; `<br><br>` renders as a visible blank line (matches\n * Gmail's output).\n * 2. Convert empty `<p></p>` to `<p><br></p>` — email clients collapse\n * zero-height paragraphs without a `<br>`.\n */\nexport function toPortableHtml(html: string): string {\n\tconst div = document.createElement('div');\n\tdiv.innerHTML = html;\n\tconst blocks = div.querySelectorAll('p');\n\tfor (const p of Array.from(blocks)) {\n\t\tif (p.childNodes.length === 0) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t\tcontinue;\n\t\t}\n\t\tconst last = p.lastChild;\n\t\tif (last instanceof HTMLBRElement && p.childNodes.length > 1) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t}\n\t}\n\treturn div.innerHTML;\n}\n\n// ---------------------------------------------------------------------------\n// Inline content insertion\n// ---------------------------------------------------------------------------\n\n/**\n * Insert HTML at the current cursor position, merging the first paragraph\n * inline while preserving the block structure of subsequent paragraphs/lists.\n *\n * Uses `parseSlice` which returns an open-ended Slice — the first block merges\n * at the cursor, middle blocks stay intact, and the last block merges with any\n * text that follows the cursor.\n */\nexport function insertInlineHtml(editor: Editor, html: string): void {\n\tconst normalised = normalizeHtml(html);\n\tconst dom = document.createElement('div');\n\tdom.innerHTML = normalised;\n\tconst slice = DOMParser.fromSchema(editor.schema).parseSlice(dom);\n\n\tif (slice.size === 0) return;\n\n\tconst { from, to } = editor.state.selection;\n\tconst tr = editor.state.tr.replaceRange(from, to, slice);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Section divider HTML\n// ---------------------------------------------------------------------------\n\nexport const SECTION_DIVIDER_HTML = (id: string) =>\n\t`<div data-section-divider=\"${id}\" style=\"display:none;height:0;line-height:0;overflow:hidden;\"></div>`;\n\n/**\n * Build initial HTML for an editor with one or more named sections below the body.\n * Each section is separated by an invisible divider node.\n *\n * @example\n * buildSectionedContent('<p>Body</p>', [\n * { id: 'signature', content: '<p>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- fwd ---</p>' },\n * ])\n */\nexport function buildSectionedContent(\n\tbody: string,\n\tsections: Array<{ id: string; content: string }>,\n): string {\n\tlet html = body;\n\tfor (const s of sections) {\n\t\thtml += SECTION_DIVIDER_HTML(s.id) + s.content;\n\t}\n\treturn html;\n}\n\n// ---------------------------------------------------------------------------\n// Section extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the editor HTML into a Map of sectionId → raw HTML string.\n * The special key `'body'` always holds the content before the first divider.\n * Map iteration order matches document order.\n */\nexport function extractAllSections(editor: Editor): Map<string, string> {\n\treturn extractAllSectionsFromHtml(editor.getHTML());\n}\n\n/**\n * Extract just the body portion (everything before the first section divider)\n * from an HTML string produced by BikEditor. Safe to call on plain HTML too —\n * returns the full string when no dividers are present.\n */\nexport function getBodyHtml(html: string): string {\n\tif (!html) return '';\n\tconst sections = extractAllSectionsFromHtml(html);\n\treturn sections.get('body') ?? html;\n}\n\n/**\n * Extract everything from the first section divider onward (dividers + their\n * content). Returns an empty string when there are no sections.\n */\nexport function getSectionsHtml(html: string): string {\n\tif (!html) return '';\n\tconst body = getBodyHtml(html);\n\treturn html.substring(body.length);\n}\n\nexport function extractAllSectionsFromHtml(html: string): Map<string, string> {\n\tconst sections = new Map<string, string>();\n\tconst dividerRegex =\n\t\t/<div[^>]*data-section-divider=\"([^\"]*)[^>]*>\\s*<\\/div>/g;\n\tconst dividers: Array<{ id: string; index: number; endIndex: number }> = [];\n\n\tlet match: RegExpExecArray | null;\n\twhile ((match = dividerRegex.exec(html)) !== null) {\n\t\tdividers.push({\n\t\t\tid: match[1],\n\t\t\tindex: match.index,\n\t\t\tendIndex: match.index + match[0].length,\n\t\t});\n\t}\n\n\tif (dividers.length === 0) {\n\t\tsections.set('body', html);\n\t\treturn sections;\n\t}\n\n\tsections.set('body', html.slice(0, dividers[0].index));\n\tfor (let i = 0; i < dividers.length; i++) {\n\t\tconst start = dividers[i].endIndex;\n\t\tconst end = i + 1 < dividers.length ? dividers[i + 1].index : html.length;\n\t\tsections.set(dividers[i].id, html.slice(start, end));\n\t}\n\n\treturn sections;\n}\n\n/** Returns BikEditorContent for a single named section (or 'body'). */\nexport function extractSectionContent(\n\teditor: Editor,\n\tid: string,\n): EditorSnapshot {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1)\n\t\treturn { html: '', text: '', isEmpty: true, characterCount: 0 };\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst slice = editor.state.doc.slice(startPos, endPos);\n\tconst container = document.createElement('div');\n\tconst serializer = DOMSerializer.fromSchema(editor.schema);\n\tcontainer.appendChild(serializer.serializeFragment(slice.content));\n\tconst html = toPortableHtml(container.innerHTML);\n\tconst text = container.textContent ?? '';\n\treturn { html, text, isEmpty: !text.trim(), characterCount: text.length };\n}\n\n/**\n * Parse an HTML string into ProseMirror nodes using the editor's schema.\n */\nfunction parseHtmlToNodes(editor: Editor, html: string) {\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\treturn DOMParser.fromSchema(editor.schema).parse(container).content;\n}\n\n/**\n * Replace the content of a single named section without touching others.\n * Uses a ProseMirror transaction to replace only the target range,\n * preserving undo history and other sections' state.\n * If the section doesn't exist yet, it is appended at the end of the document.\n */\nexport function setSectionContentInEditor(\n\teditor: Editor,\n\tid: string,\n\thtml: string,\n): void {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1) {\n\t\tconst endPos = editor.state.doc.content.size;\n\t\tconst dividerHtml = SECTION_DIVIDER_HTML(id) + html;\n\t\teditor.commands.insertContentAt(endPos, dividerHtml, {\n\t\t\tupdateSelection: false,\n\t\t});\n\t\treturn;\n\t}\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst fragment = parseHtmlToNodes(editor, html);\n\tconst { tr } = editor.state;\n\t// For body, replace from position 0 (before the first paragraph's opening\n\t// boundary) so ProseMirror swaps entire block nodes cleanly. Position 1\n\t// (inside the paragraph) would force ProseMirror to close the open paragraph\n\t// first, creating a ghost empty <p></p>.\n\tconst replaceFrom = id === 'body' ? 0 : startPos;\n\ttr.replaceWith(replaceFrom, endPos, fragment);\n\ttr.setMeta('addToHistory', false);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Convenience shorthands (body = first section)\n// ---------------------------------------------------------------------------\n\n/** Extract BikEditorContent for just the body (before the first divider). */\nexport function extractBodyContent(editor: Editor): EditorSnapshot {\n\treturn extractSectionContent(editor, 'body');\n}\n\n// ---------------------------------------------------------------------------\n// Active formats\n// ---------------------------------------------------------------------------\n\nexport function extractActiveFormats(editor: Editor): FormatState {\n\treturn {\n\t\tbold: editor.isActive('bold'),\n\t\titalic: editor.isActive('italic'),\n\t\tunderline: editor.isActive('underline'),\n\t\tstrike: editor.isActive('strike'),\n\t\tbulletList: editor.isActive('bulletList'),\n\t\torderedList: editor.isActive('orderedList'),\n\t\tblockquote: editor.isActive('blockquote'),\n\t\tcodeBlock: editor.isActive('codeBlock'),\n\t\tlink: (() => {\n\t\t\tif (editor.isActive('link')) {\n\t\t\t\treturn { href: editor.getAttributes('link')['href'] ?? '' };\n\t\t\t}\n\t\t\tconst { $from } = editor.state.selection;\n\t\t\tif ($from.pos > 0) {\n\t\t\t\tconst before = $from.doc.resolve($from.pos);\n\t\t\t\tconst linkMark = before.marks().find((m) => m.type.name === 'link');\n\t\t\t\tif (!linkMark) {\n\t\t\t\t\tconst nodeBefore = $from.nodeBefore;\n\t\t\t\t\tconst markOnPrev = nodeBefore?.marks.find(\n\t\t\t\t\t\t(m) => m.type.name === 'link',\n\t\t\t\t\t);\n\t\t\t\t\tif (markOnPrev) {\n\t\t\t\t\t\treturn { href: markOnPrev.attrs['href'] ?? '' };\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn { href: linkMark.attrs['href'] ?? '' };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t})(),\n\t\ttextAlign: editor.isActive({ textAlign: 'center' })\n\t\t\t? 'center'\n\t\t\t: editor.isActive({ textAlign: 'right' })\n\t\t\t? 'right'\n\t\t\t: editor.isActive({ textAlign: 'justify' })\n\t\t\t? 'justify'\n\t\t\t: editor.isActive({ textAlign: 'left' })\n\t\t\t? 'left'\n\t\t\t: null,\n\t\tfontFamily: editor.getAttributes('textStyle')['fontFamily'] ?? null,\n\t\tfontSize: editor.getAttributes('textStyle')['fontSize'] ?? null,\n\t\tcolor: editor.getAttributes('textStyle')['color'] ?? null,\n\t\thighlight: editor.isActive('highlight')\n\t\t\t? editor.getAttributes('highlight')['color'] ?? 'default'\n\t\t\t: null,\n\t\tsuperscript: editor.isActive('superscript'),\n\t\tsubscript: editor.isActive('subscript'),\n\t};\n}\n\nexport function extractContent(editor: Editor): EditorSnapshot {\n\tconst html = toPortableHtml(editor.getHTML());\n\tconst normalized = normalizeHardBreaks(editor.state.doc.content);\n\tconst tempDoc = editor.schema.node('doc', null, normalized);\n\t// Paragraph boundary → blank line (\\n\\n), soft line break (hardBreak) → \\n.\n\t// Matches Gmail's plain-text output for the same content.\n\tconst text = tempDoc.textBetween(0, tempDoc.content.size, '\\n\\n', '\\n');\n\treturn {\n\t\thtml,\n\t\ttext,\n\t\tisEmpty: editor.isEmpty,\n\t\tcharacterCount: text.length,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Section position helpers (used by insertAtSectionStart/End and appendBodyContent)\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the ProseMirror position immediately after a section's opening divider\n * (i.e. the start of the section's own content).\n * For `'body'`, returns 1 (beginning of the document).\n * Returns -1 if the named section's divider is not found.\n */\nexport function findSectionStartPos(editor: Editor, sectionId: string): number {\n\tif (sectionId === 'body') return 1;\n\tlet startPos = -1;\n\teditor.state.doc.descendants((node, pos) => {\n\t\tif (startPos !== -1) return false;\n\t\tif (\n\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t) {\n\t\t\tstartPos = pos + node.nodeSize; // position right after the divider\n\t\t}\n\t});\n\treturn startPos;\n}\n\n/**\n * Returns the ProseMirror position at the end of a named section's content:\n * - For `'body'`: position of the first section divider, or end of document.\n * - For a named section: position of the next divider, or end of document.\n */\nexport function findSectionEndPos(editor: Editor, sectionId: string): number {\n\tconst doc = editor.state.doc;\n\tconst docSize = doc.content.size;\n\n\tif (sectionId === 'body') {\n\t\tlet firstDividerPos = -1;\n\t\tdoc.descendants((node, pos) => {\n\t\t\tif (firstDividerPos !== -1) return false;\n\t\t\tif (node.type.name === 'sectionDivider') {\n\t\t\t\tfirstDividerPos = pos;\n\t\t\t}\n\t\t});\n\t\treturn firstDividerPos !== -1 ? firstDividerPos : docSize;\n\t}\n\n\tlet passedSection = false;\n\tlet nextDividerPos = -1;\n\tdoc.descendants((node, pos) => {\n\t\tif (nextDividerPos !== -1) return false;\n\t\tif (!passedSection) {\n\t\t\tif (\n\t\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t\t) {\n\t\t\t\tpassedSection = true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (node.type.name === 'sectionDivider') {\n\t\t\tnextDividerPos = pos;\n\t\t}\n\t});\n\treturn nextDividerPos !== -1 ? nextDividerPos : docSize;\n}\n"],"names":["normalizeHtml","html","document","container","createElement","innerHTML","querySelectorAll","forEach","span","hasAttribute","parent","parentNode","firstChild","insertBefore","removeChild","p","length","_a","textContent","trim","toPortableHtml","div","blocks","Array","from","childNodes","appendChild","lastChild","HTMLBRElement","insertInlineHtml","editor","normalised","dom","slice","DOMParser","fromSchema","schema","parseSlice","size","to","state","selection","tr","replaceRange","view","dispatch","SECTION_DIVIDER_HTML","id","buildSectionedContent","body","sections","s","content","getBodyHtml","extractAllSectionsFromHtml","get","getSectionsHtml","substring","Map","dividerRegex","dividers","match","exec","push","index","endIndex","set","i","start","end","extractSectionContent","startPos","findSectionStartPos","text","isEmpty","characterCount","endPos","findSectionEndPos","doc","serializer","DOMSerializer","serializeFragment","setSectionContentInEditor","dividerHtml","commands","insertContentAt","updateSelection","fragment","parse","parseHtmlToNodes","replaceFrom","replaceWith","setMeta","extractBodyContent","extractActiveFormats","bold","isActive","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","href","getAttributes","$from","pos","linkMark","resolve","marks","find","m","type","name","_c","attrs","nodeBefore","markOnPrev","_b","textAlign","fontFamily","fontSize","color","highlight","_d","superscript","subscript","extractContent","getHTML","normalized","normalizeHardBreaks","tempDoc","node","textBetween","sectionId","descendants","nodeSize","docSize","firstDividerPos","passedSection","nextDividerPos"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BikEditor.utils.js","sources":["../../../src/editor/BikEditor.utils.ts"],"sourcesContent":["import { Editor } from '@tiptap/core';\nimport { DOMParser, DOMSerializer } from 'prosemirror-model';\nimport { EditorSnapshot, FormatState } from './BikEditor.types';\nimport { normalizeHardBreaks } from './extensions/plainClipboard/pasteUtils';\n\n// ---------------------------------------------------------------------------\n// HTML normalisation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalise HTML before passing it to TipTap.\n *\n * 1. Strip all `<span>` tags first (keeps inner content). This exposes\n * bare `<br>` inside otherwise-empty paragraphs like Quill's\n * `<p><span class=\"ql-cursor\"><br></span></p>`.\n * 2. Then convert `<p><br></p>` → `<p></p>`. ProseMirror parses `<p></p>`\n * as an empty paragraph node and adds its own DOM `<br>` for cursor\n * positioning — one blank line, correct height. Leaving the `<br>`\n * would create a `hard_break` child AND the cursor `<br>`, producing\n * double-height blank lines.\n */\nexport function normalizeHtml(html: string): string {\n\treturn html\n\t\t.replace(/<\\/?span[^>]*>/gi, '')\n\t\t.replace(/(<p[^>]*>)\\s*<br\\s*\\/?>\\s*(<\\/p>)/gi, '$1$2');\n}\n\n/**\n * Make editor HTML portable across email clients and chat renderers.\n *\n * 1. Double a trailing `<br>` in content paragraphs so the blank line is\n * visible. A single trailing `<br>` before `</p>` is swallowed by most\n * email clients; `<br><br>` renders as a visible blank line (matches\n * Gmail's output).\n * 2. Convert empty `<p></p>` to `<p><br></p>` — email clients collapse\n * zero-height paragraphs without a `<br>`.\n */\nexport function toPortableHtml(html: string): string {\n\tconst div = document.createElement('div');\n\tdiv.innerHTML = html;\n\tconst blocks = div.querySelectorAll('p');\n\tfor (const p of Array.from(blocks)) {\n\t\tif (p.childNodes.length === 0) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t\tcontinue;\n\t\t}\n\t\tconst last = p.lastChild;\n\t\tif (last instanceof HTMLBRElement && p.childNodes.length > 1) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t}\n\t}\n\treturn div.innerHTML;\n}\n\n// ---------------------------------------------------------------------------\n// Inline content insertion\n// ---------------------------------------------------------------------------\n\n/**\n * Insert HTML at the current cursor position, merging the first paragraph\n * inline while preserving the block structure of subsequent paragraphs/lists.\n *\n * Uses `parseSlice` which returns an open-ended Slice — the first block merges\n * at the cursor, middle blocks stay intact, and the last block merges with any\n * text that follows the cursor.\n */\nexport function insertInlineHtml(editor: Editor, html: string): void {\n\tconst normalised = normalizeHtml(html);\n\tconst dom = document.createElement('div');\n\tdom.innerHTML = normalised;\n\tconst slice = DOMParser.fromSchema(editor.schema).parseSlice(dom);\n\n\tif (slice.size === 0) return;\n\n\tconst { from, to } = editor.state.selection;\n\tconst tr = editor.state.tr.replaceRange(from, to, slice);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Section divider HTML\n// ---------------------------------------------------------------------------\n\nexport const SECTION_DIVIDER_HTML = (id: string) =>\n\t`<div data-section-divider=\"${id}\" style=\"display:none;height:0;line-height:0;overflow:hidden;\"></div>`;\n\n/**\n * Build initial HTML for an editor with one or more named sections below the body.\n * Each section is separated by an invisible divider node.\n *\n * @example\n * buildSectionedContent('<p>Body</p>', [\n * { id: 'signature', content: '<p>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- fwd ---</p>' },\n * ])\n */\nexport function buildSectionedContent(\n\tbody: string,\n\tsections: Array<{ id: string; content: string }>,\n): string {\n\tlet html = body;\n\tfor (const s of sections) {\n\t\thtml += SECTION_DIVIDER_HTML(s.id) + s.content;\n\t}\n\treturn html;\n}\n\n// ---------------------------------------------------------------------------\n// Section extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the editor HTML into a Map of sectionId → raw HTML string.\n * The special key `'body'` always holds the content before the first divider.\n * Map iteration order matches document order.\n */\nexport function extractAllSections(editor: Editor): Map<string, string> {\n\treturn extractAllSectionsFromHtml(editor.getHTML());\n}\n\n/**\n * Extract just the body portion (everything before the first section divider)\n * from an HTML string produced by BikEditor. Safe to call on plain HTML too —\n * returns the full string when no dividers are present.\n */\nexport function getBodyHtml(html: string): string {\n\tif (!html) return '';\n\tconst sections = extractAllSectionsFromHtml(html);\n\treturn sections.get('body') ?? html;\n}\n\n/**\n * Extract everything from the first section divider onward (dividers + their\n * content). Returns an empty string when there are no sections.\n */\nexport function getSectionsHtml(html: string): string {\n\tif (!html) return '';\n\tconst body = getBodyHtml(html);\n\treturn html.substring(body.length);\n}\n\nexport function extractAllSectionsFromHtml(html: string): Map<string, string> {\n\tconst sections = new Map<string, string>();\n\tconst dividerRegex =\n\t\t/<div[^>]*data-section-divider=\"([^\"]*)[^>]*>\\s*<\\/div>/g;\n\tconst dividers: Array<{ id: string; index: number; endIndex: number }> = [];\n\n\tlet match: RegExpExecArray | null;\n\twhile ((match = dividerRegex.exec(html)) !== null) {\n\t\tdividers.push({\n\t\t\tid: match[1],\n\t\t\tindex: match.index,\n\t\t\tendIndex: match.index + match[0].length,\n\t\t});\n\t}\n\n\tif (dividers.length === 0) {\n\t\tsections.set('body', html);\n\t\treturn sections;\n\t}\n\n\tsections.set('body', html.slice(0, dividers[0].index));\n\tfor (let i = 0; i < dividers.length; i++) {\n\t\tconst start = dividers[i].endIndex;\n\t\tconst end = i + 1 < dividers.length ? dividers[i + 1].index : html.length;\n\t\tsections.set(dividers[i].id, html.slice(start, end));\n\t}\n\n\treturn sections;\n}\n\n/** Returns BikEditorContent for a single named section (or 'body'). */\nexport function extractSectionContent(\n\teditor: Editor,\n\tid: string,\n): EditorSnapshot {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1)\n\t\treturn { html: '', text: '', isEmpty: true, characterCount: 0 };\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst slice = editor.state.doc.slice(startPos, endPos);\n\tconst container = document.createElement('div');\n\tconst serializer = DOMSerializer.fromSchema(editor.schema);\n\tcontainer.appendChild(serializer.serializeFragment(slice.content));\n\tconst html = toPortableHtml(container.innerHTML);\n\tconst text = container.textContent ?? '';\n\treturn { html, text, isEmpty: !text.trim(), characterCount: text.length };\n}\n\n/**\n * Parse an HTML string into ProseMirror nodes using the editor's schema.\n */\nfunction parseHtmlToNodes(editor: Editor, html: string) {\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\treturn DOMParser.fromSchema(editor.schema).parse(container).content;\n}\n\n/**\n * Replace the content of a single named section without touching others.\n * Uses a ProseMirror transaction to replace only the target range,\n * preserving undo history and other sections' state.\n * If the section doesn't exist yet, it is appended at the end of the document.\n */\nexport function setSectionContentInEditor(\n\teditor: Editor,\n\tid: string,\n\thtml: string,\n): void {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1) {\n\t\tconst endPos = editor.state.doc.content.size;\n\t\tconst dividerHtml = SECTION_DIVIDER_HTML(id) + html;\n\t\teditor.commands.insertContentAt(endPos, dividerHtml, {\n\t\t\tupdateSelection: false,\n\t\t});\n\t\treturn;\n\t}\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst fragment = parseHtmlToNodes(editor, html);\n\tconst { tr } = editor.state;\n\t// For body, replace from position 0 (before the first paragraph's opening\n\t// boundary) so ProseMirror swaps entire block nodes cleanly. Position 1\n\t// (inside the paragraph) would force ProseMirror to close the open paragraph\n\t// first, creating a ghost empty <p></p>.\n\tconst replaceFrom = id === 'body' ? 0 : startPos;\n\ttr.replaceWith(replaceFrom, endPos, fragment);\n\ttr.setMeta('addToHistory', false);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Convenience shorthands (body = first section)\n// ---------------------------------------------------------------------------\n\n/** Extract BikEditorContent for just the body (before the first divider). */\nexport function extractBodyContent(editor: Editor): EditorSnapshot {\n\treturn extractSectionContent(editor, 'body');\n}\n\n// ---------------------------------------------------------------------------\n// Active formats\n// ---------------------------------------------------------------------------\n\nexport function extractActiveFormats(editor: Editor): FormatState {\n\treturn {\n\t\tbold: editor.isActive('bold'),\n\t\titalic: editor.isActive('italic'),\n\t\tunderline: editor.isActive('underline'),\n\t\tstrike: editor.isActive('strike'),\n\t\tbulletList: editor.isActive('bulletList'),\n\t\torderedList: editor.isActive('orderedList'),\n\t\tblockquote: editor.isActive('blockquote'),\n\t\tcodeBlock: editor.isActive('codeBlock'),\n\t\tlink: (() => {\n\t\t\tif (editor.isActive('link')) {\n\t\t\t\treturn { href: editor.getAttributes('link')['href'] ?? '' };\n\t\t\t}\n\t\t\tconst { $from } = editor.state.selection;\n\t\t\tif ($from.pos > 0) {\n\t\t\t\tconst before = $from.doc.resolve($from.pos);\n\t\t\t\tconst linkMark = before.marks().find((m) => m.type.name === 'link');\n\t\t\t\tif (!linkMark) {\n\t\t\t\t\tconst nodeBefore = $from.nodeBefore;\n\t\t\t\t\tconst markOnPrev = nodeBefore?.marks.find(\n\t\t\t\t\t\t(m) => m.type.name === 'link',\n\t\t\t\t\t);\n\t\t\t\t\tif (markOnPrev) {\n\t\t\t\t\t\treturn { href: markOnPrev.attrs['href'] ?? '' };\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn { href: linkMark.attrs['href'] ?? '' };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t})(),\n\t\ttextAlign: editor.isActive({ textAlign: 'center' })\n\t\t\t? 'center'\n\t\t\t: editor.isActive({ textAlign: 'right' })\n\t\t\t? 'right'\n\t\t\t: editor.isActive({ textAlign: 'justify' })\n\t\t\t? 'justify'\n\t\t\t: editor.isActive({ textAlign: 'left' })\n\t\t\t? 'left'\n\t\t\t: null,\n\t\tfontFamily: editor.getAttributes('textStyle')['fontFamily'] ?? null,\n\t\tfontSize: editor.getAttributes('textStyle')['fontSize'] ?? null,\n\t\tcolor: editor.getAttributes('textStyle')['color'] ?? null,\n\t\thighlight: editor.isActive('highlight')\n\t\t\t? editor.getAttributes('highlight')['color'] ?? 'default'\n\t\t\t: null,\n\t\tsuperscript: editor.isActive('superscript'),\n\t\tsubscript: editor.isActive('subscript'),\n\t};\n}\n\nexport function extractContent(editor: Editor): EditorSnapshot {\n\tconst html = toPortableHtml(editor.getHTML());\n\tconst normalized = normalizeHardBreaks(editor.state.doc.content);\n\tconst tempDoc = editor.schema.node('doc', null, normalized);\n\tconst text = tempDoc.textBetween(0, tempDoc.content.size, '\\n', '\\n');\n\treturn {\n\t\thtml,\n\t\ttext,\n\t\tisEmpty: editor.isEmpty,\n\t\tcharacterCount: text.length,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Section position helpers (used by insertAtSectionStart/End and appendBodyContent)\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the ProseMirror position immediately after a section's opening divider\n * (i.e. the start of the section's own content).\n * For `'body'`, returns 1 (beginning of the document).\n * Returns -1 if the named section's divider is not found.\n */\nexport function findSectionStartPos(editor: Editor, sectionId: string): number {\n\tif (sectionId === 'body') return 1;\n\tlet startPos = -1;\n\teditor.state.doc.descendants((node, pos) => {\n\t\tif (startPos !== -1) return false;\n\t\tif (\n\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t) {\n\t\t\tstartPos = pos + node.nodeSize; // position right after the divider\n\t\t}\n\t});\n\treturn startPos;\n}\n\n/**\n * Returns the ProseMirror position at the end of a named section's content:\n * - For `'body'`: position of the first section divider, or end of document.\n * - For a named section: position of the next divider, or end of document.\n */\nexport function findSectionEndPos(editor: Editor, sectionId: string): number {\n\tconst doc = editor.state.doc;\n\tconst docSize = doc.content.size;\n\n\tif (sectionId === 'body') {\n\t\tlet firstDividerPos = -1;\n\t\tdoc.descendants((node, pos) => {\n\t\t\tif (firstDividerPos !== -1) return false;\n\t\t\tif (node.type.name === 'sectionDivider') {\n\t\t\t\tfirstDividerPos = pos;\n\t\t\t}\n\t\t});\n\t\treturn firstDividerPos !== -1 ? firstDividerPos : docSize;\n\t}\n\n\tlet passedSection = false;\n\tlet nextDividerPos = -1;\n\tdoc.descendants((node, pos) => {\n\t\tif (nextDividerPos !== -1) return false;\n\t\tif (!passedSection) {\n\t\t\tif (\n\t\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t\t) {\n\t\t\t\tpassedSection = true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (node.type.name === 'sectionDivider') {\n\t\t\tnextDividerPos = pos;\n\t\t}\n\t});\n\treturn nextDividerPos !== -1 ? nextDividerPos : docSize;\n}\n"],"names":["normalizeHtml","html","replace","toPortableHtml","div","document","createElement","innerHTML","blocks","querySelectorAll","p","Array","from","childNodes","length","appendChild","lastChild","HTMLBRElement","insertInlineHtml","editor","normalised","dom","slice","DOMParser","fromSchema","schema","parseSlice","size","to","state","selection","tr","replaceRange","view","dispatch","SECTION_DIVIDER_HTML","id","buildSectionedContent","body","sections","s","content","getBodyHtml","_a","extractAllSectionsFromHtml","get","getSectionsHtml","substring","Map","dividerRegex","dividers","match","exec","push","index","endIndex","set","i","start","end","extractSectionContent","startPos","findSectionStartPos","text","isEmpty","characterCount","endPos","findSectionEndPos","doc","container","serializer","DOMSerializer","serializeFragment","textContent","trim","setSectionContentInEditor","dividerHtml","commands","insertContentAt","updateSelection","fragment","parse","parseHtmlToNodes","replaceFrom","replaceWith","setMeta","extractBodyContent","extractActiveFormats","bold","isActive","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","href","getAttributes","$from","pos","linkMark","resolve","marks","find","m","type","name","_c","attrs","nodeBefore","markOnPrev","_b","textAlign","fontFamily","fontSize","color","highlight","_d","superscript","subscript","extractContent","getHTML","normalized","normalizeHardBreaks","tempDoc","node","textBetween","sectionId","descendants","nodeSize","docSize","firstDividerPos","passedSection","nextDividerPos"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"
|
|
1
|
+
import{mergeAttributes as e,Extension as t}from"../../node_modules/@tiptap/core/dist/index.js";import o from"@tiptap/extension-character-count";import n from"@tiptap/extension-color";import i from"@tiptap/extension-font-family";import r from"@tiptap/extension-highlight";import a from"@tiptap/extension-image";import s from"@tiptap/extension-link";import p from"../../node_modules/@tiptap/extension-paragraph/dist/index.js";import m from"@tiptap/extension-placeholder";import d from"@tiptap/extension-subscript";import l from"@tiptap/extension-superscript";import c from"@tiptap/extension-text-align";import{TextStyle as u}from"@tiptap/extension-text-style";import f from"@tiptap/extension-underline";import h from"@tiptap/starter-kit";import{FontSizeExtension as x}from"./FontSizeExtension.js";import{buildAgentMentionExtension as g,buildTeamMentionExtension as v}from"./mention/MentionExtension.js";import{PasteExtension as S}from"./paste/PasteExtension.js";import{PasteNormalizationExtension as k}from"./plainClipboard/PasteNormalizationExtension.js";import{SectionDividerNode as M}from"./sectionDivider/SectionDividerNode.js";import{SendShortcutExtension as C}from"./sendShortcut/SendShortcutExtension.js";import{buildSlashCommandExtension as b}from"./slashCommand/SlashCommandExtension.js";import{VariableDecorationExtension as j}from"./variable/VariableDecorationExtension.js";function E(E){var P,y,D,T,w,I,H,L,z;const A=null!==(y=null===(P=E.features)||void 0===P?void 0:P.richPaste)&&void 0!==y&&y,B=null!==(T=null===(D=E.features)||void 0===D?void 0:D.richTypography)&&void 0!==T&&T,N=null===(w=E.features)||void 0===w?void 0:w.allowedMarks,O=e=>!N||N.includes(e),R=p.extend({renderHTML(t){let{HTMLAttributes:o}=t;return["p",e(o,{style:"margin: 0px; padding: 0px;"}),0]}});return[...[h.configure({link:!1,underline:!1,paragraph:!A&&{},hardBreak:!1,bold:!!O("bold")&&{},italic:!!O("italic")&&{},strike:!!O("strike")&&{},code:!!O("code")&&{}}),t.create({name:"shiftEnterParagraph",addKeyboardShortcuts:()=>({"Shift-Enter":e=>{let{editor:t}=e;return t.commands.splitBlock()}})}),...A?[R]:[],...O("underline")?[f]:[],s.extend({addPasteRules:()=>[],addInputRules:()=>[]}).configure({openOnClick:!1,autolink:!1,linkOnPaste:!1,HTMLAttributes:{rel:"noopener noreferrer",class:"bik-link"}}),u,m.configure({placeholder:null!==(I=E.placeholder)&&void 0!==I?I:"Type a message..."}),...E.onPaste?[S.configure({onPaste:E.onPaste})]:[],k.configure({preserveMarks:A}),C.configure({onSend:E.onSend,sendShortcut:E.sendShortcut,extraShortcuts:null!==(H=E.shortcuts)&&void 0!==H?H:[]}),j,...E.maxCharacters?[o.configure({limit:E.maxCharacters})]:[],M],...[...(null===(L=E.mentions)||void 0===L?void 0:L.agents)?[g(E.mentions.agents,E.onMentionSelected,E.renderMentionItem,E.renderMentionDropdown)]:[],...(null===(z=E.mentions)||void 0===z?void 0:z.teams)?[v(E.mentions.teams,E.onMentionSelected,E.renderMentionItem,E.renderMentionDropdown)]:[],...E.slashCommands?[b(E.slashCommands,E.onSlashCommandSelected,E.renderSlashCommandItem,E.renderSlashCommandDropdown)]:[]],...B?[n,r.configure({multicolor:!0}),i,x,c.configure({types:["heading","paragraph"]}),d,l,a]:[n]]}export{E as buildExtensions};
|
|
2
2
|
//# sourceMappingURL=buildExtensions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport HardBreak from '@tiptap/extension-hard-break';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\t// Soft line break. Shift-Enter inserts a <br> inside the current block\n\t// (matches Gmail); plain Enter still splits the block into a new paragraph\n\t// via StarterKit's default. HardBreak is registered standalone — like Link\n\t// and Underline above — instead of through StarterKit, so we can bind ONLY\n\t// Shift-Enter here and leave Mod-Enter free for the send shortcut.\n\tconst softBreak = HardBreak.extend({\n\t\taddKeyboardShortcuts() {\n\t\t\treturn {\n\t\t\t\t'Shift-Enter': () => this.editor.commands.setHardBreak(),\n\t\t\t};\n\t\t},\n\t});\n\n\t// Paragraph that preserves a zero block-margin from the source (Gmail-style).\n\t// Most sources (ChatGPT, typed text) use plain <p> → no margin attr → the\n\t// editor/viewer default 1em margin provides the gap. Google-Docs-style\n\t// content uses margin:0 paragraphs and a separate blank-line paragraph for\n\t// spacing; we keep that margin:0 so the blank line — not stacked margins —\n\t// provides a single-line gap, exactly like Gmail. The paste pipeline also\n\t// sets margin:'0' on every paragraph when a paste contains blank-line\n\t// paragraphs (see applyParagraphSpacing), so spacing comes from EITHER the\n\t// margin OR the blank line, never both (no double gap).\n\tconst isZeroLen = (v: string) =>\n\t\tv === '0' || v === '0px' || v === '0pt' || v === '0em';\n\tconst ParagraphWithMargin = Paragraph.extend({\n\t\taddAttributes() {\n\t\t\treturn {\n\t\t\t\t...(this.parent?.() ?? {}),\n\t\t\t\tmargin: {\n\t\t\t\t\tdefault: null,\n\t\t\t\t\tparseHTML: (el: HTMLElement) =>\n\t\t\t\t\t\tisZeroLen(el.style.marginTop) && isZeroLen(el.style.marginBottom)\n\t\t\t\t\t\t\t? '0'\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\trenderHTML: (attrs: { margin?: string | null }) =>\n\t\t\t\t\t\tattrs.margin === '0' ? { style: 'margin: 0px' } : {},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\thardBreak: false,\n\t\t\tparagraph: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tParagraphWithMargin,\n\t\tsoftBreak,\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t\t// Only email + signature (richPaste) keep the rich HTML paste pipeline.\n\t\t\t// Every other channel (WhatsApp, IG, FB, comments) forces plain-text\n\t\t\t// paste for BOTH Cmd/Ctrl+V and Cmd/Ctrl+Shift+V — no formatting.\n\t\t\tplainOnly: !hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["buildExtensions","config","hasRichPaste","_b","_a","features","richPaste","hasRichTypography","_d","_c","richTypography","allowedMarks","_e","isMark","m","includes","softBreak","HardBreak","extend","addKeyboardShortcuts","Shift-Enter","this","editor","commands","setHardBreak","isZeroLen","v","ParagraphWithMargin","Paragraph","addAttributes","Object","assign","parent","call","margin","default","parseHTML","el","style","marginTop","marginBottom","renderHTML","attrs","StarterKit","configure","link","underline","hardBreak","paragraph","bold","italic","strike","code","Underline","Link","addPasteRules","addInputRules","openOnClick","autolink","linkOnPaste","HTMLAttributes","rel","class","TextStyle","Placeholder","placeholder","_f","onPaste","PasteExtension","PasteNormalizationExtension","preserveMarks","plainOnly","SendShortcutExtension","onSend","sendShortcut","extraShortcuts","_g","shortcuts","VariableDecorationExtension","maxCharacters","CharacterCount","limit","SectionDividerNode","_h","mentions","agents","buildAgentMentionExtension","onMentionSelected","renderMentionItem","renderMentionDropdown","_j","teams","buildTeamMentionExtension","slashCommands","buildSlashCommandExtension","onSlashCommandSelected","renderSlashCommandItem","renderSlashCommandDropdown","Color","Highlight","multicolor","FontFamily","FontSizeExtension","TextAlign","types","Subscript","Superscript","Image"],"mappings":"ozCAsEM,SAAUA,EAAgBC,yBAC/B,MAAMC,EAA6C,QAA9BC,EAAiB,QAAjBC,EAAAH,EAAOI,gBAAU,IAAAD,OAAA,EAAAA,EAAAE,iBAAa,IAAAH,GAAAA,EAC7CI,EAAuD,QAAnCC,EAAiB,QAAjBC,EAAAR,EAAOI,gBAAU,IAAAI,OAAA,EAAAA,EAAAC,sBAAkB,IAAAF,GAAAA,EAKvDG,EAA8B,QAAfC,EAAAX,EAAOI,gBAAQ,IAAAO,OAAA,EAAAA,EAAED,aAChCE,EAAUC,IACdH,GAAgBA,EAAaI,SAASD,GAOlCE,EAAYC,EAAUC,OAAO,CAClCC,uBACC,MAAO,CACN,cAAeC,IAAMC,KAAKC,OAAOC,SAASC,eAE5C,IAYKC,EAAaC,GACZ,MAANA,GAAmB,QAANA,GAAqB,QAANA,GAAqB,QAANA,EACtCC,EAAsBC,EAAUV,OAAO,CAC5CW,wBACC,OAAAC,OAAAC,OAAAD,OAAAC,OAAA,CAAA,EACoB,QAAf5B,EAAe,QAAfC,EAAAiB,KAAKW,cAAU,IAAA5B,OAAA,EAAAA,EAAA6B,KAAAZ,aAAA,IAAAlB,EAAAA,EAAI,CAAG,GAC1B,CAAA+B,OAAQ,CACPC,QAAS,KACTC,UAAYC,GACXZ,EAAUY,EAAGC,MAAMC,YAAcd,EAAUY,EAAGC,MAAME,cACjD,IACA,KACJC,WAAaC,GACK,MAAjBA,EAAMR,OAAiB,CAAEI,MAAO,eAAkB,CAAE,IAGxD,IAiHD,MAAO,IA9GM,CAIZK,EAAWC,UAAU,CACpBC,MAAM,EACNC,WAAW,EACXC,WAAW,EACXC,WAAW,EACXC,OAAMpC,EAAO,SAAU,CAAE,EACzBqC,SAAQrC,EAAO,WAAY,CAAE,EAC7BsC,SAAQtC,EAAO,WAAY,CAAE,EAC7BuC,OAAMvC,EAAO,SAAU,CAAE,IAE1Bc,EACAX,KACIH,EAAO,aAAe,CAACwC,GAAa,GAGxCC,EAAKpC,OAAO,CACXqC,cAAeA,IAAM,GACrBC,cAAeA,IAAM,KACnBZ,UAAU,CACZa,aAAa,EACbC,UAAU,EACVC,aAAa,EACbC,eAAgB,CACfC,IAAK,sBACLC,MAAO,cAGTC,EACAC,EAAYpB,UAAU,CACrBqB,oBAAaC,EAAAjE,EAAOgE,2BAAe,yBAKhChE,EAAOkE,QACR,CAACC,EAAexB,UAAU,CAAEuB,QAASlE,EAAOkE,WAC5C,GACHE,EAA4BzB,UAAU,CACrC0B,cAAepE,EAIfqE,WAAYrE,IAEbsE,EAAsB5B,UAAU,CAC/B6B,OAAQxE,EAAOwE,OACfC,aAAczE,EAAOyE,aACrBC,uBAAgBC,EAAA3E,EAAO4E,yBAAa,KAErCC,KACI7E,EAAO8E,cACR,CAACC,EAAepC,UAAU,CAAEqC,MAAOhF,EAAO8E,iBAC1C,GAKHG,MAGyB,aACrBC,EAAAlF,EAAOmF,+BAAUC,QAClB,CACAC,EACCrF,EAAOmF,SAASC,OAChBpF,EAAOsF,kBACPtF,EAAOuF,kBACPvF,EAAOwF,wBAGR,eACCC,EAAAzF,EAAOmF,+BAAUO,OAClB,CACAC,EACC3F,EAAOmF,SAASO,MAChB1F,EAAOsF,kBACPtF,EAAOuF,kBACPvF,EAAOwF,wBAGR,MACCxF,EAAO4F,cACR,CACAC,EACC7F,EAAO4F,cACP5F,EAAO8F,uBACP9F,EAAO+F,uBACP/F,EAAOgG,6BAGR,OAGmB1F,EACpB,CACA2F,EACAC,EAAUvD,UAAU,CAAEwD,YAAY,IAClCC,EACAC,EACAC,EAAU3D,UAAU,CAAE4D,MAAO,CAAC,UAAW,eACzCC,EACAC,EACAC,GAEA,CAACT,GAGL"}
|
|
1
|
+
{"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import { Extension, mergeAttributes } from '@tiptap/core';\nimport CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\tconst emailParagraph = Paragraph.extend({\n\t\trenderHTML({ HTMLAttributes }) {\n\t\t\treturn [\n\t\t\t\t'p',\n\t\t\t\tmergeAttributes(HTMLAttributes, {\n\t\t\t\t\tstyle: 'margin: 0px; padding: 0px;',\n\t\t\t\t}),\n\t\t\t\t0,\n\t\t\t];\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\tparagraph: hasRichPaste ? false : {},\n\t\t\thardBreak: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tExtension.create({\n\t\t\tname: 'shiftEnterParagraph',\n\t\t\taddKeyboardShortcuts() {\n\t\t\t\treturn {\n\t\t\t\t\t'Shift-Enter': ({ editor }) => editor.commands.splitBlock(),\n\t\t\t\t};\n\t\t\t},\n\t\t}),\n\t\t...(hasRichPaste ? [emailParagraph] : []),\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["buildExtensions","config","hasRichPaste","_b","_a","features","richPaste","hasRichTypography","_d","_c","richTypography","allowedMarks","_e","isMark","m","includes","emailParagraph","Paragraph","extend","renderHTML","_ref","HTMLAttributes","mergeAttributes","style","StarterKit","configure","link","underline","paragraph","hardBreak","bold","italic","strike","code","Extension","create","name","addKeyboardShortcuts","_ref2","editor","commands","splitBlock","Underline","Link","addPasteRules","addInputRules","openOnClick","autolink","linkOnPaste","rel","class","TextStyle","Placeholder","placeholder","_f","onPaste","PasteExtension","PasteNormalizationExtension","preserveMarks","SendShortcutExtension","onSend","sendShortcut","extraShortcuts","_g","shortcuts","VariableDecorationExtension","maxCharacters","CharacterCount","limit","SectionDividerNode","_h","mentions","agents","buildAgentMentionExtension","onMentionSelected","renderMentionItem","renderMentionDropdown","_j","teams","buildTeamMentionExtension","slashCommands","buildSlashCommandExtension","onSlashCommandSelected","renderSlashCommandItem","renderSlashCommandDropdown","Color","Highlight","multicolor","FontFamily","FontSizeExtension","TextAlign","types","Subscript","Superscript","Image"],"mappings":"u2CAsEM,SAAUA,EAAgBC,yBAC/B,MAAMC,EAA6C,QAA9BC,EAAiB,QAAjBC,EAAAH,EAAOI,gBAAU,IAAAD,OAAA,EAAAA,EAAAE,iBAAa,IAAAH,GAAAA,EAC7CI,EAAuD,QAAnCC,EAAiB,QAAjBC,EAAAR,EAAOI,gBAAU,IAAAI,OAAA,EAAAA,EAAAC,sBAAkB,IAAAF,GAAAA,EAKvDG,EAA8B,QAAfC,EAAAX,EAAOI,gBAAQ,IAAAO,OAAA,EAAAA,EAAED,aAChCE,EAAUC,IACdH,GAAgBA,EAAaI,SAASD,GAElCE,EAAiBC,EAAUC,OAAO,CACvCC,WAAUC,GAAmB,IAAlBC,eAAEA,GAAgBD,EAC5B,MAAO,CACN,IACAE,EAAgBD,EAAgB,CAC/BE,MAAO,+BAER,EAEF,IAoHD,MAAO,IAjHM,CAIZC,EAAWC,UAAU,CACpBC,MAAM,EACNC,WAAW,EACXC,WAAW1B,GAAuB,CAAE,EACpC2B,WAAW,EACXC,OAAMjB,EAAO,SAAU,CAAE,EACzBkB,SAAQlB,EAAO,WAAY,CAAE,EAC7BmB,SAAQnB,EAAO,WAAY,CAAE,EAC7BoB,OAAMpB,EAAO,SAAU,CAAE,IAE1BqB,EAAUC,OAAO,CAChBC,KAAM,sBACNC,qBAAoBA,KACZ,CACN,cAAeC,IAAA,IAACC,OAAEA,GAAQD,EAAA,OAAKC,EAAOC,SAASC,YAAY,SAI1DvC,EAAe,CAACc,GAAkB,MAClCH,EAAO,aAAe,CAAC6B,GAAa,GAGxCC,EAAKzB,OAAO,CACX0B,cAAeA,IAAM,GACrBC,cAAeA,IAAM,KACnBpB,UAAU,CACZqB,aAAa,EACbC,UAAU,EACVC,aAAa,EACb3B,eAAgB,CACf4B,IAAK,sBACLC,MAAO,cAGTC,EACAC,EAAY3B,UAAU,CACrB4B,oBAAaC,EAAArD,EAAOoD,2BAAe,yBAKhCpD,EAAOsD,QACR,CAACC,EAAe/B,UAAU,CAAE8B,QAAStD,EAAOsD,WAC5C,GACHE,EAA4BhC,UAAU,CACrCiC,cAAexD,IAEhByD,EAAsBlC,UAAU,CAC/BmC,OAAQ3D,EAAO2D,OACfC,aAAc5D,EAAO4D,aACrBC,uBAAgBC,EAAA9D,EAAO+D,yBAAa,KAErCC,KACIhE,EAAOiE,cACR,CAACC,EAAe1C,UAAU,CAAE2C,MAAOnE,EAAOiE,iBAC1C,GAKHG,MAGyB,aACrBC,EAAArE,EAAOsE,+BAAUC,QAClB,CACAC,EACCxE,EAAOsE,SAASC,OAChBvE,EAAOyE,kBACPzE,EAAO0E,kBACP1E,EAAO2E,wBAGR,eACCC,EAAA5E,EAAOsE,+BAAUO,OAClB,CACAC,EACC9E,EAAOsE,SAASO,MAChB7E,EAAOyE,kBACPzE,EAAO0E,kBACP1E,EAAO2E,wBAGR,MACC3E,EAAO+E,cACR,CACAC,EACChF,EAAO+E,cACP/E,EAAOiF,uBACPjF,EAAOkF,uBACPlF,EAAOmF,6BAGR,OAGmB7E,EACpB,CACA8E,EACAC,EAAU7D,UAAU,CAAE8D,YAAY,IAClCC,EACAC,EACAC,EAAUjE,UAAU,CAAEkE,MAAO,CAAC,UAAW,eACzCC,EACAC,EACAC,GAEA,CAACT,GAGL"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Extension as t}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as e}from"@tiptap/pm/model";import{Plugin as
|
|
1
|
+
import{Extension as t}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as e}from"@tiptap/pm/model";import{Plugin as r}from"@tiptap/pm/state";import{Decoration as a,DecorationSet as n}from"@tiptap/pm/view";import{parseClipboardText as o,wrapInlineContent as s,cleanBlockBrs as i,stripTrailingBrs as p,normalizeHardBreaks as d,stripRichMarks as c}from"./pasteUtils.js";const l=t.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const t=this.options.preserveMarks;return[new r({props:{decorations(t){const e=[];return t.doc.descendants(((t,r)=>{"paragraph"===t.type.name&&0===t.childCount&&e.push(a.node(r,r+t.nodeSize,{class:"is-blank"}))})),n.create(t.doc,e)},handlePaste(t,e){var r,a;const n=null===(r=e.clipboardData)||void 0===r?void 0:r.getData("text/html");if(null==n?void 0:n.includes("data-pm-slice"))return!1;const s=null===(a=e.clipboardData)||void 0===a?void 0:a.getData("text/plain");if(!s)return!1;const i=o(s,t.state.schema);return t.dispatch(t.state.tr.replaceSelection(i)),!0},clipboardTextParser:(t,e,r,a)=>o(t,a.state.schema),transformPastedHTML(t){if(t.includes("data-pm-slice"))return t;const e=document.createElement("div");return e.innerHTML=t,s(e),i(e),p(e),e.innerHTML},transformPasted(r){let a=d(r.content);return t||(a=c(a)),new e(a,r.openStart,r.openEnd)}}})]}});export{l as PasteNormalizationExtension};
|
|
2
2
|
//# sourceMappingURL=PasteNormalizationExtension.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport { Decoration, DecorationSet } from '@tiptap/pm/view';\nimport {\n\tcleanBlockBrs,\n\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (preserves blank lines)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalizes hardBreaks, strips marks)\n *\n * Also decorates empty paragraphs with an `is-blank` CSS class so the\n * editor stylesheet can give blank lines precise height without extra margins.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tprops: {\n\t\t\t\t\tdecorations(state) {\n\t\t\t\t\t\tconst decorations: Decoration[] = [];\n\t\t\t\t\t\tstate.doc.descendants((node, pos) => {\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph' && node.childCount === 0) {\n\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\tDecoration.node(pos, pos + node.nodeSize, {\n\t\t\t\t\t\t\t\t\t\tclass: 'is-blank',\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn DecorationSet.create(state.doc, decorations);\n\t\t\t\t\t},\n\n\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\tconst html = event.clipboardData?.getData('text/html');\n\t\t\t\t\t\tif (html?.includes('data-pm-slice')) return false;\n\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain');\n\t\t\t\t\t\tif (!text) return false;\n\t\t\t\t\t\tconst slice = parseClipboardText(text, view.state.schema);\n\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\treturn parseClipboardText(text, view.state.schema);\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","addProseMirrorPlugins","this","options","Plugin","props","decorations","state","doc","descendants","node","pos","type","childCount","push","Decoration","nodeSize","class","DecorationSet","handlePaste","view","event","html","_a","clipboardData","getData","includes","text","_b","slice","parseClipboardText","schema","dispatch","tr","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"wYAwBaA,EAA8BC,EAAUC,OAAO,CAC3DC,KAAM,qBACNC,WAAUA,KACF,CACNC,eAAe,IAGjBC,wBACC,MAAMD,EAAgBE,KAAKC,QAAQH,cACnC,MAAO,CACN,IAAII,EAAO,CACVC,MAAO,CACNC,YAAYC,GACX,MAAMD,EAA4B,GAUlC,OATAC,EAAMC,IAAIC,aAAY,CAACC,EAAMC,KACL,cAAnBD,EAAKE,KAAKd,MAA4C,IAApBY,EAAKG,YAC1CP,EAAYQ,KACXC,EAAWL,KAAKC,EAAKA,EAAMD,EAAKM,SAAU,CACzCC,MAAO,aAGT,IAEKC,EAAcrB,OAAOU,EAAMC,IAAKF,EACvC,EAEDa,YAAYC,EAAMC,WACjB,MAAMC,EAA0B,QAAnBC,EAAAF,EAAMG,qBAAa,IAAAD,OAAA,EAAAA,EAAEE,QAAQ,aAC1C,GAAIH,aAAI,EAAJA,EAAMI,SAAS,iBAAkB,OAAO,EAC5C,MAAMC,EAA0B,QAAnBC,EAAAP,EAAMG,qBAAa,IAAAI,OAAA,EAAAA,EAAEH,QAAQ,cAC1C,IAAKE,EAAM,OAAO,EAClB,MAAME,EAAQC,EAAmBH,EAAMP,EAAKb,MAAMwB,QAElD,OADAX,EAAKY,SAASZ,EAAKb,MAAM0B,GAAGC,iBAAiBL,KACtC,CACP,EAEDM,oBAAmBA,CAACR,EAAMS,EAAWC,EAAQjB,IACrCU,EAAmBH,EAAMP,EAAKb,MAAMwB,QAG5CO,oBAAoBhB,GACnB,GAAIA,EAAKI,SAAS,iBAAkB,OAAOJ,EAC3C,MAAMiB,EAAYC,SAASC,cAAc,OAKzC,OAJAF,EAAUG,UAAYpB,EACtBqB,EAAkBJ,GAClBK,EAAcL,GACdM,EAAiBN,GACVA,EAAUG,SACjB,EAEDI,gBAAgBjB,GACf,IAAIkB,EAAUC,EAAoBnB,EAAMkB,SAIxC,OAHK/C,IACJ+C,EAAUE,EAAeF,IAEnB,IAAIG,EAAMH,EAASlB,EAAMsB,UAAWtB,EAAMuB,QAClD,KAIJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Fragment as e,Slice as t}from"@tiptap/pm/model";const
|
|
1
|
+
import{Fragment as e,Slice as t}from"@tiptap/pm/model";const o=new Set(["P","LI","H1","H2","H3","H4","H5","H6","TD","TH","PRE"]),n="p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre",r=new Set(["bold","italic","strike","underline","code"]);function l(e){if(e.querySelector(n))return;const t=Array.from(e.childNodes),o=[[]];for(let e=0;e<t.length;e++){const n=t[e];n instanceof HTMLBRElement&&t[e+1]instanceof HTMLBRElement?(o.push([]),e++):o[o.length-1].push(n)}for(;e.firstChild;)e.firstChild.remove();for(const t of o){const o=document.createElement("p");for(const e of t)o.appendChild(e);e.appendChild(o)}}function a(e){const t=Array.from(e.querySelectorAll("br"));for(const n of t){let t=!1,r=n.parentElement;for(;r&&r!==e;){if(o.has(r.tagName)){t=!0;break}r=r.parentElement}t||n.parentElement.replaceChild(document.createElement("p"),n)}}function c(e){for(const t of["p","div"])for(const o of Array.from(e.querySelectorAll(t))){const e=o.lastElementChild;"BR"===(null==e?void 0:e.tagName)&&o.childNodes.length>1&&e.remove()}}function i(t){const o=[];return t.forEach((t=>{var n,r;if("paragraph"===t.type.name){if(1===t.childCount&&"hardBreak"===(null===(n=t.firstChild)||void 0===n?void 0:n.type.name))return void o.push(t.type.create(t.attrs));if(t.childCount>1&&"hardBreak"===(null===(r=t.lastChild)||void 0===r?void 0:r.type.name)){const n=[];return t.content.forEach(((e,o,r)=>{r<t.childCount-1&&n.push(e)})),void o.push(t.copy(e.from(n)))}}t.isBlock&&!t.isLeaf?o.push(t.copy(i(t.content))):o.push(t)})),e.from(o)}function s(t){const o=[];return t.forEach((e=>{if(e.isText){const t=e.marks.filter((e=>r.has(e.type.name)));o.push(t.length===e.marks.length?e:e.mark(t))}else o.push(e.copy(s(e.content)))})),e.from(o)}function f(o,n){const r=o.split("\n").map((e=>n.node("paragraph",null,e?[n.text(e)]:[])));return new t(e.fromArray(r),1,1)}export{n as BLOCK_SELECTOR,o as TEXTBLOCK_TAGS,a as cleanBlockBrs,i as normalizeHardBreaks,f as parseClipboardText,s as stripRichMarks,c as stripTrailingBrs,l as wrapInlineContent};
|
|
2
2
|
//# sourceMappingURL=pasteUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pasteUtils.js","sources":["../../../../../src/editor/extensions/plainClipboard/pasteUtils.ts"],"sourcesContent":["import { Fragment, Node as PMNode, Schema, Slice } from '@tiptap/pm/model';\n\nexport const TEXTBLOCK_TAGS = new Set([\n\t'P',\n\t'LI',\n\t'H1',\n\t'H2',\n\t'H3',\n\t'H4',\n\t'H5',\n\t'H6',\n\t'TD',\n\t'TH',\n\t'PRE',\n]);\n\nexport const BLOCK_SELECTOR =\n\t'p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre';\n\nconst BASIC_MARKS = new Set(['bold', 'italic', 'strike', 'underline', 'code']);\n\n/** Normalise clipboard newlines: Windows CRLF and lone CR → LF. */\nexport function normalizeNewlines(text: string): string {\n\treturn text.replace(/\\r\\n?/g, '\\n');\n}\n\n/**\n * When pasted HTML is entirely inline (no block elements), split at\n * `<br><br>` boundaries into `<p>` elements. Single `<br>` stays as a\n * line break inside the paragraph.\n */\nexport function wrapInlineContent(container: HTMLElement): void {\n\tif (container.querySelector(BLOCK_SELECTOR)) return;\n\n\tconst children = Array.from(container.childNodes);\n\tconst groups: Node[][] = [[]];\n\n\tfor (let i = 0; i < children.length; i++) {\n\t\tconst node = children[i];\n\t\tif (\n\t\t\tnode instanceof HTMLBRElement &&\n\t\t\tchildren[i + 1] instanceof HTMLBRElement\n\t\t) {\n\t\t\tgroups.push([]);\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tgroups[groups.length - 1].push(node);\n\t}\n\n\twhile (container.firstChild) container.firstChild.remove();\n\n\tfor (const group of groups) {\n\t\tconst p = document.createElement('p');\n\t\tfor (const node of group) p.appendChild(node);\n\t\tcontainer.appendChild(p);\n\t}\n}\n\n/**\n * Replace `<br>` elements that sit at block level (not inside a textblock\n * like `<p>`, `<li>`, etc.) with empty `<p></p>` elements. Google Docs\n * puts standalone `<br>` tags between paragraphs to represent blank lines.\n */\nexport function cleanBlockBrs(container: HTMLElement): void {\n\tconst brs = Array.from(container.querySelectorAll('br'));\n\tfor (const br of brs) {\n\t\tlet insideTextblock = false;\n\t\tlet el = br.parentElement;\n\t\twhile (el && el !== container) {\n\t\t\tif (TEXTBLOCK_TAGS.has(el.tagName)) {\n\t\t\t\tinsideTextblock = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tel = el.parentElement;\n\t\t}\n\t\tif (!insideTextblock) {\n\t\t\tbr.parentElement!.replaceChild(document.createElement('p'), br);\n\t\t}\n\t}\n}\n\n/**\n * Strip trailing `<br>` from content paragraphs in the DOM.\n * Google Docs adds a `<br>` at the end of non-empty paragraphs as a cursor\n * placeholder. Without this, ProseMirror creates a trailing hardBreak that\n * adds a visual blank line at the bottom of the paragraph.\n */\nexport function stripTrailingBrs(container: HTMLElement): void {\n\tfor (const tag of ['p', 'div']) {\n\t\tfor (const el of Array.from(container.querySelectorAll(tag))) {\n\t\t\tconst last = el.lastElementChild;\n\t\t\tif (last?.tagName === 'BR' && el.childNodes.length > 1) {\n\t\t\t\tlast.remove();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Convert a paragraph whose ONLY child is a hardBreak (`<p><br></p>`) into a\n * truly empty paragraph. ProseMirror parses the `<br>` as a hardBreak node AND\n * adds its own cursor `<br>`, rendering double-height; an empty paragraph\n * (childCount 0) gives a single-height blank line.\n *\n * Does NOT strip trailing hardBreaks from content paragraphs, and does NOT\n * collapse consecutive empties — those `<br>`s/blank lines are intentional\n * (e.g. shift+enter / enter runs from Google Docs) and Gmail preserves them.\n */\nexport function normalizeHardBreaks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph') {\n\t\t\tif (node.childCount === 1 && node.firstChild?.type.name === 'hardBreak') {\n\t\t\t\tnodes.push(node.type.create(node.attrs));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (node.isBlock && !node.isLeaf) {\n\t\t\tnodes.push(node.copy(normalizeHardBreaks(node.content)));\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Mirror the source paragraph-margin convention across a pasted block.\n *\n * Gmail decides spacing from the SOURCE: it keeps default margins for content\n * pasted from ChatGPT/plain `<p>`, and `margin:0` for Google-Docs-style content\n * (whose paragraphs carry `margin:0`). We do the same. A paragraph's margin is\n * already preserved from the source (the Paragraph node's `margin` attribute),\n * so the only gap is the synthesized blank-line paragraphs (created from a\n * Google-Docs block-level `<br>`), which have no source margin. If the pasted\n * block is margin:0-style (any paragraph carries margin:0), apply margin:0 to\n * every paragraph of THIS paste — so the blank lines match. Pastes with no\n * margin:0 paragraph (ChatGPT, plain text) are left untouched → default margins.\n * Scoped to the pasted slice, so it never tightens existing/typed content.\n */\nexport function mirrorParagraphMargins(fragment: Fragment): Fragment {\n\tlet isMarginZeroBlock = false;\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph' && node.attrs['margin'] === '0') {\n\t\t\tisMarginZeroBlock = true;\n\t\t}\n\t});\n\tif (!isMarginZeroBlock) return fragment;\n\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph' && node.attrs['margin'] !== '0') {\n\t\t\tnodes.push(\n\t\t\t\tnode.type.create(\n\t\t\t\t\t{ ...node.attrs, margin: '0' },\n\t\t\t\t\tnode.content,\n\t\t\t\t\tnode.marks,\n\t\t\t\t),\n\t\t\t);\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Strip \"rich\" marks (link, textStyle, highlight, etc.) while keeping\n * basic marks (bold, italic, strike, underline, code) that messaging\n * channels can represent.\n */\nexport function stripRichMarks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.isText) {\n\t\t\tconst kept = node.marks.filter((m) => BASIC_MARKS.has(m.type.name));\n\t\t\tnodes.push(kept.length === node.marks.length ? node : node.mark(kept));\n\t\t} else {\n\t\t\tnodes.push(node.copy(stripRichMarks(node.content)));\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Parse plain clipboard text into a ProseMirror Slice, preserving blank\n * lines as empty paragraphs. This replaces ProseMirror's default text\n * parser which uses `\\n+` and loses all blank lines.\n *\n * Best for channels with compact paragraph gaps (e.g. WhatsApp) where\n * blank lines need structural representation as empty paragraphs.\n */\nexport function parseClipboardText(text: string, schema: Schema): Slice {\n\t// Normalise Windows CRLF (and lone CR) to LF first — otherwise the stray\n\t// \\r survives splitting and turns blank lines into whitespace paragraphs.\n\tconst paragraphs = normalizeNewlines(text)\n\t\t.split('\\n')\n\t\t.map((line) =>\n\t\t\tschema.node('paragraph', null, line ? [schema.text(line)] : []),\n\t\t);\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n\n/**\n * Parse plain clipboard text into a single paragraph with hardBreaks for\n * every `\\n`. Produces the same structure as Gmail's Cmd+Shift+V output:\n * one block element with `<br>` for line breaks and `<br><br>` for blank\n * lines. This gives identical html and text output to Gmail.\n *\n * The paragraph is tight (`margin: '0'`) — plain/paste-without-formatting is\n * Gmail's `<div>` (no margin), and typed Enter inherits this margin, so\n * splitting plain-pasted text stays tight like Gmail (not default-margin gaps).\n */\nexport function parseClipboardTextAsBreaks(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst para = (children: PMNode[]) =>\n\t\tschema.node('paragraph', { margin: '0' }, children);\n\t// Normalise Windows CRLF (and lone CR) to LF, then drop trailing blank\n\t// lines so a copied source's terminal newline doesn't add an empty line.\n\tconst trimmed = normalizeNewlines(text).replace(/\\n+$/, '');\n\tif (!trimmed) {\n\t\treturn new Slice(Fragment.from(para([])), 1, 1);\n\t}\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst lines = trimmed.split('\\n');\n\tconst children: PMNode[] = [];\n\tlines.forEach((line, i) => {\n\t\tif (line) children.push(schema.text(line));\n\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\tchildren.push(hardBreak.create());\n\t\t}\n\t});\n\treturn new Slice(Fragment.from(para(children)), 1, 1);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BLOCK_SELECTOR","BASIC_MARKS","normalizeNewlines","text","replace","wrapInlineContent","container","querySelector","children","Array","from","childNodes","groups","i","length","node","HTMLBRElement","push","firstChild","remove","group","p","document","createElement","appendChild","cleanBlockBrs","brs","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","stripTrailingBrs","tag","last","lastElementChild","normalizeHardBreaks","fragment","nodes","forEach","type","name","childCount","_a","isBlock","isLeaf","copy","content","create","attrs","Fragment","mirrorParagraphMargins","isMarginZeroBlock","Object","assign","margin","marks","stripRichMarks","isText","kept","filter","m","mark","parseClipboardTextAsBreaks","schema","para","trimmed","Slice","hardBreak","lines","split","line"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pasteUtils.js","sources":["../../../../../src/editor/extensions/plainClipboard/pasteUtils.ts"],"sourcesContent":["import { Fragment, Node as PMNode, Schema, Slice } from '@tiptap/pm/model';\n\nexport const TEXTBLOCK_TAGS = new Set([\n\t'P',\n\t'LI',\n\t'H1',\n\t'H2',\n\t'H3',\n\t'H4',\n\t'H5',\n\t'H6',\n\t'TD',\n\t'TH',\n\t'PRE',\n]);\n\nexport const BLOCK_SELECTOR =\n\t'p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre';\n\nconst BASIC_MARKS = new Set(['bold', 'italic', 'strike', 'underline', 'code']);\n\n/**\n * When pasted HTML is entirely inline (no block elements), split at\n * `<br><br>` boundaries into `<p>` elements. Single `<br>` stays as a\n * line break inside the paragraph.\n */\nexport function wrapInlineContent(container: HTMLElement): void {\n\tif (container.querySelector(BLOCK_SELECTOR)) return;\n\n\tconst children = Array.from(container.childNodes);\n\tconst groups: Node[][] = [[]];\n\n\tfor (let i = 0; i < children.length; i++) {\n\t\tconst node = children[i];\n\t\tif (\n\t\t\tnode instanceof HTMLBRElement &&\n\t\t\tchildren[i + 1] instanceof HTMLBRElement\n\t\t) {\n\t\t\tgroups.push([]);\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tgroups[groups.length - 1].push(node);\n\t}\n\n\twhile (container.firstChild) container.firstChild.remove();\n\n\tfor (const group of groups) {\n\t\tconst p = document.createElement('p');\n\t\tfor (const node of group) p.appendChild(node);\n\t\tcontainer.appendChild(p);\n\t}\n}\n\n/**\n * Replace `<br>` elements that sit at block level (not inside a textblock\n * like `<p>`, `<li>`, etc.) with empty `<p></p>` elements. Google Docs\n * puts standalone `<br>` tags between paragraphs to represent blank lines.\n */\nexport function cleanBlockBrs(container: HTMLElement): void {\n\tconst brs = Array.from(container.querySelectorAll('br'));\n\tfor (const br of brs) {\n\t\tlet insideTextblock = false;\n\t\tlet el = br.parentElement;\n\t\twhile (el && el !== container) {\n\t\t\tif (TEXTBLOCK_TAGS.has(el.tagName)) {\n\t\t\t\tinsideTextblock = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tel = el.parentElement;\n\t\t}\n\t\tif (!insideTextblock) {\n\t\t\tbr.parentElement!.replaceChild(document.createElement('p'), br);\n\t\t}\n\t}\n}\n\n/**\n * Strip trailing `<br>` from content paragraphs in the DOM.\n * Google Docs adds a `<br>` at the end of non-empty paragraphs as a cursor\n * placeholder. Without this, ProseMirror creates a trailing hardBreak that\n * adds a visual blank line at the bottom of the paragraph.\n */\nexport function stripTrailingBrs(container: HTMLElement): void {\n\tfor (const tag of ['p', 'div']) {\n\t\tfor (const el of Array.from(container.querySelectorAll(tag))) {\n\t\t\tconst last = el.lastElementChild;\n\t\t\tif (last?.tagName === 'BR' && el.childNodes.length > 1) {\n\t\t\t\tlast.remove();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Convert paragraphs containing only a hardBreak into truly empty paragraphs.\n * Google Docs represents blank lines as `<p><br></p>` — ProseMirror parses\n * the `<br>` as a hardBreak node, which renders double-height. Converting to\n * an empty paragraph (childCount 0) gives a single-height blank line.\n *\n * Does NOT collapse consecutive empties — that would destroy user intent.\n */\nexport function normalizeHardBreaks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph') {\n\t\t\tif (node.childCount === 1 && node.firstChild?.type.name === 'hardBreak') {\n\t\t\t\tnodes.push(node.type.create(node.attrs));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (node.childCount > 1 && node.lastChild?.type.name === 'hardBreak') {\n\t\t\t\tconst children: PMNode[] = [];\n\t\t\t\tnode.content.forEach((child, _offset, index) => {\n\t\t\t\t\tif (index < node.childCount - 1) children.push(child);\n\t\t\t\t});\n\t\t\t\tnodes.push(node.copy(Fragment.from(children)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (node.isBlock && !node.isLeaf) {\n\t\t\tnodes.push(node.copy(normalizeHardBreaks(node.content)));\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Strip \"rich\" marks (link, textStyle, highlight, etc.) while keeping\n * basic marks (bold, italic, strike, underline, code) that messaging\n * channels can represent.\n */\nexport function stripRichMarks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.isText) {\n\t\t\tconst kept = node.marks.filter((m) => BASIC_MARKS.has(m.type.name));\n\t\t\tnodes.push(kept.length === node.marks.length ? node : node.mark(kept));\n\t\t} else {\n\t\t\tnodes.push(node.copy(stripRichMarks(node.content)));\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Parse plain clipboard text into a ProseMirror Slice, preserving blank\n * lines as empty paragraphs. This replaces ProseMirror's default text\n * parser which uses `\\n+` and loses all blank lines.\n *\n * Best for channels with compact paragraph gaps (e.g. WhatsApp) where\n * blank lines need structural representation as empty paragraphs.\n */\nexport function parseClipboardText(text: string, schema: Schema): Slice {\n\tconst paragraphs = text\n\t\t.split('\\n')\n\t\t.map((line) =>\n\t\t\tschema.node('paragraph', null, line ? [schema.text(line)] : []),\n\t\t);\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n\n/**\n * Parse plain clipboard text into a single paragraph with hardBreaks for\n * every `\\n`. Produces the same structure as Gmail's Cmd+Shift+V output:\n * one block element with `<br>` for line breaks and `<br><br>` for blank\n * lines. This gives identical html and text output to Gmail.\n */\nexport function parseClipboardTextAsBreaks(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst trimmed = text.replace(/\\n+$/, '');\n\tif (!trimmed) {\n\t\treturn new Slice(Fragment.from(schema.node('paragraph', null, [])), 1, 1);\n\t}\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst lines = trimmed.split('\\n');\n\tconst children: PMNode[] = [];\n\tlines.forEach((line, i) => {\n\t\tif (line) children.push(schema.text(line));\n\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\tchildren.push(hardBreak.create());\n\t\t}\n\t});\n\treturn new Slice(\n\t\tFragment.from(\n\t\t\tschema.node('paragraph', null, children.length ? children : []),\n\t\t),\n\t\t1,\n\t\t1,\n\t);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BLOCK_SELECTOR","BASIC_MARKS","wrapInlineContent","container","querySelector","children","Array","from","childNodes","groups","i","length","node","HTMLBRElement","push","firstChild","remove","group","p","document","createElement","appendChild","cleanBlockBrs","brs","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","stripTrailingBrs","tag","last","lastElementChild","normalizeHardBreaks","fragment","nodes","forEach","type","name","childCount","_a","create","attrs","_b","lastChild","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","stripRichMarks","isText","kept","marks","filter","m","mark","parseClipboardText","text","schema","paragraphs","split","map","line","Slice","fromArray"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { QueryBuilderNodeProps } from "../../../
|
|
2
|
+
import { QueryBuilderNodeProps } from "../../../types/QueryBuilderNodeProps.type";
|
|
3
3
|
declare const EventsTrigger: React.FC<QueryBuilderNodeProps>;
|
|
4
4
|
export default EventsTrigger;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { QueryBuilderAPIData } from "../../../
|
|
1
|
+
import { QueryBuilderAPIData } from "../../../types/QueryBuilderAPI.type";
|
|
2
2
|
export declare const useIGTriggerNameCacheSelector: () => QueryBuilderAPIData[];
|