import { Type } from '@ephox/katamari'; import Editor from '../api/Editor'; import * as NodeType from '../dom/NodeType'; import * as NewLineUtils from './NewLineUtils'; const hasFirstChild = (elm: Node, name: string) => { return elm.firstChild && elm.firstChild.nodeName === name; }; const isFirstChild = (elm: Node) => { return elm.parentNode?.firstChild === elm; }; const hasParent = (elm: Node | null, parentName: string): boolean => { const parentNode = elm?.parentNode; return Type.isNonNullable(parentNode) && parentNode.nodeName === parentName; }; const isListBlock = (elm: Node | null): elm is HTMLElement => { return Type.isNonNullable(elm) && /^(OL|UL|LI)$/.test(elm.nodeName); }; const isListItem = (elm: Node | null): elm is HTMLElement => { return Type.isNonNullable(elm) && /^(LI|DT|DD)$/.test(elm.nodeName); }; const isNestedList = (elm: Element) => { return isListBlock(elm) && isListBlock(elm.parentNode); }; const getContainerBlock = (containerBlock: Element): Element => { const containerBlockParent = containerBlock.parentNode; return isListItem(containerBlockParent) ? containerBlockParent : containerBlock; }; const isFirstOrLastLi = (containerBlock: Element, parentBlock: Element, first: boolean) => { let node = containerBlock[first ? 'firstChild' : 'lastChild']; // Find first/last element since there might be whitespace there while (node) { if (NodeType.isElement(node)) { break; } node = node[first ? 'nextSibling' : 'previousSibling']; } return node === parentBlock; }; // Inserts a block or br before/after or in the middle of a split list of the LI is empty const insert = (editor: Editor, createNewBlock: (name: string) => Element, containerBlock: Element, parentBlock: Element, newBlockName: string): void => { const dom = editor.dom; const rng = editor.selection.getRng(); const containerParent = containerBlock.parentNode; if (containerBlock === editor.getBody() || !containerParent) { return; } if (isNestedList(containerBlock)) { newBlockName = 'LI'; } let newBlock = createNewBlock(newBlockName); if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { if (hasParent(containerBlock, 'LI')) { // Nested list is inside a LI const containerBlockParent = getContainerBlock(containerBlock); dom.insertAfter(newBlock, containerBlockParent); if (isFirstChild(containerBlock)) { dom.remove(containerBlockParent); } else { dom.remove(containerBlock); } } else { // Is first and last list item then replace the OL/UL with a text block dom.replace(newBlock, containerBlock); } } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { if (hasParent(containerBlock, 'LI')) { // List nested in an LI then move the list to a new sibling LI dom.insertAfter(newBlock, getContainerBlock(containerBlock)); newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed newBlock.appendChild(containerBlock); } else { // First LI in list then remove LI and add text block before list containerParent.insertBefore(newBlock, containerBlock); } dom.remove(parentBlock); } else if (isFirstOrLastLi(containerBlock, parentBlock, false)) { // Last LI in list then remove LI and add text block after list dom.insertAfter(newBlock, getContainerBlock(containerBlock)); dom.remove(parentBlock); } else { // Middle LI in list then split the list and insert a text block in the middle // Extract after fragment and insert it after the current block containerBlock = getContainerBlock(containerBlock); const tmpRng = rng.cloneRange(); tmpRng.setStartAfter(parentBlock); tmpRng.setEndAfter(containerBlock); const fragment = tmpRng.extractContents(); if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { newBlock = fragment.firstChild as HTMLLIElement; dom.insertAfter(fragment, containerBlock); } else { dom.insertAfter(fragment, containerBlock); dom.insertAfter(newBlock, containerBlock); } dom.remove(parentBlock); } NewLineUtils.moveToCaretPosition(editor, newBlock); }; export { insert };