Files
tolaria/src/components/titleHeadingInteractions.ts
2026-05-09 18:39:41 +02:00

101 lines
3.2 KiB
TypeScript

import { useCallback, type ClipboardEvent } from 'react'
import { handleFreshListItemPlainTextPaste, type InlineContentEditor } from './freshListItemPaste'
type TitleHeadingEditor = InlineContentEditor & {
document: Array<{ id?: unknown; type?: string }>
setTextCursorPosition: (blockId: string, placement: 'end') => void
}
type EditorActionRunner = (action: () => void) => void
const TITLE_HEADING_SELECTOR = 'h1, [data-content-type="heading"][data-level="1"], [data-content-type="heading"]:not([data-level])'
const TITLE_HEADING_WRAPPER_SELECTOR = '.bn-block-outer, .bn-block'
function eventTargetElement(target: EventTarget | null): HTMLElement | null {
if (target instanceof HTMLElement) return target
return target instanceof Node ? target.parentElement : null
}
function isSelectionInsideElement(element: HTMLElement): boolean {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return false
const anchor = selection.anchorNode
const focus = selection.focusNode
return Boolean(anchor && focus && element.contains(anchor) && element.contains(focus))
}
function findTitleHeadingElement(target: HTMLElement): HTMLElement | null {
const directHeading = target.closest<HTMLElement>(TITLE_HEADING_SELECTOR)
if (directHeading) return directHeading
const titleWrapper = target.closest<HTMLElement>(TITLE_HEADING_WRAPPER_SELECTOR)
return titleWrapper?.querySelector<HTMLElement>(TITLE_HEADING_SELECTOR) ?? null
}
function richClipboardPlainText(clipboardData: DataTransfer): string | null {
const text = clipboardData.getData('text/plain')
const richMimeType = 'text/' + 'html'
const hasRichMarkup = clipboardData.types
? Array.from(clipboardData.types).includes(richMimeType)
: clipboardData.getData(richMimeType).length > 0
return text.length > 0 && hasRichMarkup ? text : null
}
export function useEditorPasteHandler(options: {
editable: boolean
editor: TitleHeadingEditor
runEditorAction: EditorActionRunner
}) {
const { editable, editor, runEditorAction } = options
return useCallback((event: ClipboardEvent<HTMLDivElement>) => {
if (handleFreshListItemPlainTextPaste({
editable,
editor,
event,
runEditorAction,
})) return
if (!editable) return
const target = eventTargetElement(event.target)
if (!target) return
const titleHeading = findTitleHeadingElement(target)
if (!titleHeading || !event.currentTarget.contains(titleHeading)) return
const text = richClipboardPlainText(event.clipboardData)
if (!text) return
event.preventDefault()
runEditorAction(() => {
editor.focus()
editor.insertInlineContent(text, { updateSelection: true })
})
}, [editable, editor, runEditorAction])
}
export function queueTitleHeadingCursorRepair(
target: HTMLElement,
editor: TitleHeadingEditor,
): boolean {
const titleHeading = findTitleHeadingElement(target)
if (!titleHeading) return false
queueMicrotask(() => {
if (isSelectionInsideElement(titleHeading)) return
const firstBlock = editor.document[0]
if (firstBlock?.type !== 'heading' || typeof firstBlock.id !== 'string') return
try {
editor.setTextCursorPosition(firstBlock.id, 'end')
} catch {
return
}
editor.focus()
})
return true
}