import { Fun, Obj, Optional, Optionals } from '@ephox/katamari'; import { Insert, PredicateFind, Remove, SugarElement, SugarNode, Traverse } from '@ephox/sugar'; import Editor from '../api/Editor'; import * as CaretCandidate from '../caret/CaretCandidate'; import * as CaretFinder from '../caret/CaretFinder'; import CaretPosition from '../caret/CaretPosition'; import * as Empty from '../dom/Empty'; import * as NodeType from '../dom/NodeType'; import * as MergeText from './MergeText'; const needsReposition = (pos: CaretPosition, elm: Node): boolean => { const container = pos.container(); const offset = pos.offset(); return !CaretPosition.isTextPosition(pos) && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); }; const reposition = (elm: Node, pos: CaretPosition): CaretPosition => needsReposition(pos, elm) ? CaretPosition(pos.container(), pos.offset() - 1) : pos; const beforeOrStartOf = (node: Node): CaretPosition => NodeType.isText(node) ? CaretPosition(node, 0) : CaretPosition.before(node); const afterOrEndOf = (node: Node): CaretPosition => NodeType.isText(node) ? CaretPosition(node, node.data.length) : CaretPosition.after(node); const getPreviousSiblingCaretPosition = (elm: Node): Optional => { if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { return Optional.some(afterOrEndOf(elm.previousSibling)); } else { return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Optional.none(); } }; const getNextSiblingCaretPosition = (elm: Node): Optional => { if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { return Optional.some(beforeOrStartOf(elm.nextSibling)); } else { return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Optional.none(); } }; const findCaretPositionBackwardsFromElm = (rootElement: Node, elm: Node): Optional => { return Optional.from(elm.previousSibling ? elm.previousSibling : elm.parentNode) .bind((node) => CaretFinder.prevPosition(rootElement, CaretPosition.before(node))) .orThunk(() => CaretFinder.nextPosition(rootElement, CaretPosition.after(elm))); }; const findCaretPositionForwardsFromElm = (rootElement: Node, elm: Node): Optional => CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).orThunk( () => CaretFinder.prevPosition(rootElement, CaretPosition.before(elm)) ); const findCaretPositionBackwards = (rootElement: Node, elm: Node): Optional => getPreviousSiblingCaretPosition(elm).orThunk(() => getNextSiblingCaretPosition(elm)) .orThunk(() => findCaretPositionBackwardsFromElm(rootElement, elm)); const findCaretPositionForward = (rootElement: Node, elm: Node): Optional => getNextSiblingCaretPosition(elm) .orThunk(() => getPreviousSiblingCaretPosition(elm)) .orThunk(() => findCaretPositionForwardsFromElm(rootElement, elm)); const findCaretPosition = (forward: boolean, rootElement: Node, elm: Node): Optional => forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); const findCaretPosOutsideElmAfterDelete = (forward: boolean, rootElement: Node, elm: Node): Optional => findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); const setSelection = (editor: Editor, forward: boolean, pos: Optional): void => { pos.fold( () => { editor.focus(); }, (pos) => { editor.selection.setRng(pos.toRange(), forward); } ); }; const eqRawNode = (rawNode: Node) => (elm: SugarElement): boolean => elm.dom === rawNode; const isBlock = (editor: Editor, elm: SugarElement): boolean => elm && Obj.has(editor.schema.getBlockElements(), SugarNode.name(elm)); const paddEmptyBlock = (elm: SugarElement): Optional => { if (Empty.isEmpty(elm)) { const br = SugarElement.fromHtml('
'); Remove.empty(elm); Insert.append(elm, br); return Optional.some(CaretPosition.before(br.dom)); } else { return Optional.none(); } }; const deleteNormalized = (elm: SugarElement, afterDeletePosOpt: Optional, normalizeWhitespace?: boolean): Optional => { const prevTextOpt = Traverse.prevSibling(elm).filter(SugarNode.isText); const nextTextOpt = Traverse.nextSibling(elm).filter(SugarNode.isText); // Delete the element Remove.remove(elm); // Merge and normalize any prev/next text nodes, so that they are merged and don't lose meaningful whitespace // eg.

a b

->

a &nsbp;b

or

a

->

 a return Optionals.lift3(prevTextOpt, nextTextOpt, afterDeletePosOpt, (prev, next, pos) => { const prevNode = prev.dom, nextNode = next.dom; const offset = prevNode.data.length; MergeText.mergeTextNodes(prevNode, nextNode, normalizeWhitespace); // Update the cursor position if required return pos.container() === nextNode ? CaretPosition(prevNode, offset) : pos; }).orThunk(() => { if (normalizeWhitespace) { prevTextOpt.each((elm) => MergeText.normalizeWhitespaceBefore(elm.dom, elm.dom.length)); nextTextOpt.each((elm) => MergeText.normalizeWhitespaceAfter(elm.dom, 0)); } return afterDeletePosOpt; }); }; const isInlineElement = (editor: Editor, element: SugarElement): boolean => Obj.has(editor.schema.getTextInlineElements(), SugarNode.name(element)); const deleteElement = (editor: Editor, forward: boolean, elm: SugarElement, moveCaret: boolean = true): void => { const afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom); const parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody())); const normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos, isInlineElement(editor, elm)); if (editor.dom.isEmpty(editor.getBody())) { editor.setContent(''); editor.selection.setCursorLocation(); } else { parentBlock.bind(paddEmptyBlock).fold( () => { if (moveCaret) { setSelection(editor, forward, normalizedAfterDeletePos); } }, (paddPos) => { if (moveCaret) { setSelection(editor, forward, Optional.some(paddPos)); } } ); } }; export { deleteElement };