export function closestBlock( node: Node | null, root: HTMLElement ): HTMLElement | null { while (node && node !== root) { if (node instanceof HTMLElement) { const display = getComputedStyle(node).display; if ( node.tagName.match(/^(P|DIV|H1|H2|H3|H4|H5|H6|LI)$/) || display === 'block' || display === 'list-item' ) { return node; } } node = node?.parentNode || null; } return root; } export function setBlockTag(root: HTMLElement, tag: 'p' | 'h1' | 'h2' | 'h3') { const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); const block = closestBlock(range.startContainer, root); if (!block) return; if (block.tagName.toLowerCase() === tag) return; const nb = document.createElement(tag); while (block.firstChild) nb.appendChild(block.firstChild); block.replaceWith(nb); const r = document.createRange(); r.selectNodeContents(nb); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } export function setAlignment(root: HTMLElement, alignment: string) { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const commonAncestor = range.commonAncestorContainer; const blocks: HTMLElement[] = []; const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_ELEMENT, { acceptNode: node => { const el = node as HTMLElement; if ( range.intersectsNode(el) && /^(P|DIV|LI|H[1-6])$/.test(el.tagName) ) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_SKIP; }, } ); let node = walker.nextNode(); while (node) { blocks.push(node as HTMLElement); node = walker.nextNode(); } if (blocks.length === 0) { const block = closestBlock(selection.focusNode, root); if (block) blocks.push(block); } blocks.forEach(block => { block.style.textAlign = alignment; }); } export function nearestElement(n: Node | null): HTMLElement | null { while (n && !(n instanceof HTMLElement)) n = n.parentNode as Node | null; return n as HTMLElement | null; }