/*!
 * Jodit Editor (https://xdsoft.net/jodit/)
 * Released under MIT see LICENSE.txt in the project root for license information.
 * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
 */
import * as consts from "../constants.js";
import { globalDocument, INSEPARABLE_TAGS, LIST_TAGS, MARKER_CLASS, NO_EMPTY_TAGS, TEMP_ATTR } from "../constants.js";
import { toArray } from "../helpers/array/to-array.js";
import { isArray } from "../helpers/checker/is-array.js";
import { isFunction } from "../helpers/checker/is-function.js";
import { isHTML } from "../helpers/checker/is-html.js";
import { isSet } from "../helpers/checker/is-set.js";
import { isString } from "../helpers/checker/is-string.js";
import { isVoid } from "../helpers/checker/is-void.js";
import { trim } from "../helpers/string/trim.js";
import { assert } from "../helpers/utils/assert.js";
import { attr } from "../helpers/utils/attr.js";
import { css } from "../helpers/utils/css.js";
import { dataBind } from "../helpers/utils/data-bind.js";
import { error } from "../helpers/utils/error/index.js";
import { call } from "../helpers/utils/utils.js";
/**
 * Module for working with DOM
 */
export class Dom {
    constructor() {
        throw new Error('Dom is static module');
    }
    /**
     * Remove all content from element
     */
    static detach(node) {
        while (node && node.firstChild) {
            node.removeChild(node.firstChild);
        }
    }
    /**
     * Wrap all inline next siblings
     */
    static wrapNextInline(current, tag, editor) {
        let tmp;
        const first = current;
        let last = current;
        editor.s.save();
        let needFindNext = false;
        do {
            needFindNext = false;
            tmp = last.nextSibling;
            if (tmp && !Dom.isBlock(tmp) && !Dom.isTag(tmp, 'br')) {
                needFindNext = true;
                last = tmp;
            }
        } while (needFindNext);
        return Dom.__wrapElements(tag, editor, first, last);
    }
    static __wrapElements(tag, editor, first, last) {
        const wrapper = isString(tag) ? editor.createInside.element(tag) : tag;
        if (first.parentNode) {
            first.parentNode.insertBefore(wrapper, first);
        }
        let next = first;
        while (next) {
            next = first.nextSibling;
            wrapper.appendChild(first);
            if (first === last || !next) {
                break;
            }
            first = next;
        }
        editor.s.restore();
        return wrapper;
    }
    /**
     * Wrap all inline siblings
     */
    static wrapInline(current, tag, editor) {
        let tmp;
        let first = current;
        let last = current;
        editor.s.save();
        let needFindNext = false;
        do {
            needFindNext = false;
            tmp = first.previousSibling;
            if (tmp && !Dom.isBlock(tmp)) {
                needFindNext = true;
                first = tmp;
            }
        } while (needFindNext);
        do {
            needFindNext = false;
            tmp = last.nextSibling;
            if (tmp && !Dom.isBlock(tmp)) {
                needFindNext = true;
                last = tmp;
            }
        } while (needFindNext);
        return Dom.__wrapElements(tag, editor, first, last);
    }
    /**
     * Wrap node inside another node
     */
    static wrap(current, tag, create) {
        const wrapper = isString(tag) ? create.element(tag) : tag;
        if (Dom.isNode(current)) {
            if (!current.parentNode) {
                throw error('Element should be in DOM');
            }
            current.parentNode.insertBefore(wrapper, current);
            wrapper.appendChild(current);
        }
        else {
            const fragment = current.extractContents();
            current.insertNode(wrapper);
            wrapper.appendChild(fragment);
        }
        return wrapper;
    }
    /**
     * Remove parent of node and insert this node instead that parent
     */
    static unwrap(node) {
        const parent = node.parentNode;
        if (parent) {
            while (node.firstChild) {
                parent.insertBefore(node.firstChild, node);
            }
            Dom.safeRemove(node);
        }
    }
    /**
     * Call functions for all nodes between `start` and `end`
     */
    static between(start, end, callback) {
        let next = start;
        while (next && next !== end) {
            if (start !== next && callback(next)) {
                break;
            }
            let step = next.firstChild || next.nextSibling;
            if (!step) {
                while (next && !next.nextSibling) {
                    next = next.parentNode;
                }
                step = next === null || next === void 0 ? void 0 : next.nextSibling;
            }
            next = step;
        }
    }
    static replace(elm, newTagName, create, withAttributes = false, notMoveContent = false) {
        if (isHTML(newTagName)) {
            assert(create, 'Need create instance for new tag');
            newTagName = create.fromHTML(newTagName);
        }
        let tag;
        if (isString(newTagName)) {
            assert(create, 'Need create instance for new tag');
            tag = create.element(newTagName);
        }
        else {
            tag = newTagName;
        }
        if (!notMoveContent) {
            while (elm.firstChild) {
                tag.appendChild(elm.firstChild);
            }
        }
        if (withAttributes && Dom.isElement(elm) && Dom.isElement(tag)) {
            toArray(elm.attributes).forEach(attr => {
                tag.setAttribute(attr.name, attr.value);
            });
        }
        if (elm.parentNode) {
            elm.parentNode.replaceChild(tag, elm);
        }
        return tag;
    }
    /**
     * Checks whether the Node text and blank (in this case it may contain invisible auxiliary characters ,
     * it is also empty )
     *
     * @param node - The element of wood to be checked
     */
    static isEmptyTextNode(node) {
        return (Dom.isText(node) &&
            (!node.nodeValue ||
                node.nodeValue
                    .replace(consts.INVISIBLE_SPACE_REG_EXP(), '')
                    .trim().length === 0));
    }
    static isEmptyContent(node) {
        return Dom.each(node, (elm) => Dom.isEmptyTextNode(elm));
    }
    /**
     * The node is editable
     */
    static isContentEditable(node, root) {
        return (Dom.isNode(node) &&
            !Dom.closest(node, elm => Dom.isElement(elm) &&
                elm.getAttribute('contenteditable') === 'false', root));
    }
    static isEmpty(node, condNoEmptyElement = NO_EMPTY_TAGS) {
        if (!node) {
            return true;
        }
        let cond;
        if (!isFunction(condNoEmptyElement)) {
            cond = (elm) => condNoEmptyElement.has(elm.nodeName.toLowerCase());
        }
        else {
            cond = condNoEmptyElement;
        }
        const emptyText = (node) => node.nodeValue == null || trim(node.nodeValue).length === 0;
        if (Dom.isText(node)) {
            return emptyText(node);
        }
        return (!(Dom.isElement(node) && cond(node)) &&
            Dom.each(node, (elm) => {
                if ((Dom.isText(elm) && !emptyText(elm)) ||
                    (Dom.isElement(elm) && cond(elm))) {
                    return false;
                }
            }));
    }
    /**
     * Returns true if it is a DOM node
     */
    static isNode(object) {
        // Duck-typing
        return Boolean(object &&
            isString(object.nodeName) &&
            typeof object.nodeType === 'number' &&
            object.childNodes &&
            isFunction(object.appendChild));
    }
    /**
     *  Check if element is table cell
     */
    static isCell(elm) {
        return (Dom.isNode(elm) && (elm.nodeName === 'TD' || elm.nodeName === 'TH'));
    }
    /**
     * Check if element is a list	element UL or OL
     */
    static isList(elm) {
        return Dom.isTag(elm, LIST_TAGS);
    }
    /**
     * Check if element is a part of list	element LI
     */
    static isLeaf(elm) {
        return Dom.isTag(elm, 'li');
    }
    /**
     * Check is element is Image element
     */
    static isImage(elm) {
        return (Dom.isNode(elm) && /^(img|svg|picture|canvas)$/i.test(elm.nodeName));
    }
    /**
     * Check the `node` is a block element
     * @param node - Object to check
     */
    static isBlock(node) {
        return (!isVoid(node) &&
            typeof node === 'object' &&
            Dom.isNode(node) &&
            consts.IS_BLOCK.test(node.nodeName));
    }
    /**
     * Check if element is text node
     */
    static isText(node) {
        return Boolean(node && node.nodeType === Node.TEXT_NODE);
    }
    /**
     * Check if element is comment node
     */
    static isComment(node) {
        return Boolean(node && node.nodeType === Node.COMMENT_NODE);
    }
    /**
     * Check if element is element node
     */
    static isElement(node) {
        if (!Dom.isNode(node)) {
            return false;
        }
        // no `defaultView` requirement — nodes of an inert document
        // (`DOMParser`, `implementation.createHTMLDocument`) are still elements
        return node.nodeType === Node.ELEMENT_NODE;
    }
    /**
     * Check if element is document fragment
     */
    static isFragment(node) {
        var _a;
        if (!Dom.isNode(node)) {
            return false;
        }
        const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
        return Boolean(win && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE);
    }
    /**
     * Check if element is HTMLElement node
     */
    static isHTMLElement(node) {
        var _a;
        if (!Dom.isNode(node)) {
            return false;
        }
        // an inert document has no browsing context (`defaultView` is null),
        // but its nodes are same-realm HTMLElements
        const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
        return node instanceof (win ? win.HTMLElement : HTMLElement);
    }
    /**
     * Check element is inline block
     */
    static isInlineBlock(node) {
        return (Dom.isElement(node) &&
            !/^(BR|HR)$/i.test(node.tagName) &&
            ['inline', 'inline-block'].indexOf(css(node, 'display').toString()) !== -1);
    }
    /**
     * It's block and it can be split
     */
    static canSplitBlock(node) {
        return (!isVoid(node) &&
            Dom.isHTMLElement(node) &&
            Dom.isBlock(node) &&
            !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
            node.style !== undefined &&
            !/^(fixed|absolute)/i.test(node.style.position));
    }
    /**
     * Get first matched node inside root
     */
    static first(root, condition) {
        let first = root === null || root === void 0 ? void 0 : root.firstChild;
        if (!first) {
            return null;
        }
        do {
            if (condition(first)) {
                return first;
            }
            let next = first.firstChild;
            if (!next) {
                next = first.nextSibling;
            }
            if (!next && first.parentNode !== root) {
                do {
                    first = first.parentNode;
                } while (first &&
                    !(first === null || first === void 0 ? void 0 : first.nextSibling) &&
                    first.parentNode !== root);
                next = first === null || first === void 0 ? void 0 : first.nextSibling;
            }
            first = next;
        } while (first);
        return null;
    }
    /**
     * Get last matched node inside root
     */
    static last(root, condition) {
        let last = root === null || root === void 0 ? void 0 : root.lastChild;
        if (!last) {
            return null;
        }
        do {
            if (condition(last)) {
                return last;
            }
            let next = last.lastChild;
            if (!next) {
                next = last.previousSibling;
            }
            if (!next && last.parentNode !== root) {
                do {
                    last = last.parentNode;
                } while (last &&
                    !(last === null || last === void 0 ? void 0 : last.previousSibling) &&
                    last.parentNode !== root);
                next = last === null || last === void 0 ? void 0 : last.previousSibling;
            }
            last = next;
        } while (last);
        return null;
    }
    /**
     * Find previous node
     */
    static prev(node, condition, root, withChild = true) {
        return Dom.find(node, condition, root, false, withChild);
    }
    /**
     * Find next node what `condition(next) === true`
     */
    static next(node, condition, root, withChild = true) {
        return Dom.find(node, condition, root, true, withChild);
    }
    static prevWithClass(node, className) {
        return Dom.prev(node, node => {
            return (Dom.isElement(node) && node.classList.contains(className));
        }, node.parentNode);
    }
    static nextWithClass(node, className) {
        return Dom.next(node, elm => Dom.isElement(elm) && elm.classList.contains(className), node.parentNode);
    }
    /**
     * Find next/prev node what `condition(next) === true`
     */
    static find(node, condition, root, leftToRight = true, withChild = true) {
        const gen = this.nextGen(node, root, leftToRight, withChild);
        let item = gen.next();
        while (!item.done) {
            if (condition(item.value)) {
                return item.value;
            }
            item = gen.next();
        }
        return null;
    }
    /**
     * Find next/prev node what `condition(next) === true`
     */
    static *nextGen(start, root, leftToRight = true, withChild = true) {
        const stack = [];
        let currentNode = start;
        do {
            let next = leftToRight
                ? currentNode.nextSibling
                : currentNode.previousSibling;
            while (next) {
                stack.unshift(next);
                next = leftToRight ? next.nextSibling : next.previousSibling;
            }
            yield* this.runInStack(start, stack, leftToRight, withChild);
            currentNode = currentNode.parentNode;
        } while (currentNode && currentNode !== root);
        return null;
    }
    /**
     * It goes through all the internal elements of the node, causing a callback function
     *
     * @param elm - the element whose children and descendants you want to iterate over
     * @param callback - It called for each item found
     * @example
     * ```javascript
     * Jodit.modules.Dom.each(editor.s.current(), function (node) {
     *  if (node.nodeType === Node.TEXT_NODE) {
     *      node.nodeValue = node.nodeValue.replace(Jodit.INVISIBLE_SPACE_REG_EX, '') // remove all of the text element codes invisible character
     *  }
     * });
     * ```
     */
    static each(elm, callback, leftToRight = true) {
        const gen = this.eachGen(elm, leftToRight);
        let item = gen.next();
        while (!item.done) {
            if (callback(item.value) === false) {
                return false;
            }
            item = gen.next();
        }
        return true;
    }
    static eachGen(root, leftToRight = true) {
        return this.runInStack(root, [root], leftToRight);
    }
    static *runInStack(start, stack, leftToRight, withChild = true) {
        while (stack.length) {
            const item = stack.pop();
            if (withChild) {
                let child = leftToRight ? item.lastChild : item.firstChild;
                while (child) {
                    stack.push(child);
                    child = leftToRight
                        ? child.previousSibling
                        : child.nextSibling;
                }
            }
            if (start !== item) {
                yield item;
            }
        }
    }
    /**
     * Find next/prev node what `condition(next) === true`
     */
    static findWithCurrent(node, condition, root, sibling = 'nextSibling', child = 'firstChild') {
        let next = node;
        do {
            if (condition(next)) {
                return next || null;
            }
            if (child && next && next[child]) {
                const nextOne = Dom.findWithCurrent(next[child], condition, next, sibling, child);
                if (nextOne) {
                    return nextOne;
                }
            }
            while (next && !next[sibling] && next !== root) {
                next = next.parentNode;
            }
            if (next && next[sibling] && next !== root) {
                next = next[sibling];
            }
        } while (next && next !== root);
        return null;
    }
    /**
     * Get not empty sibling
     */
    static findSibling(node, left = true, cond = (n) => !Dom.isEmptyTextNode(n)) {
        let sibling = Dom.sibling(node, left);
        while (sibling && !cond(sibling)) {
            sibling = Dom.sibling(sibling, left);
        }
        return sibling && cond(sibling) ? sibling : null;
    }
    /**
     * Returns the nearest non-empty sibling
     */
    static findNotEmptySibling(node, left) {
        return Dom.findSibling(node, left, n => {
            var _a;
            return (!Dom.isEmptyTextNode(n) &&
                Boolean(!Dom.isText(n) || (((_a = n.nodeValue) === null || _a === void 0 ? void 0 : _a.length) && trim(n.nodeValue))));
        });
    }
    /**
     * Returns the nearest non-empty neighbor
     */
    static findNotEmptyNeighbor(node, left, root) {
        return call(left ? Dom.prev : Dom.next, node, n => Boolean(n &&
            (!(Dom.isText(n) || Dom.isComment(n)) ||
                trim((n === null || n === void 0 ? void 0 : n.nodeValue) || '').length)), root);
    }
    static sibling(node, left) {
        return left ? node.previousSibling : node.nextSibling;
    }
    /**
     * It goes through all the elements in ascending order, and checks to see if they meet the predetermined condition
     */
    static up(node, condition, root, checkRoot = false) {
        let start = node;
        if (!start) {
            return null;
        }
        do {
            if (condition(start)) {
                return start;
            }
            if (start === root || !start.parentNode) {
                break;
            }
            start = start.parentNode;
        } while (start && start !== root);
        if (start === root && checkRoot && condition(start)) {
            return start;
        }
        return null;
    }
    static closest(node, tagsOrCondition, root) {
        let condition;
        const lc = (s) => s.toLowerCase();
        if (isFunction(tagsOrCondition)) {
            condition = tagsOrCondition;
        }
        else if (isArray(tagsOrCondition) || isSet(tagsOrCondition)) {
            const set = isSet(tagsOrCondition)
                ? tagsOrCondition
                : new Set(tagsOrCondition.map(lc));
            condition = (tag) => Boolean(tag && set.has(lc(tag.nodeName)));
        }
        else {
            condition = (tag) => Boolean(tag && lc(tagsOrCondition) === lc(tag.nodeName));
        }
        return Dom.up(node, condition, root);
    }
    /**
     * Furthest parent node matching condition
     */
    static furthest(node, condition, root) {
        let matchedParent = null, current = node === null || node === void 0 ? void 0 : node.parentElement;
        while (current && current !== root) {
            if (condition(current)) {
                matchedParent = current;
            }
            current = current === null || current === void 0 ? void 0 : current.parentElement;
        }
        return matchedParent;
    }
    /**
     * Append new element in the start of root
     */
    static appendChildFirst(root, newElement) {
        const child = root.firstChild;
        if (child) {
            if (child !== newElement) {
                root.insertBefore(newElement, child);
            }
        }
        else {
            root.appendChild(newElement);
        }
    }
    /**
     * Insert newElement after element
     */
    static after(elm, newElement) {
        const { parentNode } = elm;
        if (!parentNode) {
            return;
        }
        if (parentNode.lastChild === elm) {
            parentNode.appendChild(newElement);
        }
        else {
            parentNode.insertBefore(newElement, elm.nextSibling);
        }
    }
    /**
     * Insert newElement before element
     */
    static before(elm, newElement) {
        const { parentNode } = elm;
        if (!parentNode) {
            return;
        }
        parentNode.insertBefore(newElement, elm);
    }
    /**
     * Insert newElement as first child inside element
     */
    static prepend(root, newElement) {
        root.insertBefore(newElement, root.firstChild);
    }
    static append(root, newElement) {
        if (isArray(newElement)) {
            newElement.forEach(node => {
                this.append(root, node);
            });
        }
        else {
            root.appendChild(newElement);
        }
    }
    /**
     * Move all content to another element
     */
    static moveContent(from, to, inStart = false, filter = () => true) {
        const fragment = (from.ownerDocument || globalDocument).createDocumentFragment();
        toArray(from.childNodes)
            .filter(elm => {
            if (filter(elm)) {
                return true;
            }
            Dom.safeRemove(elm);
            return false;
        })
            .forEach((node) => {
            fragment.appendChild(node);
        });
        if (!inStart || !to.firstChild) {
            to.appendChild(fragment);
        }
        else {
            to.insertBefore(fragment, to.firstChild);
        }
    }
    /**
     * Check root contains child or equal child
     */
    static isOrContains(root, child, onlyContains = false) {
        if (root === child) {
            return !onlyContains;
        }
        return Boolean(child && root && this.up(child, nd => nd === root, root, true));
    }
    /**
     * Safe remove element from DOM
     */
    static safeRemove(...nodes) {
        nodes.forEach(node => Dom.isNode(node) &&
            node.parentNode &&
            node.parentNode.removeChild(node));
    }
    static safeInsertNode(range, node) {
        range.collapsed || range.deleteContents();
        const child = Dom.isFragment(node) ? node.lastChild : node;
        if (range.startContainer === range.endContainer &&
            range.collapsed &&
            Dom.isTag(range.startContainer, INSEPARABLE_TAGS)) {
            Dom.after(range.startContainer, node);
        }
        else {
            range.insertNode(node);
            child && range.setStartBefore(child);
        }
        range.collapse(true);
        // https://developer.mozilla.org/en-US/docs/Web/API/Range/insertNode
        // if the new node is to be added to a text Node, that Node is split at the
        // insertion point, and the insertion occurs between the two text nodes.
        [node.nextSibling, node.previousSibling].forEach(n => Dom.isText(n) && !n.nodeValue && Dom.safeRemove(n));
    }
    /**
     * Hide element
     */
    static hide(node) {
        if (!node) {
            return;
        }
        dataBind(node, '__old_display', node.style.display);
        node.style.display = 'none';
    }
    /**
     * Show element
     */
    static show(node) {
        if (!node) {
            return;
        }
        const display = dataBind(node, '__old_display');
        if (node.style.display === 'none') {
            node.style.display = display || '';
        }
    }
    static isTag(node, tagNames) {
        if (!this.isElement(node)) {
            return false;
        }
        const nameL = node.tagName.toLowerCase();
        const nameU = node.tagName.toUpperCase();
        if (tagNames instanceof Set) {
            return tagNames.has(nameL) || tagNames.has(nameU);
        }
        if (Array.isArray(tagNames)) {
            throw new TypeError('Dom.isTag does not support array');
        }
        const tags = tagNames;
        if (nameL === tags || nameU === tags) {
            return true;
        }
        return false;
    }
    /**
     * Marks an item as temporary
     */
    static markTemporary(element, attributes) {
        attributes && attr(element, attributes);
        attr(element, TEMP_ATTR, true);
        return element;
    }
    /**
     * Check if element is temporary
     */
    static isTemporary(element) {
        if (!Dom.isElement(element)) {
            return false;
        }
        return Dom.isMarker(element) || attr(element, TEMP_ATTR) === 'true';
    }
    /**
     * Define element is selection helper
     */
    static isMarker(elm) {
        return (Dom.isNode(elm) &&
            Dom.isTag(elm, 'span') &&
            elm.hasAttribute('data-' + MARKER_CLASS));
    }
    /**
     * Replace temporary elements from string
     */
    static replaceTemporaryFromString(value) {
        return value.replace(/<([a-z]+)[^>]+data-jodit-temp[^>]+>(.+?)<\/\1>/gi, '$2');
    }
    /**
     * Get temporary list
     * @deprecated Just do not use it, it is not needed anymore
     */
    static temporaryList(root) {
        return toArray(root.querySelectorAll(`[${TEMP_ATTR}]`));
    }
}
