export function getSelection(state: any) { return state.selection } export function getSelectionRange(state: any) { return state.range } export function isStyleActive(style: string, doc: Document) { const selection = doc.getSelection() if (!selection || !selection.rangeCount) { return false } const range = selection.getRangeAt(0) const container = range.commonAncestorContainer const parent = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element if (!parent) { return false } // Check if the current node or any parent has the style const checkParent = (element: Element | null, checker: (el: Element) => boolean): boolean => { if (!element) { return false } if (checker(element)) { return true } return checkParent(element.parentElement, checker) } // Define style checkers for different formatting types const styleCheckers: { [key: string]: (el: Element) => boolean } = { // Text formatting - check for elements only, not CSS styles bold: (el) => { const tagName = el.tagName?.toLowerCase() // Only consider and tags, not CSS bold styling return tagName === 'strong' || tagName === 'b' }, italic: (el) => { const tagName = el.tagName?.toLowerCase() // Only consider and tags, not CSS italic styling return tagName === 'em' || tagName === 'i' }, underline: (el) => { const tagName = el.tagName?.toLowerCase() // Only consider tag, not CSS underline styling return tagName === 'u' }, // Block elements h1: el => el.tagName?.toLowerCase() === 'h1', h2: el => el.tagName?.toLowerCase() === 'h2', h3: el => el.tagName?.toLowerCase() === 'h3', h4: el => el.tagName?.toLowerCase() === 'h4', h5: el => el.tagName?.toLowerCase() === 'h5', h6: el => el.tagName?.toLowerCase() === 'h6', p: el => el.tagName?.toLowerCase() === 'p', blockquote: el => el.tagName?.toLowerCase() === 'blockquote', // List elements - check if we're inside a list orderedList: (el) => { // Check if we're inside an ordered list return !!el.closest('ol') }, unorderedList: (el) => { // Check if we're inside an unordered list return !!el.closest('ul') }, // Text alignment alignLeft: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.style.textAlign === 'start' }, alignCenter: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.style.textAlign === 'center' }, alignRight: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.style.textAlign === 'end' }, alignJustify: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.style.textAlign === 'justify' }, // Text direction textDirection: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.dir === 'rtl' }, ltrDirection: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.dir === 'ltr' }, rtlDirection: (el) => { const paragraph = el.closest('p, h1, h2, h3, h4, h5, h6') return (paragraph as HTMLElement)?.dir === 'rtl' } } // Special handling for view state commands if (['splitView', 'codeView', 'fullScreen'].includes(style)) { return false // These are handled by the editor state directly } // Use the appropriate checker const checker = styleCheckers[style] if (checker) { return checkParent(parent, checker) } // Fallback: check if element matches the style tag name return checkParent(parent, el => el.tagName?.toLowerCase() === style.toLowerCase()) } export interface SelectionInfo { startBlock: Element endBlock: Element isMultiBlock: boolean originalStart: Node originalEnd: Node originalStartOffset: number originalEndOffset: number parent: Element } export function analyzeSelection(doc: Document, range: Range): SelectionInfo | null { const container = range.commonAncestorContainer const parent = container.nodeType === 3 ? container.parentElement : container as Element if (!parent) { return null } const startBlock = parent.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || parent // Analyze if selection spans multiple blocks const isMultiBlock = (() => { if (!range.collapsed) { const { endContainer } = range const endParent = endContainer.nodeType === 3 ? endContainer.parentElement : endContainer as Element const endBlock = endParent?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || endParent return startBlock !== endBlock } return false })() const { endContainer } = range const endParent = endContainer.nodeType === 3 ? endContainer.parentElement : endContainer as Element const endBlock = endParent?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || endParent if (!endBlock) { return null } return { startBlock, endBlock, isMultiBlock, originalStart: range.startContainer, originalEnd: range.endContainer, originalStartOffset: range.startOffset, originalEndOffset: range.endOffset, parent } } export function restoreSelection( doc: Document, range: Range, selection: Selection, info: SelectionInfo, fallbackNode?: Node ) { try { range.setStart(info.originalStart, info.originalStartOffset) range.setEnd(info.originalEnd, info.originalEndOffset) } catch { if (fallbackNode) { range.selectNodeContents(fallbackNode) } } selection.removeAllRanges() selection.addRange(range) } export function getBlocksBetween(startBlock: Element, endBlock: Element): Element[] { const blocks = [] let currentBlock = startBlock while (currentBlock) { blocks.push(currentBlock) if (currentBlock === endBlock) { break } currentBlock = currentBlock.nextElementSibling as Element } return blocks }