/**
 * @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
 */
/* global DOMParser */
/**
 * @module widget/widgettypearound/widgettypearound
 */
import { Plugin } from '@ckeditor/ckeditor5-core';
import { Template } from '@ckeditor/ckeditor5-ui';
import { Enter } from '@ckeditor/ckeditor5-enter';
import { Delete } from '@ckeditor/ckeditor5-typing';
import { env, isForwardArrowKeyCode } from '@ckeditor/ckeditor5-utils';
import { isTypeAroundWidget, getClosestTypeAroundDomButton, getTypeAroundButtonPosition, getClosestWidgetViewElement, getTypeAroundFakeCaretPosition, TYPE_AROUND_SELECTION_ATTRIBUTE } from './utils.js';
import { isWidget } from '../utils.js';
import returnIcon from '../../theme/icons/return-arrow.svg';
import '../../theme/widgettypearound.css';
const POSSIBLE_INSERTION_POSITIONS = ['before', 'after'];
// Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString(returnIcon, 'image/svg+xml').firstChild;
const PLUGIN_DISABLED_EDITING_ROOT_CLASS = 'ck-widget__type-around_disabled';
/**
 * A plugin that allows users to type around widgets where normally it is impossible to place the caret due
 * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
 * the first (or last) child of its parent or between two block widgets.
 *
 * This plugin extends the {@link module:widget/widget~Widget `Widget`} plugin and injects the user interface
 * with two buttons into each widget instance in the editor. Each of the buttons can be clicked by the
 * user if the widget is next to the "tight spot". Once clicked, a paragraph is created with the selection anchored
 * in it so that users can type (or insert content, paste, etc.) straight away.
 */
export default class WidgetTypeAround extends Plugin {
    constructor() {
        super(...arguments);
        /**
         * A reference to the model widget element that has the fake caret active
         * on either side of it. It is later used to remove CSS classes associated with the fake caret
         * when the widget no longer needs it.
         */
        this._currentFakeCaretModelElement = null;
    }
    /**
     * @inheritDoc
     */
    static get pluginName() {
        return 'WidgetTypeAround';
    }
    /**
     * @inheritDoc
     */
    static get requires() {
        return [Enter, Delete];
    }
    /**
     * @inheritDoc
     */
    init() {
        const editor = this.editor;
        const editingView = editor.editing.view;
        // Set a CSS class on the view editing root when the plugin is disabled so all the buttons
        // and lines visually disappear. All the interactions are disabled in individual plugin methods.
        this.on('change:isEnabled', (evt, data, isEnabled) => {
            editingView.change(writer => {
                for (const root of editingView.document.roots) {
                    if (isEnabled) {
                        writer.removeClass(PLUGIN_DISABLED_EDITING_ROOT_CLASS, root);
                    }
                    else {
                        writer.addClass(PLUGIN_DISABLED_EDITING_ROOT_CLASS, root);
                    }
                }
            });
            if (!isEnabled) {
                editor.model.change(writer => {
                    writer.removeSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE);
                });
            }
        });
        this._enableTypeAroundUIInjection();
        this._enableInsertingParagraphsOnButtonClick();
        this._enableInsertingParagraphsOnEnterKeypress();
        this._enableInsertingParagraphsOnTypingKeystroke();
        this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
        this._enableDeleteIntegration();
        this._enableInsertContentIntegration();
        this._enableInsertObjectIntegration();
        this._enableDeleteContentIntegration();
    }
    /**
     * @inheritDoc
     */
    destroy() {
        super.destroy();
        this._currentFakeCaretModelElement = null;
    }
    /**
     * Inserts a new paragraph next to a widget element with the selection anchored in it.
     *
     * **Note**: This method is heavily user-oriented and will both focus the editing view and scroll
     * the viewport to the selection in the inserted paragraph.
     *
     * @param widgetModelElement The model widget element next to which a paragraph is inserted.
     * @param position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget.
     */
    _insertParagraph(widgetModelElement, position) {
        const editor = this.editor;
        const editingView = editor.editing.view;
        const attributesToCopy = editor.model.schema.getAttributesWithProperty(widgetModelElement, 'copyOnReplace', true);
        editor.execute('insertParagraph', {
            position: editor.model.createPositionAt(widgetModelElement, position),
            attributes: attributesToCopy
        });
        editingView.focus();
        editingView.scrollToTheSelection();
    }
    /**
     * A wrapper for the {@link module:utils/emittermixin~Emitter#listenTo} method that executes the callbacks only
     * when the plugin {@link #isEnabled is enabled}.
     *
     * @param emitter The object that fires the event.
     * @param event The name of the event.
     * @param callback The function to be called on event.
     * @param options Additional options.
     * @param options.priority The priority of this event callback. The higher the priority value the sooner
     * the callback will be fired. Events having the same priority are called in the order they were added.
     */
    _listenToIfEnabled(emitter, event, callback, options) {
        this.listenTo(emitter, event, (...args) => {
            // Do not respond if the plugin is disabled.
            if (this.isEnabled) {
                callback(...args);
            }
        }, options);
    }
    /**
     * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
     * does not expect a position. Instead, it performs the insertion next to a selected widget
     * according to the `widget-type-around` model selection attribute value (fake caret position).
     *
     * Because this method requires the `widget-type-around` attribute to be set,
     * the insertion can only happen when the widget's fake caret is active (e.g. activated
     * using the keyboard).
     *
     * @returns Returns `true` when the paragraph was inserted (the attribute was present) and `false` otherwise.
     */
    _insertParagraphAccordingToFakeCaretPosition() {
        const editor = this.editor;
        const model = editor.model;
        const modelSelection = model.document.selection;
        const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(modelSelection);
        if (!typeAroundFakeCaretPosition) {
            return false;
        }
        // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
        // @if CK_DEBUG_TYPING // 	console.info( '%c[WidgetTypeAround]%c Fake caret -> insert paragraph',
        // @if CK_DEBUG_TYPING // 		'font-weight: bold; color: green', ''
        // @if CK_DEBUG_TYPING // 	);
        // @if CK_DEBUG_TYPING // }
        const selectedModelElement = modelSelection.getSelectedElement();
        this._insertParagraph(selectedModelElement, typeAroundFakeCaretPosition);
        return true;
    }
    /**
     * Creates a listener in the editing conversion pipeline that injects the widget type around
     * UI into every single widget instance created in the editor.
     *
     * The UI is delivered as a {@link module:engine/view/uielement~UIElement}
     * wrapper which renders DOM buttons that users can use to insert paragraphs.
     */
    _enableTypeAroundUIInjection() {
        const editor = this.editor;
        const schema = editor.model.schema;
        const t = editor.locale.t;
        const buttonTitles = {
            before: t('Insert paragraph before block'),
            after: t('Insert paragraph after block')
        };
        editor.editing.downcastDispatcher.on('insert', (evt, data, conversionApi) => {
            const viewElement = conversionApi.mapper.toViewElement(data.item);
            if (!viewElement) {
                return;
            }
            // Filter out non-widgets and inline widgets.
            if (isTypeAroundWidget(viewElement, data.item, schema)) {
                injectUIIntoWidget(conversionApi.writer, buttonTitles, viewElement);
                const widgetLabel = viewElement.getCustomProperty('widgetLabel');
                widgetLabel.push(() => {
                    return this.isEnabled ? t('Press Enter to type after or press Shift + Enter to type before the widget') : '';
                });
            }
        }, { priority: 'low' });
    }
    /**
     * Brings support for the fake caret that appears when either:
     *
     * * the selection moves to a widget from a position next to it using arrow keys,
     * * the arrow key is pressed when the widget is already selected.
     *
     * The fake caret lets the user know that they can start typing or just press
     * <kbd>Enter</kbd> to insert a paragraph at the position next to a widget as suggested by the fake caret.
     *
     * The fake caret disappears when the user changes the selection or the editor
     * gets blurred.
     *
     * The whole idea is as follows:
     *
     * 1. A user does one of the 2 scenarios described at the beginning.
     * 2. The "keydown" listener is executed and the decision is made whether to show or hide the fake caret.
     * 3. If it should show up, the `widget-type-around` model selection attribute is set indicating
     *    on which side of the widget it should appear.
     * 4. The selection dispatcher reacts to the selection attribute and sets CSS classes responsible for the
     *    fake caret on the view widget.
     * 5. If the fake caret should disappear, the selection attribute is removed and the dispatcher
     *    does the CSS class clean-up in the view.
     * 6. Additionally, `change:range` and `FocusTracker#isFocused` listeners also remove the selection
     *    attribute (the former also removes widget CSS classes).
     */
    _enableTypeAroundFakeCaretActivationUsingKeyboardArrows() {
        const editor = this.editor;
        const model = editor.model;
        const modelSelection = model.document.selection;
        const schema = model.schema;
        const editingView = editor.editing.view;
        // This is the main listener responsible for the fake caret.
        // Note: The priority must precede the default Widget class keydown handler ("high").
        this._listenToIfEnabled(editingView.document, 'arrowKey', (evt, domEventData) => {
            this._handleArrowKeyPress(evt, domEventData);
        }, { context: [isWidget, '$text'], priority: 'high' });
        // This listener makes sure the widget type around selection attribute will be gone from the model
        // selection as soon as the model range changes. This attribute only makes sense when a widget is selected
        // (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
        // let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
        this._listenToIfEnabled(modelSelection, 'change:range', (evt, data) => {
            // Do not reset the selection attribute when the change was indirect.
            if (!data.directChange) {
                return;
            }
            // Get rid of the widget type around attribute of the selection on every change:range.
            // If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
            editor.model.change(writer => {
                writer.removeSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE);
            });
        });
        // Get rid of the widget type around attribute of the selection on every document change
        // that makes widget not selected any more (i.e. widget was removed).
        this._listenToIfEnabled(model.document, 'change:data', () => {
            const selectedModelElement = modelSelection.getSelectedElement();
            if (selectedModelElement) {
                const selectedViewElement = editor.editing.mapper.toViewElement(selectedModelElement);
                if (isTypeAroundWidget(selectedViewElement, selectedModelElement, schema)) {
                    return;
                }
            }
            editor.model.change(writer => {
                writer.removeSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE);
            });
        });
        // React to changes of the model selection attribute made by the arrow keys listener.
        // If the block widget is selected and the attribute changes, downcast the attribute to special
        // CSS classes associated with the active ("fake horizontal caret") mode of the widget.
        this._listenToIfEnabled(editor.editing.downcastDispatcher, 'selection', (evt, data, conversionApi) => {
            const writer = conversionApi.writer;
            if (this._currentFakeCaretModelElement) {
                const selectedViewElement = conversionApi.mapper.toViewElement(this._currentFakeCaretModelElement);
                if (selectedViewElement) {
                    // Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget.
                    writer.removeClass(POSSIBLE_INSERTION_POSITIONS.map(positionToWidgetCssClass), selectedViewElement);
                    this._currentFakeCaretModelElement = null;
                }
            }
            const selectedModelElement = data.selection.getSelectedElement();
            if (!selectedModelElement) {
                return;
            }
            const selectedViewElement = conversionApi.mapper.toViewElement(selectedModelElement);
            if (!isTypeAroundWidget(selectedViewElement, selectedModelElement, schema)) {
                return;
            }
            const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(data.selection);
            if (!typeAroundFakeCaretPosition) {
                return;
            }
            writer.addClass(positionToWidgetCssClass(typeAroundFakeCaretPosition), selectedViewElement);
            // Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the
            // selection changes
            this._currentFakeCaretModelElement = selectedModelElement;
        });
        this._listenToIfEnabled(editor.ui.focusTracker, 'change:isFocused', (evt, name, isFocused) => {
            if (!isFocused) {
                editor.model.change(writer => {
                    writer.removeSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE);
                });
            }
        });
        function positionToWidgetCssClass(position) {
            return `ck-widget_type-around_show-fake-caret_${position}`;
        }
    }
    /**
     * A listener executed on each "keydown" in the view document, a part of
     * {@link #_enableTypeAroundFakeCaretActivationUsingKeyboardArrows}.
     *
     * It decides whether the arrow keypress should activate the fake caret or not (also whether it should
     * be deactivated).
     *
     * The fake caret activation is done by setting the `widget-type-around` model selection attribute
     * in this listener, and stopping and preventing the event that would normally be handled by the widget
     * plugin that is responsible for the regular keyboard navigation near/across all widgets (that
     * includes inline widgets, which are ignored by the widget type around plugin).
     */
    _handleArrowKeyPress(evt, domEventData) {
        const editor = this.editor;
        const model = editor.model;
        const modelSelection = model.document.selection;
        const schema = model.schema;
        const editingView = editor.editing.view;
        const keyCode = domEventData.keyCode;
        const isForward = isForwardArrowKeyCode(keyCode, editor.locale.contentLanguageDirection);
        const selectedViewElement = editingView.document.selection.getSelectedElement();
        const selectedModelElement = editor.editing.mapper.toModelElement(selectedViewElement);
        let shouldStopAndPreventDefault;
        // Handle keyboard navigation when a type-around-compatible widget is currently selected.
        if (isTypeAroundWidget(selectedViewElement, selectedModelElement, schema)) {
            shouldStopAndPreventDefault = this._handleArrowKeyPressOnSelectedWidget(isForward);
        }
        // Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget
        // and the widget is about to be selected.
        else if (modelSelection.isCollapsed) {
            shouldStopAndPreventDefault = this._handleArrowKeyPressWhenSelectionNextToAWidget(isForward);
        }
        // Handle collapsing a non-collapsed selection that is wider than on a single widget.
        else if (!domEventData.shiftKey) {
            shouldStopAndPreventDefault = this._handleArrowKeyPressWhenNonCollapsedSelection(isForward);
        }
        if (shouldStopAndPreventDefault) {
            domEventData.preventDefault();
            evt.stop();
        }
    }
    /**
     * Handles the keyboard navigation on "keydown" when a widget is currently selected and activates or deactivates
     * the fake caret for that widget, depending on the current value of the `widget-type-around` model
     * selection attribute and the direction of the pressed arrow key.
     *
     * @param isForward `true` when the pressed arrow key was responsible for the forward model selection movement
     * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
     * @returns Returns `true` when the keypress was handled and no other keydown listener of the editor should
     * process the event any further. Returns `false` otherwise.
     */
    _handleArrowKeyPressOnSelectedWidget(isForward) {
        const editor = this.editor;
        const model = editor.model;
        const modelSelection = model.document.selection;
        const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(modelSelection);
        return model.change(writer => {
            // If the fake caret is displayed...
            if (typeAroundFakeCaretPosition) {
                const isLeavingWidget = typeAroundFakeCaretPosition === (isForward ? 'after' : 'before');
                // If the keyboard arrow works against the value of the selection attribute...
                // then remove the selection attribute but prevent default DOM actions
                // and do not let the Widget plugin listener move the selection. This brings
                // the widget back to the state, for instance, like if was selected using the mouse.
                //
                // **Note**: If leaving the widget when the fake caret is active, then the default
                // Widget handler will change the selection and, in turn, this will automatically discard
                // the selection attribute.
                if (!isLeavingWidget) {
                    writer.removeSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE);
                    return true;
                }
            }
            // If the fake caret wasn't displayed, let's set it now according to the direction of the arrow
            // key press. This also means we cannot let the Widget plugin listener move the selection.
            else {
                writer.setSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before');
                return true;
            }
            return false;
        });
    }
    /**
     * Handles the keyboard navigation on "keydown" when **no** widget is selected but the selection is **directly** next
     * to one and upon the fake caret should become active for this widget upon arrow keypress
     * (AKA entering/selecting the widget).
     *
     * **Note**: This code mirrors the implementation from the widget plugin but also adds the selection attribute.
     * Unfortunately, there is no safe way to let the widget plugin do the selection part first and then just set the
     * selection attribute here in the widget type around plugin. This is why this code must duplicate some from the widget plugin.
     *
     * @param isForward `true` when the pressed arrow key was responsible for the forward model selection movement
     * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
     * @returns Returns `true` when the keypress was handled and no other keydown listener of the editor should
     * process the event any further. Returns `false` otherwise.
     */
    _handleArrowKeyPressWhenSelectionNextToAWidget(isForward) {
        const editor = this.editor;
        const model = editor.model;
        const schema = model.schema;
        const widgetPlugin = editor.plugins.get('Widget');
        // This is the widget the selection is about to be set on.
        const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection(isForward);
        const viewElementNextToSelection = editor.editing.mapper.toViewElement(modelElementNextToSelection);
        if (isTypeAroundWidget(viewElementNextToSelection, modelElementNextToSelection, schema)) {
            model.change(writer => {
                widgetPlugin._setSelectionOverElement(modelElementNextToSelection);
                writer.setSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after');
            });
            // The change() block above does the same job as the Widget plugin. The event can
            // be safely canceled.
            return true;
        }
        return false;
    }
    /**
     * Handles the keyboard navigation on "keydown" when a widget is currently selected (together with some other content)
     * and the widget is the first or last element in the selection. It activates or deactivates the fake caret for that widget.
     *
     * @param isForward `true` when the pressed arrow key was responsible for the forward model selection movement
     * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
     * @returns Returns `true` when the keypress was handled and no other keydown listener of the editor should
     * process the event any further. Returns `false` otherwise.
     */
    _handleArrowKeyPressWhenNonCollapsedSelection(isForward) {
        const editor = this.editor;
        const model = editor.model;
        const schema = model.schema;
        const mapper = editor.editing.mapper;
        const modelSelection = model.document.selection;
        const selectedModelNode = isForward ?
            modelSelection.getLastPosition().nodeBefore :
            modelSelection.getFirstPosition().nodeAfter;
        const selectedViewNode = mapper.toViewElement(selectedModelNode);
        // There is a widget at the collapse position so collapse the selection to the fake caret on it.
        if (isTypeAroundWidget(selectedViewNode, selectedModelNode, schema)) {
            model.change(writer => {
                writer.setSelection(selectedModelNode, 'on');
                writer.setSelectionAttribute(TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before');
            });
            return true;
        }
        return false;
    }
    /**
     * Registers a `mousedown` listener for the view document which intercepts events
     * coming from the widget type around UI, which happens when a user clicks one of the buttons
     * that insert a paragraph next to a widget.
     */
    _enableInsertingParagraphsOnButtonClick() {
        const editor = this.editor;
        const editingView = editor.editing.view;
        this._listenToIfEnabled(editingView.document, 'mousedown', (evt, domEventData) => {
            const button = getClosestTypeAroundDomButton(domEventData.domTarget);
            if (!button) {
                return;
            }
            const buttonPosition = getTypeAroundButtonPosition(button);
            const widgetViewElement = getClosestWidgetViewElement(button, editingView.domConverter);
            const widgetModelElement = editor.editing.mapper.toModelElement(widgetViewElement);
            this._insertParagraph(widgetModelElement, buttonPosition);
            domEventData.preventDefault();
            evt.stop();
        });
    }
    /**
     * Creates the <kbd>Enter</kbd> key listener on the view document that allows the user to insert a paragraph
     * near the widget when either:
     *
     * * The fake caret was first activated using the arrow keys,
     * * The entire widget is selected in the model.
     *
     * In the first case, the new paragraph is inserted according to the `widget-type-around` selection
     * attribute (see {@link #_handleArrowKeyPress}).
     *
     * In the second case, the new paragraph is inserted based on whether a soft (<kbd>Shift</kbd>+<kbd>Enter</kbd>) keystroke
     * was pressed or not.
     */
    _enableInsertingParagraphsOnEnterKeypress() {
        const editor = this.editor;
        const selection = editor.model.document.selection;
        const editingView = editor.editing.view;
        this._listenToIfEnabled(editingView.document, 'enter', (evt, domEventData) => {
            // This event could be triggered from inside the widget but we are interested
            // only when the widget is selected itself.
            if (evt.eventPhase != 'atTarget') {
                return;
            }
            const selectedModelElement = selection.getSelectedElement();
            const selectedViewElement = editor.editing.mapper.toViewElement(selectedModelElement);
            const schema = editor.model.schema;
            let wasHandled;
            // First check if the widget is selected and there's a type around selection attribute associated
            // with the fake caret that would tell where to insert a new paragraph.
            if (this._insertParagraphAccordingToFakeCaretPosition()) {
                wasHandled = true;
            }
            // Then, if there is no selection attribute associated with the fake caret, check if the widget
            // simply is selected and create a new paragraph according to the keystroke (Shift+)Enter.
            else if (isTypeAroundWidget(selectedViewElement, selectedModelElement, schema)) {
                this._insertParagraph(selectedModelElement, domEventData.isSoft ? 'before' : 'after');
                wasHandled = true;
            }
            if (wasHandled) {
                domEventData.preventDefault();
                evt.stop();
            }
        }, { context: isWidget });
    }
    /**
     * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user
     * to insert a paragraph next to a widget when the fake caret was activated using arrow
     * keys but it responds to typing instead of <kbd>Enter</kbd>.
     *
     * Listener enabled by this method will insert a new paragraph according to the `widget-type-around`
     * model selection attribute as the user simply starts typing, which creates the impression that the fake caret
     * behaves like a real one rendered by the browser (AKA your text appears where the caret was).
     *
     * **Note**: At the moment this listener creates 2 undo steps: one for the `insertParagraph` command
     * and another one for actual typing. It is not a disaster but this may need to be fixed
     * sooner or later.
     */
    _enableInsertingParagraphsOnTypingKeystroke() {
        const editor = this.editor;
        const viewDocument = editor.editing.view.document;
        // Note: The priority must precede the default Input plugin insertText handler.
        this._listenToIfEnabled(viewDocument, 'insertText', (evt, data) => {
            if (this._insertParagraphAccordingToFakeCaretPosition()) {
                // The view selection in the event data contains the widget. If the new paragraph
                // was inserted, modify the view selection passed along with the insertText event
                // so the default event handler in the Input plugin starts typing inside the paragraph.
                // Otherwise, the typing would be over the widget.
                data.selection = viewDocument.selection;
            }
        }, { priority: 'high' });
        if (env.isAndroid) {
            // On Android with English keyboard, the composition starts just by putting caret
            // at the word end or by selecting a table column. This is not a real composition started.
            // Trigger delete content on first composition key pressed.
            this._listenToIfEnabled(viewDocument, 'keydown', (evt, data) => {
                if (data.keyCode == 229) {
                    this._insertParagraphAccordingToFakeCaretPosition();
                }
            });
        }
        else {
            // Note: The priority must precede the default Input plugin compositionstart handler (to call it before delete content).
            this._listenToIfEnabled(viewDocument, 'compositionstart', () => {
                this._insertParagraphAccordingToFakeCaretPosition();
            }, { priority: 'high' });
        }
    }
    /**
     * It creates a "delete" event listener on the view document to handle cases when the <kbd>Delete</kbd> or <kbd>Backspace</kbd>
     * is pressed and the fake caret is currently active.
     *
     * The fake caret should create an illusion of a real browser caret so that when it appears before or after
     * a widget, pressing <kbd>Delete</kbd> or <kbd>Backspace</kbd> should remove a widget or delete the content
     * before or after a widget (depending on the content surrounding the widget).
     */
    _enableDeleteIntegration() {
        const editor = this.editor;
        const editingView = editor.editing.view;
        const model = editor.model;
        const schema = model.schema;
        this._listenToIfEnabled(editingView.document, 'delete', (evt, domEventData) => {
            // This event could be triggered from inside the widget but we are interested
            // only when the widget is selected itself.
            if (evt.eventPhase != 'atTarget') {
                return;
            }
            const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(model.document.selection);
            // This listener handles only these cases when the fake caret is active.
            if (!typeAroundFakeCaretPosition) {
                return;
            }
            const direction = domEventData.direction;
            const selectedModelWidget = model.document.selection.getSelectedElement();
            const isFakeCaretBefore = typeAroundFakeCaretPosition === 'before';
            const isDeleteForward = direction == 'forward';
            const shouldDeleteEntireWidget = isFakeCaretBefore === isDeleteForward;
            if (shouldDeleteEntireWidget) {
                editor.execute('delete', {
                    selection: model.createSelection(selectedModelWidget, 'on')
                });
            }
            else {
                const range = schema.getNearestSelectionRange(model.createPositionAt(selectedModelWidget, typeAroundFakeCaretPosition), direction);
                // If there is somewhere to move selection to, then there will be something to delete.
                if (range) {
                    // If the range is NOT collapsed, then we know that the range contains an object (see getNearestSelectionRange() docs).
                    if (!range.isCollapsed) {
                        model.change(writer => {
                            writer.setSelection(range);
                            editor.execute(isDeleteForward ? 'deleteForward' : 'delete');
                        });
                    }
                    else {
                        const probe = model.createSelection(range.start);
                        model.modifySelection(probe, { direction });
                        // If the range is collapsed, let's see if a non-collapsed range exists that can could be deleted.
                        // If such range exists, use the editor command because it it safe for collaboration (it merges where it can).
                        if (!probe.focus.isEqual(range.start)) {
                            model.change(writer => {
                                writer.setSelection(range);
                                editor.execute(isDeleteForward ? 'deleteForward' : 'delete');
                            });
                        }
                        // If there is no non-collapsed range to be deleted then we are sure that there is an empty element
                        // next to a widget that should be removed. "delete" and "deleteForward" commands cannot get rid of it
                        // so calling Model#deleteContent here manually.
                        else {
                            const deepestEmptyRangeAncestor = getDeepestEmptyElementAncestor(schema, range.start.parent);
                            model.deleteContent(model.createSelection(deepestEmptyRangeAncestor, 'on'), {
                                doNotAutoparagraph: true
                            });
                        }
                    }
                }
            }
            // If some content was deleted, don't let the handler from the Widget plugin kick in.
            // If nothing was deleted, then the default handler will have nothing to do anyway.
            domEventData.preventDefault();
            evt.stop();
        }, { context: isWidget });
    }
    /**
     * Attaches the {@link module:engine/model/model~Model#event:insertContent} event listener that, for instance, allows the user to paste
     * content near a widget when the fake caret is first activated using the arrow keys.
     *
     * The content is inserted according to the `widget-type-around` selection attribute (see {@link #_handleArrowKeyPress}).
     */
    _enableInsertContentIntegration() {
        const editor = this.editor;
        const model = this.editor.model;
        const documentSelection = model.document.selection;
        this._listenToIfEnabled(editor.model, 'insertContent', (evt, [content, selectable]) => {
            if (selectable && !selectable.is('documentSelection')) {
                return;
            }
            const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(documentSelection);
            if (!typeAroundFakeCaretPosition) {
                return;
            }
            evt.stop();
            return model.change(writer => {
                const selectedElement = documentSelection.getSelectedElement();
                const position = model.createPositionAt(selectedElement, typeAroundFakeCaretPosition);
                const selection = writer.createSelection(position);
                const result = model.insertContent(content, selection);
                writer.setSelection(selection);
                return result;
            });
        }, { priority: 'high' });
    }
    /**
     * Attaches the {@link module:engine/model/model~Model#event:insertObject} event listener that modifies the
     * `options.findOptimalPosition`parameter to position of fake caret in relation to selected element
     * to reflect user's intent of desired insertion position.
     *
     * The object is inserted according to the `widget-type-around` selection attribute (see {@link #_handleArrowKeyPress}).
     */
    _enableInsertObjectIntegration() {
        const editor = this.editor;
        const model = this.editor.model;
        const documentSelection = model.document.selection;
        this._listenToIfEnabled(editor.model, 'insertObject', (evt, args) => {
            const [, selectable, options = {}] = args;
            if (selectable && !selectable.is('documentSelection')) {
                return;
            }
            const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(documentSelection);
            if (!typeAroundFakeCaretPosition) {
                return;
            }
            options.findOptimalPosition = typeAroundFakeCaretPosition;
            args[3] = options;
        }, { priority: 'high' });
    }
    /**
     * Attaches the {@link module:engine/model/model~Model#event:deleteContent} event listener to block the event when the fake
     * caret is active.
     *
     * This is required for cases that trigger {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`}
     * before calling {@link module:engine/model/model~Model#insertContent `model.insertContent()`} like, for instance,
     * plain text pasting.
     */
    _enableDeleteContentIntegration() {
        const editor = this.editor;
        const model = this.editor.model;
        const documentSelection = model.document.selection;
        this._listenToIfEnabled(editor.model, 'deleteContent', (evt, [selection]) => {
            if (selection && !selection.is('documentSelection')) {
                return;
            }
            const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(documentSelection);
            // Disable removing the selection content while pasting plain text.
            if (typeAroundFakeCaretPosition) {
                evt.stop();
            }
        }, { priority: 'high' });
    }
}
/**
 * Injects the type around UI into a view widget instance.
 */
function injectUIIntoWidget(viewWriter, buttonTitles, widgetViewElement) {
    const typeAroundWrapper = viewWriter.createUIElement('div', {
        class: 'ck ck-reset_all ck-widget__type-around'
    }, function (domDocument) {
        const wrapperDomElement = this.toDomElement(domDocument);
        injectButtons(wrapperDomElement, buttonTitles);
        injectFakeCaret(wrapperDomElement);
        return wrapperDomElement;
    });
    // Inject the type around wrapper into the widget's wrapper.
    viewWriter.insert(viewWriter.createPositionAt(widgetViewElement, 'end'), typeAroundWrapper);
}
/**
 * FYI: Not using the IconView class because each instance would need to be destroyed to avoid memory leaks
 * and it's pretty hard to figure out when a view (widget) is gone for good so it's cheaper to use raw
 * <svg> here.
 */
function injectButtons(wrapperDomElement, buttonTitles) {
    for (const position of POSSIBLE_INSERTION_POSITIONS) {
        const buttonTemplate = new Template({
            tag: 'div',
            attributes: {
                class: [
                    'ck',
                    'ck-widget__type-around__button',
                    `ck-widget__type-around__button_${position}`
                ],
                title: buttonTitles[position],
                'aria-hidden': 'true'
            },
            children: [
                wrapperDomElement.ownerDocument.importNode(RETURN_ARROW_ICON_ELEMENT, true)
            ]
        });
        wrapperDomElement.appendChild(buttonTemplate.render());
    }
}
function injectFakeCaret(wrapperDomElement) {
    const caretTemplate = new Template({
        tag: 'div',
        attributes: {
            class: [
                'ck',
                'ck-widget__type-around__fake-caret'
            ]
        }
    });
    wrapperDomElement.appendChild(caretTemplate.render());
}
/**
 * Returns the ancestor of an element closest to the root which is empty. For instance,
 * for `<baz>`:
 *
 * ```
 * <foo>abc<bar><baz></baz></bar></foo>
 * ```
 *
 * it returns `<bar>`.
 */
function getDeepestEmptyElementAncestor(schema, element) {
    let deepestEmptyAncestor = element;
    for (const ancestor of element.getAncestors({ parentFirst: true })) {
        if (ancestor.childCount > 1 || schema.isLimit(ancestor)) {
            break;
        }
        deepestEmptyAncestor = ancestor;
    }
    return deepestEmptyAncestor;
}
