import { Strings } from '@ephox/katamari';
import DOMUtils from '../api/dom/DOMUtils';
import Editor from '../api/Editor';
import Tools from '../api/util/Tools';
import * as Bookmarks from '../bookmark/Bookmarks';
import ElementUtils from '../dom/ElementUtils';
import * as NodeType from '../dom/NodeType';
import { isCaretNode } from './FormatContainer';
import { ApplyFormat, FormatVars } from './FormatTypes';
import * as FormatUtils from './FormatUtils';
const each = Tools.each;
const isElementNode = (node: Node): node is HTMLElement =>
NodeType.isElement(node) && !Bookmarks.isBookmarkNode(node) && !isCaretNode(node) && !NodeType.isBogus(node);
const findElementSibling = (node: Node, siblingName: 'nextSibling' | 'previousSibling') => {
for (let sibling: Node | null = node; sibling; sibling = sibling[siblingName]) {
if (NodeType.isText(sibling) && Strings.isNotEmpty(sibling.data)) {
return node;
}
if (NodeType.isElement(sibling) && !Bookmarks.isBookmarkNode(sibling)) {
return sibling;
}
}
return node;
};
const mergeSiblingsNodes = (editor: Editor, prev: Node | undefined, next: Node | undefined) => {
const elementUtils = ElementUtils(editor);
const isPrevEditable = NodeType.isElement(prev) && FormatUtils.isEditable(prev);
const isNextEditable = NodeType.isElement(next) && FormatUtils.isEditable(next);
// Check if next/prev exists and that they are elements
if (isPrevEditable && isNextEditable) {
// If previous sibling is empty then jump over it
const prevSibling = findElementSibling(prev, 'previousSibling');
const nextSibling = findElementSibling(next, 'nextSibling');
// Compare next and previous nodes
if (elementUtils.compare(prevSibling, nextSibling)) {
// Append nodes between
for (let sibling = prevSibling.nextSibling; sibling && sibling !== nextSibling;) {
const tmpSibling = sibling;
sibling = sibling.nextSibling;
prevSibling.appendChild(tmpSibling);
}
editor.dom.remove(nextSibling);
Tools.each(Tools.grep(nextSibling.childNodes), (node) => {
prevSibling.appendChild(node);
});
return prevSibling;
}
}
return next;
};
const mergeSiblings = (editor: Editor, format: ApplyFormat, vars: FormatVars | undefined, node: Node): void => {
// Merge next and previous siblings if they are similar texttext becomes texttext
// Note: mergeSiblingNodes attempts to not merge sibilings if they are noneditable
if (node && format.merge_siblings !== false) {
// Previous sibling
const newNode = mergeSiblingsNodes(editor, FormatUtils.getNonWhiteSpaceSibling(node), node) ?? node;
// Next sibling
mergeSiblingsNodes(editor, newNode, FormatUtils.getNonWhiteSpaceSibling(newNode, true));
}
};
const clearChildStyles = (dom: DOMUtils, format: ApplyFormat, node: Node): void => {
if (format.clear_child_styles) {
const selector = format.links ? '*:not(a)' : '*';
each(dom.select(selector, node), (childNode) => {
if (isElementNode(childNode) && FormatUtils.isEditable(childNode)) {
each(format.styles, (_value, name: string) => {
dom.setStyle(childNode, name, '');
});
}
});
}
};
const processChildElements = (node: Node, filter: (element: HTMLElement) => boolean, process: (element: HTMLElement) => void): void => {
each(node.childNodes, (node) => {
if (isElementNode(node)) {
if (filter(node)) {
process(node);
}
if (node.hasChildNodes()) {
processChildElements(node, filter, process);
}
}
});
};
const unwrapEmptySpan = (dom: DOMUtils, node: Node) => {
if (node.nodeName === 'SPAN' && dom.getAttribs(node as HTMLSpanElement).length === 0) {
dom.remove(node, true);
}
};
const hasStyle = (dom: DOMUtils, name: string) => (node: Element): boolean =>
!!(node && FormatUtils.getStyle(dom, node, name));
const applyStyle = (dom: DOMUtils, name: string, value: string | null) => (node: Element): void => {
dom.setStyle(node, name, value);
if (node.getAttribute('style') === '') {
node.removeAttribute('style');
}
unwrapEmptySpan(dom, node);
};
export {
applyStyle,
clearChildStyles,
hasStyle,
isElementNode,
mergeSiblings,
processChildElements
};