import { Arr, Fun, Optional } from '@ephox/katamari'; import { Compare, Insert, Replication, Remove, SugarElement, Traverse } from '@ephox/sugar'; import * as CaretFinder from '../caret/CaretFinder'; import CaretPosition from '../caret/CaretPosition'; import * as ElementType from '../dom/ElementType'; import * as Empty from '../dom/Empty'; import * as PaddingBr from '../dom/PaddingBr'; import * as Parents from '../dom/Parents'; const getChildrenUntilBlockBoundary = (block: SugarElement): SugarElement[] => { const children = Traverse.children(block); return Arr.findIndex(children, ElementType.isBlock).fold( Fun.constant(children), (index) => children.slice(0, index) ); }; const extractChildren = (block: SugarElement): SugarElement[] => { const children = getChildrenUntilBlockBoundary(block); Arr.each(children, Remove.remove); return children; }; const removeEmptyRoot = (rootNode: SugarElement, block: SugarElement) => { const parents = Parents.parentsAndSelf(block, rootNode); return Arr.find(parents.reverse(), (element) => Empty.isEmpty(element)).each(Remove.remove); }; const isEmptyBefore = (el: SugarElement): boolean => Arr.filter(Traverse.prevSiblings(el), (el) => !Empty.isEmpty(el)).length === 0; const nestedBlockMerge = ( rootNode: SugarElement, fromBlock: SugarElement, toBlock: SugarElement, insertionPoint: SugarElement ): Optional => { if (Empty.isEmpty(toBlock)) { PaddingBr.fillWithPaddingBr(toBlock); return CaretFinder.firstPositionIn(toBlock.dom); } if (isEmptyBefore(insertionPoint) && Empty.isEmpty(fromBlock)) { Insert.before(insertionPoint, SugarElement.fromTag('br')); } const position = CaretFinder.prevPosition(toBlock.dom, CaretPosition.before(insertionPoint.dom)); Arr.each(extractChildren(fromBlock), (child) => { Insert.before(insertionPoint, child); }); removeEmptyRoot(rootNode, fromBlock); return position; }; const sidelongBlockMerge = (rootNode: SugarElement, fromBlock: SugarElement, toBlock: SugarElement): Optional => { if (Empty.isEmpty(toBlock)) { if (Empty.isEmpty(fromBlock)) { const getInlineToBlockDescendants = (el: SugarElement) => { const helper = (node: SugarElement, elements: SugarElement[]): SugarElement[] => Traverse.firstChild(node).fold( () => elements, (child) => ElementType.isInline(child) ? helper(child, elements.concat(Replication.shallow(child))) : elements ); return helper(el, []); }; const newFromBlockDescendants = Arr.foldr( getInlineToBlockDescendants(toBlock), (element: SugarElement, descendant) => { Insert.wrap(element, descendant); return descendant; }, PaddingBr.createPaddingBr() ); Remove.empty(fromBlock); Insert.append(fromBlock, newFromBlockDescendants); } Remove.remove(toBlock); return CaretFinder.firstPositionIn(fromBlock.dom); } const position = CaretFinder.lastPositionIn(toBlock.dom); Arr.each(extractChildren(fromBlock), (child) => { Insert.append(toBlock, child); }); removeEmptyRoot(rootNode, fromBlock); return position; }; const findInsertionPoint = (toBlock: SugarElement, block: SugarElement) => { const parentsAndSelf = Parents.parentsAndSelf(block, toBlock); return Optional.from(parentsAndSelf[parentsAndSelf.length - 1]); }; const getInsertionPoint = (fromBlock: SugarElement, toBlock: SugarElement): Optional => Compare.contains(toBlock, fromBlock) ? findInsertionPoint(toBlock, fromBlock) : Optional.none(); const trimBr = (first: boolean, block: SugarElement) => { CaretFinder.positionIn(first, block.dom) .bind((position) => Optional.from(position.getNode())) .map(SugarElement.fromDom) .filter(ElementType.isBr) .each(Remove.remove); }; const mergeBlockInto = (rootNode: SugarElement, fromBlock: SugarElement, toBlock: SugarElement): Optional => { trimBr(true, fromBlock); trimBr(false, toBlock); return getInsertionPoint(fromBlock, toBlock).fold( Fun.curry(sidelongBlockMerge, rootNode, fromBlock, toBlock), Fun.curry(nestedBlockMerge, rootNode, fromBlock, toBlock) ); }; const mergeBlocks = (rootNode: SugarElement, forward: boolean, block1: SugarElement, block2: SugarElement): Optional => forward ? mergeBlockInto(rootNode, block2, block1) : mergeBlockInto(rootNode, block1, block2); export { mergeBlocks };