/**
 * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module find-and-replace/ui/findandreplaceformview
 */
import { View, ButtonView, LabeledFieldView, FocusCycler, createLabeledInputText, submitHandler, ViewCollection, SwitchButtonView, CollapsibleView } from 'ckeditor5/src/ui.js';
import { FocusTracker, KeystrokeHandler, Rect, isVisible } from 'ckeditor5/src/utils.js';
// See: #8833.
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
import '../../theme/findandreplaceform.css';
import { icons } from 'ckeditor5/src/core.js';
/**
 * The find and replace form view class.
 *
 * See {@link module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView}.
 */
export default class FindAndReplaceFormView extends View {
    /**
     * Creates a view of find and replace form.
     *
     * @param locale The localization services instance.
     */
    constructor(locale) {
        super(locale);
        const t = locale.t;
        this.children = this.createCollection();
        this.set('matchCount', 0);
        this.set('highlightOffset', 0);
        this.set('isDirty', false);
        this.set('_areCommandsEnabled', {});
        this.set('_resultsCounterText', '');
        this.set('_matchCase', false);
        this.set('_wholeWordsOnly', false);
        this.bind('_searchResultsFound').to(this, 'matchCount', this, 'isDirty', (matchCount, isDirty) => {
            return matchCount > 0 && !isDirty;
        });
        this._findInputView = this._createInputField(t('Find in text…'));
        this._findPrevButtonView = this._createButton({
            label: t('Previous result'),
            class: 'ck-button-prev',
            icon: icons.previousArrow,
            keystroke: 'Shift+F3',
            tooltip: true
        });
        this._findNextButtonView = this._createButton({
            label: t('Next result'),
            class: 'ck-button-next',
            icon: icons.previousArrow,
            keystroke: 'F3',
            tooltip: true
        });
        this._replaceInputView = this._createInputField(t('Replace with…'), 'ck-labeled-field-replace');
        this._inputsDivView = this._createInputsDiv();
        this._matchCaseSwitchView = this._createMatchCaseSwitch();
        this._wholeWordsOnlySwitchView = this._createWholeWordsOnlySwitch();
        this._advancedOptionsCollapsibleView = this._createAdvancedOptionsCollapsible();
        this._replaceAllButtonView = this._createButton({
            label: t('Replace all'),
            class: 'ck-button-replaceall',
            withText: true
        });
        this._replaceButtonView = this._createButton({
            label: t('Replace'),
            class: 'ck-button-replace',
            withText: true
        });
        this._findButtonView = this._createButton({
            label: t('Find'),
            class: 'ck-button-find ck-button-action',
            withText: true
        });
        this._actionButtonsDivView = this._createActionButtonsDiv();
        this._focusTracker = new FocusTracker();
        this._keystrokes = new KeystrokeHandler();
        this._focusables = new ViewCollection();
        this.focusCycler = new FocusCycler({
            focusables: this._focusables,
            focusTracker: this._focusTracker,
            keystrokeHandler: this._keystrokes,
            actions: {
                // Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
                focusPrevious: 'shift + tab',
                // Navigate form fields forwards using the <kbd>Tab</kbd> key.
                focusNext: 'tab'
            }
        });
        this.children.addMany([
            this._inputsDivView,
            this._advancedOptionsCollapsibleView,
            this._actionButtonsDivView
        ]);
        this.setTemplate({
            tag: 'form',
            attributes: {
                class: [
                    'ck',
                    'ck-find-and-replace-form'
                ],
                tabindex: '-1'
            },
            children: this.children
        });
    }
    /**
     * @inheritDoc
     */
    render() {
        super.render();
        submitHandler({ view: this });
        this._initFocusCycling();
        this._initKeystrokeHandling();
    }
    /**
     * @inheritDoc
     */
    destroy() {
        super.destroy();
        this._focusTracker.destroy();
        this._keystrokes.destroy();
    }
    /**
     * @inheritDoc
     */
    focus(direction) {
        if (direction === -1) {
            this.focusCycler.focusLast();
        }
        else {
            this.focusCycler.focusFirst();
        }
    }
    /**
     * Resets the form before re-appearing.
     *
     * It clears error messages, hides the match counter and disables the replace feature
     * until the next hit of the "Find" button.
     *
     * **Note**: It does not reset inputs and options, though. This way the form works better in editors with
     * disappearing toolbar (e.g. BalloonEditor): hiding the toolbar by accident (together with the find and replace UI)
     * does not require filling the entire form again.
     */
    reset() {
        this._findInputView.errorText = null;
        this.isDirty = true;
    }
    /**
     * Returns the value of the find input.
     */
    get _textToFind() {
        return this._findInputView.fieldView.element.value;
    }
    /**
     * Returns the value of the replace input.
     */
    get _textToReplace() {
        return this._replaceInputView.fieldView.element.value;
    }
    /**
     * Configures and returns the `<div>` aggregating all form inputs.
     */
    _createInputsDiv() {
        const locale = this.locale;
        const t = locale.t;
        const inputsDivView = new View(locale);
        // Typing in the find field invalidates all previous results (the form is "dirty").
        this._findInputView.fieldView.on('input', () => {
            this.isDirty = true;
        });
        // Pressing prev/next buttons fires related event on the form.
        this._findPrevButtonView.delegate('execute').to(this, 'findPrevious');
        this._findNextButtonView.delegate('execute').to(this, 'findNext');
        // Prev/next buttons will be disabled when related editor command gets disabled.
        this._findPrevButtonView.bind('isEnabled').to(this, '_areCommandsEnabled', ({ findPrevious }) => findPrevious);
        this._findNextButtonView.bind('isEnabled').to(this, '_areCommandsEnabled', ({ findNext }) => findNext);
        this._injectFindResultsCounter();
        this._replaceInputView.bind('isEnabled').to(this, '_areCommandsEnabled', this, '_searchResultsFound', ({ replace }, resultsFound) => replace && resultsFound);
        this._replaceInputView.bind('infoText').to(this._replaceInputView, 'isEnabled', this._replaceInputView, 'isFocused', (isEnabled, isFocused) => {
            if (isEnabled || !isFocused) {
                return '';
            }
            return t('Tip: Find some text first in order to replace it.');
        });
        inputsDivView.setTemplate({
            tag: 'div',
            attributes: {
                class: ['ck', 'ck-find-and-replace-form__inputs']
            },
            children: [
                this._findInputView,
                this._findPrevButtonView,
                this._findNextButtonView,
                this._replaceInputView
            ]
        });
        return inputsDivView;
    }
    /**
     * The action performed when the {@link #_findButtonView} is pressed.
     */
    _onFindButtonExecute() {
        // When hitting "Find" in an empty input, an error should be displayed.
        // Also, if the form was "dirty", it should remain so.
        if (!this._textToFind) {
            const t = this.t;
            this._findInputView.errorText = t('Text to find must not be empty.');
            return;
        }
        // Hitting "Find" automatically clears the dirty state.
        this.isDirty = false;
        this.fire('findNext', {
            searchText: this._textToFind,
            matchCase: this._matchCase,
            wholeWords: this._wholeWordsOnly
        });
    }
    /**
     * Configures an injects the find results counter displaying a "N of M" label of the {@link #_findInputView}.
     */
    _injectFindResultsCounter() {
        const locale = this.locale;
        const t = locale.t;
        const bind = this.bindTemplate;
        const resultsCounterView = new View(this.locale);
        this.bind('_resultsCounterText').to(this, 'highlightOffset', this, 'matchCount', (highlightOffset, matchCount) => t('%0 of %1', [highlightOffset, matchCount]));
        resultsCounterView.setTemplate({
            tag: 'span',
            attributes: {
                class: [
                    'ck',
                    'ck-results-counter',
                    // The counter only makes sense when the field text corresponds to search results in the editing.
                    bind.if('isDirty', 'ck-hidden')
                ]
            },
            children: [
                {
                    text: bind.to('_resultsCounterText')
                }
            ]
        });
        // The whole idea is that when the text of the counter changes, its width also increases/decreases and
        // it consumes more or less space over the input. The input, on the other hand, should adjust it's right
        // padding so its *entire* text always remains visible and available to the user.
        const updateFindInputPadding = () => {
            const inputElement = this._findInputView.fieldView.element;
            // Don't adjust the padding if the input (also: counter) were not rendered or not inserted into DOM yet.
            if (!inputElement || !isVisible(inputElement)) {
                return;
            }
            const counterWidth = new Rect(resultsCounterView.element).width;
            const paddingPropertyName = locale.uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
            if (!counterWidth) {
                inputElement.style[paddingPropertyName] = '';
            }
            else {
                inputElement.style[paddingPropertyName] = `calc( 2 * var(--ck-spacing-standard) + ${counterWidth}px )`;
            }
        };
        // Adjust the input padding when the text of the counter changes, for instance "1 of 200" is narrower than "123 of 200".
        // Using "low" priority to let the text be set by the template binding first.
        this.on('change:_resultsCounterText', updateFindInputPadding, { priority: 'low' });
        // Adjust the input padding when the counter shows or hides. When hidden, there should be no padding. When it shows, the
        // padding should be set according to the text of the counter.
        // Using "low" priority to let the text be set by the template binding first.
        this.on('change:isDirty', updateFindInputPadding, { priority: 'low' });
        // Put the counter element next to the <input> in the find field.
        this._findInputView.template.children[0].children.push(resultsCounterView);
    }
    /**
     * Creates the collapsible view aggregating the advanced search options.
     */
    _createAdvancedOptionsCollapsible() {
        const t = this.locale.t;
        const collapsible = new CollapsibleView(this.locale, [
            this._matchCaseSwitchView,
            this._wholeWordsOnlySwitchView
        ]);
        collapsible.set({
            label: t('Advanced options'),
            isCollapsed: true
        });
        return collapsible;
    }
    /**
     * Configures and returns the `<div>` element aggregating all form action buttons.
     */
    _createActionButtonsDiv() {
        const actionsDivView = new View(this.locale);
        this._replaceButtonView.bind('isEnabled').to(this, '_areCommandsEnabled', this, '_searchResultsFound', ({ replace }, resultsFound) => replace && resultsFound);
        this._replaceAllButtonView.bind('isEnabled').to(this, '_areCommandsEnabled', this, '_searchResultsFound', ({ replaceAll }, resultsFound) => replaceAll && resultsFound);
        this._replaceButtonView.on('execute', () => {
            this.fire('replace', {
                searchText: this._textToFind,
                replaceText: this._textToReplace
            });
        });
        this._replaceAllButtonView.on('execute', () => {
            this.fire('replaceAll', {
                searchText: this._textToFind,
                replaceText: this._textToReplace
            });
            this.focus();
        });
        this._findButtonView.on('execute', this._onFindButtonExecute.bind(this));
        actionsDivView.setTemplate({
            tag: 'div',
            attributes: {
                class: ['ck', 'ck-find-and-replace-form__actions']
            },
            children: [
                this._replaceAllButtonView,
                this._replaceButtonView,
                this._findButtonView
            ]
        });
        return actionsDivView;
    }
    /**
     * Creates, configures and returns and instance of a dropdown allowing users to narrow
     * the search criteria down. The dropdown has a list with switch buttons for each option.
     */
    _createMatchCaseSwitch() {
        const t = this.locale.t;
        const matchCaseSwitchButton = new SwitchButtonView(this.locale);
        matchCaseSwitchButton.set({
            label: t('Match case'),
            withText: true
        });
        // Let the switch be controlled by form's observable property.
        matchCaseSwitchButton.bind('isOn').to(this, '_matchCase');
        // // Update the state of the form when a switch is toggled.
        matchCaseSwitchButton.on('execute', () => {
            this._matchCase = !this._matchCase;
            // Toggling a switch makes the form dirty because this changes search criteria
            // just like typing text of the find input.
            this.isDirty = true;
        });
        return matchCaseSwitchButton;
    }
    /**
     * Creates, configures and returns and instance of a dropdown allowing users to narrow
     * the search criteria down. The dropdown has a list with switch buttons for each option.
     */
    _createWholeWordsOnlySwitch() {
        const t = this.locale.t;
        const wholeWordsOnlySwitchButton = new SwitchButtonView(this.locale);
        wholeWordsOnlySwitchButton.set({
            label: t('Whole words only'),
            withText: true
        });
        // Let the switch be controlled by form's observable property.
        wholeWordsOnlySwitchButton.bind('isOn').to(this, '_wholeWordsOnly');
        // // Update the state of the form when a switch is toggled.
        wholeWordsOnlySwitchButton.on('execute', () => {
            this._wholeWordsOnly = !this._wholeWordsOnly;
            // Toggling a switch makes the form dirty because this changes search criteria
            // just like typing text of the find input.
            this.isDirty = true;
        });
        return wholeWordsOnlySwitchButton;
    }
    /**
     * Initializes the {@link #_focusables} and {@link #_focusTracker} to allow navigation
     * using <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> keystrokes in the right order.
     */
    _initFocusCycling() {
        const childViews = [
            this._findInputView,
            this._findPrevButtonView,
            this._findNextButtonView,
            this._replaceInputView,
            this._advancedOptionsCollapsibleView.buttonView,
            this._matchCaseSwitchView,
            this._wholeWordsOnlySwitchView,
            this._replaceAllButtonView,
            this._replaceButtonView,
            this._findButtonView
        ];
        childViews.forEach(v => {
            // Register the view as focusable.
            this._focusables.add(v);
            // Register the view in the focus tracker.
            this._focusTracker.add(v.element);
        });
    }
    /**
     * Initializes the keystroke handling in the form.
     */
    _initKeystrokeHandling() {
        const stopPropagation = (data) => data.stopPropagation();
        const stopPropagationAndPreventDefault = (data) => {
            data.stopPropagation();
            data.preventDefault();
        };
        // Start listening for the keystrokes coming from #element.
        this._keystrokes.listenTo(this.element);
        // Find the next result upon F3.
        this._keystrokes.set('f3', event => {
            stopPropagationAndPreventDefault(event);
            this._findNextButtonView.fire('execute');
        });
        // Find the previous result upon F3.
        this._keystrokes.set('shift+f3', event => {
            stopPropagationAndPreventDefault(event);
            this._findPrevButtonView.fire('execute');
        });
        // Find or replace upon pressing Enter in the find and replace fields.
        this._keystrokes.set('enter', event => {
            const target = event.target;
            if (target === this._findInputView.fieldView.element) {
                if (this._areCommandsEnabled.findNext) {
                    this._findNextButtonView.fire('execute');
                }
                else {
                    this._findButtonView.fire('execute');
                }
                stopPropagationAndPreventDefault(event);
            }
            else if (target === this._replaceInputView.fieldView.element && !this.isDirty) {
                this._replaceButtonView.fire('execute');
                stopPropagationAndPreventDefault(event);
            }
        });
        // Find previous upon pressing Shift+Enter in the find field.
        this._keystrokes.set('shift+enter', event => {
            const target = event.target;
            if (target !== this._findInputView.fieldView.element) {
                return;
            }
            if (this._areCommandsEnabled.findPrevious) {
                this._findPrevButtonView.fire('execute');
            }
            else {
                this._findButtonView.fire('execute');
            }
            stopPropagationAndPreventDefault(event);
        });
        // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
        // keystroke handler would take over the key management in the URL input.
        // We need to prevent this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
        this._keystrokes.set('arrowright', stopPropagation);
        this._keystrokes.set('arrowleft', stopPropagation);
        this._keystrokes.set('arrowup', stopPropagation);
        this._keystrokes.set('arrowdown', stopPropagation);
    }
    /**
     * Creates a button view.
     *
     * @param options The properties of the `ButtonView`.
     * @returns The button view instance.
     */
    _createButton(options) {
        const button = new ButtonView(this.locale);
        button.set(options);
        return button;
    }
    /**
     * Creates a labeled input view.
     *
     * @param label The input label.
     * @returns The labeled input view instance.
     */
    _createInputField(label, className) {
        const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
        labeledInput.label = label;
        labeledInput.class = className;
        return labeledInput;
    }
}
