import { Fragment, createElement, useEffect, useImperativeHandle, useRef } from 'react' import type { CSSProperties } from 'react' import type { VaultEntry } from '../types' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { NoteTitleIcon } from './NoteTitleIcon' import { getTypeIcon } from './note-item/typeIcon' import type { InlineWikilinkChip, InlineWikilinkSegment, } from './inlineWikilinkText' import type { InlineWikilinkSuggestion } from './inlineWikilinkSuggestions' import { cn } from '@/lib/utils' function withNativeEvent(event: T): T & { nativeEvent: T } { const eventWithNativeEvent = event as T & { nativeEvent?: T } if (!eventWithNativeEvent.nativeEvent) { Object.defineProperty(event, 'nativeEvent', { configurable: true, value: event, }) } return event as T & { nativeEvent: T } } export function InlineWikilinkChipView({ chip, typeEntryMap, }: { chip: InlineWikilinkChip typeEntryMap: Record }) { const typeEntry = chip.entry.isA ? typeEntryMap[chip.entry.isA] : undefined const color = getTypeColor(chip.entry.isA, typeEntry?.color) const backgroundColor = getTypeLightColor(chip.entry.isA, typeEntry?.color) const typeIcon = getTypeIcon(chip.entry.isA, typeEntry?.icon) return ( {chip.entry.icon ? ( ) : ( createElement(typeIcon, { 'aria-hidden': true, width: 11, height: 11, className: 'shrink-0', }) )} {chip.entry.title} ) } function InlineSuggestionRow({ suggestion, selected, onHover, onSelect, typeEntryMap, }: { suggestion: InlineWikilinkSuggestion selected: boolean onHover: () => void onSelect: () => void typeEntryMap: Record }) { const typeEntry = suggestion.entry.isA ? typeEntryMap[suggestion.entry.isA] : undefined const color = getTypeColor(suggestion.entry.isA, typeEntry?.color) const backgroundColor = getTypeLightColor(suggestion.entry.isA, typeEntry?.color) const typeIcon = getTypeIcon(suggestion.entry.isA, typeEntry?.icon) return ( ) } export function InlineWikilinkSuggestionList({ suggestions, selectedIndex, onHover, onSelect, typeEntryMap, variant = 'floating', emptyLabel = 'No matching notes', }: { suggestions: InlineWikilinkSuggestion[] selectedIndex: number onHover: (index: number) => void onSelect: (index: number) => void typeEntryMap: Record variant?: 'floating' | 'palette' emptyLabel?: string }) { if (suggestions.length === 0) { return (
{emptyLabel}
) } return (
{suggestions.map((suggestion, index) => ( onHover(index)} onSelect={() => onSelect(index)} typeEntryMap={typeEntryMap} /> ))}
) } export function InlineWikilinkEditorField({ value, placeholder, disabled, inputRef, dataTestId, placeholderClassName, editorClassName, editorStyle, onCompositionEnd, onCompositionStart, onInput, onKeyDown, onCut, onDrop, onPaste, onSelectionChange, segments, typeEntryMap, }: { value: string placeholder?: string disabled: boolean inputRef: React.Ref dataTestId: string placeholderClassName?: string editorClassName?: string editorStyle?: CSSProperties onCompositionEnd: (editor: HTMLDivElement) => void onCompositionStart: () => void onInput: () => void onKeyDown: (event: React.KeyboardEvent) => void onCut: (event: React.ClipboardEvent) => void onDrop: (event: React.DragEvent) => void onPaste: (event: React.ClipboardEvent) => void onSelectionChange: () => void segments: InlineWikilinkSegment[] typeEntryMap: Record }) { const editorRef = useRef(null) const needsTrailingCaretAnchor = segments[segments.length - 1]?.kind === 'chip' useImperativeHandle(inputRef, () => editorRef.current as HTMLDivElement, []) useInlineWikilinkPlaceholder(editorRef, placeholder) useInlineWikilinkEditorEvents(editorRef, { onCompositionEnd, onCompositionStart, onCut, onDrop, onInput, onKeyDown, onPaste, onSelectionChange, }) return (
{value.length === 0 && placeholder && (
{placeholder}
)}
{segments.map((segment) => renderInlineWikilinkSegment(segment, typeEntryMap))} {needsTrailingCaretAnchor ? '\u200B' : null}
) } type InlineWikilinkEditorHandlers = Pick< Parameters[0], | 'onCompositionEnd' | 'onCompositionStart' | 'onCut' | 'onDrop' | 'onInput' | 'onKeyDown' | 'onPaste' | 'onSelectionChange' > function useInlineWikilinkPlaceholder(editorRef: React.RefObject, placeholder?: string) { useEffect(() => { const editor = editorRef.current if (!editor) return syncPlaceholderAttribute(editor, placeholder) }, [editorRef, placeholder]) } function syncPlaceholderAttribute(editor: HTMLDivElement, placeholder?: string) { if (placeholder) { editor.setAttribute('aria-placeholder', placeholder) return } editor.removeAttribute('aria-placeholder') } function useInlineWikilinkEditorEvents( editorRef: React.RefObject, handlers: InlineWikilinkEditorHandlers, ) { useEffect(() => { const editor = editorRef.current if (!editor) return const listenerMap = inlineWikilinkEditorListenerMap(handlers) for (const [eventName, listener] of listenerMap) editor.addEventListener(eventName, listener) return () => { for (const [eventName, listener] of listenerMap) editor.removeEventListener(eventName, listener) } }, [editorRef, handlers]) } function inlineWikilinkEditorListenerMap({ onCompositionEnd, onCompositionStart, onCut, onDrop, onInput, onKeyDown, onPaste, onSelectionChange, }: InlineWikilinkEditorHandlers): Array<[keyof HTMLElementEventMap, EventListener]> { const handleSelectionChange = () => onSelectionChange() return [ ['compositionstart', () => onCompositionStart()], ['compositionend', (event) => onCompositionEnd(event.currentTarget as HTMLDivElement)], ['input', () => onInput()], ['keydown', (event) => onKeyDown(withNativeEvent(event) as unknown as React.KeyboardEvent)], ['cut', (event) => onCut(withNativeEvent(event) as unknown as React.ClipboardEvent)], ['drop', (event) => onDrop(withNativeEvent(event) as unknown as React.DragEvent)], ['paste', (event) => onPaste(withNativeEvent(event) as unknown as React.ClipboardEvent)], ['click', handleSelectionChange], ['keyup', handleSelectionChange], ['mouseup', handleSelectionChange], ] } function renderInlineWikilinkSegment(segment: InlineWikilinkSegment, typeEntryMap: Record) { if (segment.kind === 'text') return {segment.text} return ( ) } export function InlineWikilinkPaletteLayout({ header, editor, suggestionList, emptyState, footer, }: { header?: React.ReactNode editor: React.ReactNode suggestionList: React.ReactNode emptyState?: React.ReactNode footer?: React.ReactNode }) { return ( <>
{header} {editor}
{suggestionList ?? emptyState}
{footer} ) }