"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchViewPlugin = void 0;
const prosemirror_search_1 = require("prosemirror-search");
const state_1 = require("../../../pm/state.js");
const utils_1 = require("../../../pm/utils.js");
const search_1 = require("../../../modules/search/index.js");
const dom_1 = require("../../../utils/dom.js");
const ReactRenderer_1 = require("../ReactRenderer/index.js");
const commands_1 = require("./commands.js");
const const_1 = require("./const.js");
const key_handler_1 = require("./key-handler.js");
const connect_tracker_1 = require("./utils/connect-tracker.js");
const focus_manager_1 = require("./utils/focus-manager.js");
const search_counter_1 = require("./utils/search-counter.js");
require("./search-plugin.css");
const searchViewPlugin = (params) => {
    return new state_1.Plugin({
        key: const_1.pluginKey,
        props: {
            handleKeyDown: key_handler_1.searchKeyHandler,
        },
        state: {
            init: () => ({ open: false }),
            apply(tr, value, _oldState, _newState) {
                const newValue = tr.getMeta(const_1.pluginKey);
                if (typeof newValue === 'object')
                    return newValue;
                return value;
            },
        },
        view(view) {
            return new SeachPluginView(view, params);
        },
    });
};
exports.searchViewPlugin = searchViewPlugin;
class SeachPluginView {
    _view;
    _renderer;
    _focusManager;
    _viewDomTrackerDispose;
    _counter;
    _isDomConnected;
    _viewState;
    _searchState;
    constructor(view, params) {
        this._view = view;
        this._viewState = const_1.pluginKey.getState(view.state);
        this._searchState = (0, prosemirror_search_1.getSearchState)(view.state)?.query;
        this._counter = (0, search_counter_1.getCounter)(view.state);
        this._isDomConnected = view.dom.ownerDocument.contains(view.dom);
        this._renderer = this._createRenderer(params);
        this._focusManager = new focus_manager_1.FocusManager(view.dom.ownerDocument);
        // uses MutationObserver to detect when view.dom is disconnected from the DOM tree
        // TODO: replace with eventBus (subscribe to change-editor-mode event) to track when to hide the search bar
        // see https://github.com/gravity-ui/markdown-editor/issues/884
        this._viewDomTrackerDispose = (0, connect_tracker_1.startTracking)(view.dom, {
            onConnect: this._onEditorViewDomConnected,
            onDisconnect: this._onEditorViewDomDisconnected,
        });
    }
    update() {
        const newCounter = (0, search_counter_1.getCounter)(this._view.state);
        const newViewState = const_1.pluginKey.getState(this._view.state);
        const newSearchState = (0, prosemirror_search_1.getSearchState)(this._view.state)?.query;
        if (newViewState !== this._viewState ||
            newSearchState !== this._searchState ||
            newCounter.total !== this._counter.total ||
            newCounter.current !== this._counter.current) {
            this._counter = newCounter;
            this._viewState = newViewState;
            this._searchState = newSearchState;
            this._renderer.rerender();
        }
    }
    destroy() {
        this._renderer.remove();
        this._viewDomTrackerDispose();
    }
    _onEditorViewDomConnected = () => {
        this._isDomConnected = true;
        this._renderer.rerender();
    };
    _onEditorViewDomDisconnected = () => {
        this._isDomConnected = false;
        this._onClose();
    };
    _createRenderer(params) {
        return (0, ReactRenderer_1.getReactRendererFromState)(this._view.state).createItem('search-view', () => {
            const { _viewState: viewState, _searchState: searchState, _isDomConnected: domConnected, } = this;
            if (!domConnected || !viewState?.open || !searchState)
                return null;
            const { dom: viewDom } = this._view;
            const container = (params.editorSelector && viewDom.closest(params.editorSelector)) ||
                viewDom.ownerDocument;
            const anchor = container.querySelector(params.anchorSelector);
            return (0, search_1.renderSearchPopup)({
                anchor,
                open: viewState.open,
                counter: this._counter,
                state: searchState,
                onClose: this._onClose,
                onChange: this._onChange,
                onSearchPrev: this._onSearchPrev,
                onSearchNext: this._onSearchNext,
                onReplaceNext: this._onReplaceNext,
                onReplaceAll: this._onReplaceAll,
                intersectionTracking: {
                    container: this._view.dom,
                    selector: `.${const_1.SearchClassName.ActiveMatch}`,
                    observerOptions: {
                        childList: true,
                        subtree: true,
                        attributes: true,
                        attributeFilter: ['class'],
                    },
                },
            });
        });
    }
    _onChange = (config) => {
        const { state, dispatch } = this._view;
        const query = new prosemirror_search_1.SearchQuery({
            search: config.search,
            replace: config.replace,
            caseSensitive: config.caseSensitive,
            wholeWord: config.wholeWord,
        });
        const tr = (0, prosemirror_search_1.setSearchState)(state.tr, query);
        const { $from, $to } = tr.selection;
        const parent = (0, utils_1.findParentNodeClosestToPos)($from, (node) => node.type.isTextblock);
        let result = null;
        // find match in [sel.$from, parent.$end]
        if (parent)
            result = query.findNext(state, $from.pos, parent.pos + parent.node.nodeSize);
        // find match in [parent.$start or sel.$from, doc.$end]
        if (!result)
            result = query.findNext(state, parent?.pos || $from.pos);
        // find match in [doc.$start, parent.$start or sel.$to]
        if (!result)
            result = query.findPrev(state, parent?.pos || $to.pos);
        // update text selection
        if (result)
            tr.setSelection(state_1.TextSelection.create(tr.doc, result.from, result.to));
        dispatch(tr);
    };
    _onClose = () => {
        (0, commands_1.closeSearch)(this._view.state, this._view.dispatch);
        this._view.focus();
    };
    _onSearchPrev = () => {
        this._preserveFocus(commands_1.findPrev);
        requestAnimationFrame(() => this._scrollToActiveIfNeeded());
    };
    _onSearchNext = () => {
        this._preserveFocus(commands_1.findNext);
        requestAnimationFrame(() => this._scrollToActiveIfNeeded());
    };
    _onReplaceNext = () => {
        this._preserveFocus(commands_1.replaceNext);
    };
    _onReplaceAll = () => {
        this._preserveFocus(commands_1.replaceAll);
    };
    _preserveFocus(command) {
        this._focusManager.storeFocus();
        this._view.focus();
        command(this._view.state, this._view.dispatch, this._view);
        this._focusManager.restoreFocus({ preventScroll: true });
    }
    _scrollToActiveIfNeeded = () => {
        const activeElem = this._view.dom
            .getElementsByClassName(const_1.SearchClassName.ActiveMatch)
            .item(0);
        if (activeElem && !(0, dom_1.isElementInViewport)(activeElem)) {
            activeElem.scrollIntoView({
                block: 'nearest',
                inline: 'nearest',
            });
        }
    };
}
//# sourceMappingURL=SearchViewPlugin.js.map
