export function toggleList(root: HTMLElement, kind: 'ul' | 'ol') { const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); function styleList(list: HTMLElement) { list.style.margin = '0'; } function isEmptyBlock(el: HTMLElement) { return !el.textContent?.trim() && !el.querySelector('img,br'); } function setCursorInside(el: HTMLElement) { const selection = document.getSelection(); if (!selection) return; const r = document.createRange(); r.selectNodeContents(el); r.collapse(true); selection.removeAllRanges(); selection.addRange(r); } function closestBlock(node: Node | null): HTMLElement | null { while (node && node !== root) { if (node instanceof HTMLElement) { const display = getComputedStyle(node).display; if ( node.tagName.match(/^(P|DIV|H[1-6]|LI)$/) || display === 'block' || display === 'list-item' ) return node; } node = node.parentNode; } return null; } function unwrapList(list: HTMLElement) { const frag = document.createDocumentFragment(); (Array.from(list.querySelectorAll('li')) as HTMLElement[]).forEach(li => { const p = document.createElement('p'); if (li.style.textAlign) p.style.textAlign = li.style.textAlign; while (li.firstChild) p.appendChild(li.firstChild); frag.appendChild(p); }); list.replaceWith(frag); } function unwrapListItem(block: HTMLElement) { const li = block.closest('li'); if (!li) return; const list = li.parentElement as HTMLElement; const p = document.createElement('p'); if (li.style.textAlign) p.style.textAlign = li.style.textAlign; while (li.firstChild) p.appendChild(li.firstChild); list.parentNode?.insertBefore(p, list.nextSibling); li.remove(); if (list.childNodes.length === 0) list.remove(); setCursorInside(p); } function switchListType(list: HTMLElement, newType: 'ul' | 'ol') { const newList = document.createElement(newType); styleList(newList); while (list.firstChild) newList.appendChild(list.firstChild); list.replaceWith(newList); } if (range.collapsed) { const block = closestBlock(range.startContainer); const insideList = block?.closest('ul,ol') as HTMLElement | null; if (insideList && insideList.tagName.toLowerCase() === kind) { unwrapListItem(block as HTMLElement); return; } if (insideList && insideList.tagName.toLowerCase() !== kind) { switchListType(insideList, kind); return; } if ( !block || block === root || block.childNodes.length === 0 || isEmptyBlock(block) ) { const list = document.createElement(kind); const li = document.createElement('li'); li.appendChild(document.createElement('br')); list.appendChild(li); styleList(list); range.insertNode(list); setCursorInside(li); return; } const list = document.createElement(kind); styleList(list); const li = document.createElement('li'); if (block.style.textAlign) li.style.textAlign = block.style.textAlign; while (block.firstChild) li.appendChild(block.firstChild); list.appendChild(li); block.replaceWith(list); setCursorInside(li); return; } const blocks: HTMLElement[] = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { acceptNode: n => { if (!(n instanceof HTMLElement)) return NodeFilter.FILTER_SKIP; if (!/^(P|DIV|LI|H[1-6])$/.test(n.tagName)) return NodeFilter.FILTER_SKIP; const r = document.createRange(); r.selectNodeContents(n); const intersects = range.compareBoundaryPoints(Range.END_TO_START, r) < 0 && range.compareBoundaryPoints(Range.START_TO_END, r) > 0; return intersects ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, }); let n: Node | null; while ((n = walker.nextNode())) blocks.push(n as HTMLElement); if (blocks.length === 0) { const block = closestBlock(range.startContainer); if (block) blocks.push(block); } const firstLi = blocks[0].closest('li'); if (firstLi) { const currentList = firstLi.closest('ul,ol') as HTMLElement | null; if (!currentList) return; if (currentList.tagName.toLowerCase() === kind) unwrapList(currentList); else switchListType(currentList, kind); return; } const newList = document.createElement(kind); styleList(newList); for (const block of blocks) { const li = document.createElement('li'); if (block.style.textAlign) li.style.textAlign = block.style.textAlign; while (block.firstChild) li.appendChild(block.firstChild); newList.appendChild(li); } blocks[0].replaceWith(newList); for (let i = 1; i < blocks.length; i++) blocks[i].remove(); }