/**
 * @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 list/list/listediting
 */
import { Plugin } from 'ckeditor5/src/core.js';
import { Delete } from 'ckeditor5/src/typing.js';
import { Enter } from 'ckeditor5/src/enter.js';
import { CKEditorError } from 'ckeditor5/src/utils.js';
import ListIndentCommand from './listindentcommand.js';
import ListCommand from './listcommand.js';
import ListMergeCommand from './listmergecommand.js';
import ListSplitCommand from './listsplitcommand.js';
import ListUtils from './listutils.js';
import { bogusParagraphCreator, createModelToViewPositionMapper, listItemDowncastConverter, listItemDowncastRemoveConverter, listItemUpcastConverter, listUpcastCleanList, reconvertItemsOnDataChange } from './converters.js';
import { findAndAddListHeadToMap, fixListIndents, fixListItemIds } from './utils/postfixers.js';
import { getAllListItemBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isSingleListItem, getSelectedBlockObject, isListItemBlock, removeListAttributes, ListItemUid } from './utils/model.js';
import { getViewElementIdForListType, getViewElementNameForListType } from './utils/view.js';
import ListWalker, { ListBlocksIterable } from './utils/listwalker.js';
import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
import '../../theme/documentlist.css';
import '../../theme/list.css';
/**
 * A list of base list model attributes.
 */
const LIST_BASE_ATTRIBUTES = ['listType', 'listIndent', 'listItemId'];
/**
 * The editing part of the document-list feature. It handles creating, editing and removing lists and list items.
 */
export default class ListEditing extends Plugin {
    /**
     * @inheritDoc
     */
    static get pluginName() {
        return 'ListEditing';
    }
    /**
     * @inheritDoc
     */
    static get requires() {
        return [Enter, Delete, ListUtils, ClipboardPipeline];
    }
    /**
     * @inheritDoc
     */
    constructor(editor) {
        super(editor);
        /**
         * The list of registered downcast strategies.
         */
        this._downcastStrategies = [];
        editor.config.define('list.multiBlock', true);
    }
    /**
     * @inheritDoc
     */
    init() {
        const editor = this.editor;
        const model = editor.model;
        const multiBlock = editor.config.get('list.multiBlock');
        if (editor.plugins.has('LegacyListEditing')) {
            /**
             * The `List` feature can not be loaded together with the `LegacyList` plugin.
             *
             * @error list-feature-conflict
             * @param conflictPlugin Name of the plugin.
             */
            throw new CKEditorError('list-feature-conflict', this, { conflictPlugin: 'LegacyListEditing' });
        }
        model.schema.register('$listItem', { allowAttributes: LIST_BASE_ATTRIBUTES });
        if (multiBlock) {
            model.schema.extend('$container', { allowAttributesOf: '$listItem' });
            model.schema.extend('$block', { allowAttributesOf: '$listItem' });
            model.schema.extend('$blockObject', { allowAttributesOf: '$listItem' });
        }
        else {
            model.schema.register('listItem', {
                inheritAllFrom: '$block',
                allowAttributesOf: '$listItem'
            });
        }
        for (const attribute of LIST_BASE_ATTRIBUTES) {
            model.schema.setAttributeProperties(attribute, {
                copyOnReplace: true
            });
        }
        // Register commands.
        editor.commands.add('numberedList', new ListCommand(editor, 'numbered'));
        editor.commands.add('bulletedList', new ListCommand(editor, 'bulleted'));
        editor.commands.add('customNumberedList', new ListCommand(editor, 'customNumbered', { multiLevel: true }));
        editor.commands.add('customBulletedList', new ListCommand(editor, 'customBulleted', { multiLevel: true }));
        editor.commands.add('indentList', new ListIndentCommand(editor, 'forward'));
        editor.commands.add('outdentList', new ListIndentCommand(editor, 'backward'));
        editor.commands.add('splitListItemBefore', new ListSplitCommand(editor, 'before'));
        editor.commands.add('splitListItemAfter', new ListSplitCommand(editor, 'after'));
        if (multiBlock) {
            editor.commands.add('mergeListItemBackward', new ListMergeCommand(editor, 'backward'));
            editor.commands.add('mergeListItemForward', new ListMergeCommand(editor, 'forward'));
        }
        this._setupDeleteIntegration();
        this._setupEnterIntegration();
        this._setupTabIntegration();
        this._setupClipboardIntegration();
        this._setupAccessibilityIntegration();
    }
    /**
     * @inheritDoc
     */
    afterInit() {
        const editor = this.editor;
        const commands = editor.commands;
        const indent = commands.get('indent');
        const outdent = commands.get('outdent');
        if (indent) {
            // Priority is high due to integration with `IndentBlock` plugin. We want to indent list first and if it's not possible
            // user can indent content with `IndentBlock` plugin.
            indent.registerChildCommand(commands.get('indentList'), { priority: 'high' });
        }
        if (outdent) {
            // Priority is lowest due to integration with `IndentBlock` and `IndentCode` plugins.
            // First we want to allow user to outdent all indendations from other features then he can oudent list item.
            outdent.registerChildCommand(commands.get('outdentList'), { priority: 'lowest' });
        }
        // Register conversion and model post-fixer after other plugins had a chance to register their attribute strategies.
        this._setupModelPostFixing();
        this._setupConversion();
    }
    /**
     * Registers a downcast strategy.
     *
     * **Note**: Strategies must be registered in the `Plugin#init()` phase so that it can be applied
     * in the `ListEditing#afterInit()`.
     *
     * @param strategy The downcast strategy to register.
     */
    registerDowncastStrategy(strategy) {
        this._downcastStrategies.push(strategy);
    }
    /**
     * Returns list of model attribute names that should affect downcast conversion.
     */
    getListAttributeNames() {
        return [
            ...LIST_BASE_ATTRIBUTES,
            ...this._downcastStrategies.map(strategy => strategy.attributeName)
        ];
    }
    /**
     * Attaches the listener to the {@link module:engine/view/document~Document#event:delete} event and handles backspace/delete
     * keys in and around document lists.
     */
    _setupDeleteIntegration() {
        const editor = this.editor;
        const mergeBackwardCommand = editor.commands.get('mergeListItemBackward');
        const mergeForwardCommand = editor.commands.get('mergeListItemForward');
        this.listenTo(editor.editing.view.document, 'delete', (evt, data) => {
            const selection = editor.model.document.selection;
            // Let the Widget plugin take care of block widgets while deleting (https://github.com/ckeditor/ckeditor5/issues/11346).
            if (getSelectedBlockObject(editor.model)) {
                return;
            }
            editor.model.change(() => {
                const firstPosition = selection.getFirstPosition();
                if (selection.isCollapsed && data.direction == 'backward') {
                    if (!firstPosition.isAtStart) {
                        return;
                    }
                    const positionParent = firstPosition.parent;
                    if (!isListItemBlock(positionParent)) {
                        return;
                    }
                    const previousBlock = ListWalker.first(positionParent, {
                        sameAttributes: 'listType',
                        sameIndent: true
                    });
                    // Outdent the first block of a first list item.
                    if (!previousBlock && positionParent.getAttribute('listIndent') === 0) {
                        if (!isLastBlockOfListItem(positionParent)) {
                            editor.execute('splitListItemAfter');
                        }
                        editor.execute('outdentList');
                    }
                    // Merge block with previous one (on the block level or on the content level).
                    else {
                        if (!mergeBackwardCommand || !mergeBackwardCommand.isEnabled) {
                            return;
                        }
                        mergeBackwardCommand.execute({
                            shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'backward')
                        });
                    }
                    data.preventDefault();
                    evt.stop();
                }
                // Non-collapsed selection or forward delete.
                else {
                    // Collapsed selection should trigger forward merging only if at the end of a block.
                    if (selection.isCollapsed && !selection.getLastPosition().isAtEnd) {
                        return;
                    }
                    if (!mergeForwardCommand || !mergeForwardCommand.isEnabled) {
                        return;
                    }
                    mergeForwardCommand.execute({
                        shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'forward')
                    });
                    data.preventDefault();
                    evt.stop();
                }
            });
        }, { context: 'li' });
    }
    /**
     * Attaches a listener to the {@link module:engine/view/document~Document#event:enter} event and handles enter key press
     * in document lists.
     */
    _setupEnterIntegration() {
        const editor = this.editor;
        const model = editor.model;
        const commands = editor.commands;
        const enterCommand = commands.get('enter');
        // Overwrite the default Enter key behavior: outdent or split the list in certain cases.
        this.listenTo(editor.editing.view.document, 'enter', (evt, data) => {
            const doc = model.document;
            const positionParent = doc.selection.getFirstPosition().parent;
            if (doc.selection.isCollapsed &&
                isListItemBlock(positionParent) &&
                positionParent.isEmpty &&
                !data.isSoft) {
                const isFirstBlock = isFirstBlockOfListItem(positionParent);
                const isLastBlock = isLastBlockOfListItem(positionParent);
                // * a      →      * a
                // * []     →      []
                if (isFirstBlock && isLastBlock) {
                    editor.execute('outdentList');
                    data.preventDefault();
                    evt.stop();
                }
                // * []     →      * []
                //   a      →      * a
                else if (isFirstBlock && !isLastBlock) {
                    editor.execute('splitListItemAfter');
                    data.preventDefault();
                    evt.stop();
                }
                // * a      →      * a
                //   []     →      * []
                else if (isLastBlock) {
                    editor.execute('splitListItemBefore');
                    data.preventDefault();
                    evt.stop();
                }
            }
        }, { context: 'li' });
        // In some cases, after the default block splitting, we want to modify the new block to become a new list item
        // instead of an additional block in the same list item.
        this.listenTo(enterCommand, 'afterExecute', () => {
            const splitCommand = commands.get('splitListItemBefore');
            // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet.
            // Let's keep it up to date and take advantage of ListSplitCommand#isEnabled.
            splitCommand.refresh();
            if (!splitCommand.isEnabled) {
                return;
            }
            const doc = editor.model.document;
            const positionParent = doc.selection.getLastPosition().parent;
            const listItemBlocks = getAllListItemBlocks(positionParent);
            // Keep in mind this split happens after the default enter handler was executed. For instance:
            //
            // │       Initial state       │    After default enter    │   Here in #afterExecute   │
            // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤
            // │          * a[]            │           * a             │           * a             │
            // │                           │             []            │           * []            │
            if (listItemBlocks.length === 2) {
                splitCommand.execute();
            }
        });
    }
    /**
     * Attaches a listener to the {@link module:engine/view/document~Document#event:tab} event and handles tab key and tab+shift keys
     * presses in document lists.
     */
    _setupTabIntegration() {
        const editor = this.editor;
        this.listenTo(editor.editing.view.document, 'tab', (evt, data) => {
            const commandName = data.shiftKey ? 'outdentList' : 'indentList';
            const command = this.editor.commands.get(commandName);
            if (command.isEnabled) {
                editor.execute(commandName);
                data.stopPropagation();
                data.preventDefault();
                evt.stop();
            }
        }, { context: 'li' });
    }
    /**
     * Registers the conversion helpers for the document-list feature.
     */
    _setupConversion() {
        const editor = this.editor;
        const model = editor.model;
        const attributeNames = this.getListAttributeNames();
        const multiBlock = editor.config.get('list.multiBlock');
        const elementName = multiBlock ? 'paragraph' : 'listItem';
        editor.conversion.for('upcast')
            // Convert <li> to a generic paragraph (or listItem element) so the content of <li> is always inside a block.
            // Setting the listType attribute to let other features (to-do list) know that this is part of a list item.
            // This is also important to properly handle simple lists so that paragraphs inside a list item won't break the list item.
            // <li>  <-- converted to listItem
            //   <p></p> <-- should be also converted to listItem, so it won't split and replace the listItem generated from the above li.
            .elementToElement({
            view: 'li',
            model: (viewElement, { writer }) => writer.createElement(elementName, { listType: '' })
        })
            // Convert paragraph to the list block (without list type defined yet).
            // This is important to properly handle bogus paragraph and to-do lists.
            // Most of the time the bogus paragraph should not appear in the data of to-do list,
            // but if there is any marker or an attribute on the paragraph then the bogus paragraph
            // is preserved in the data, and we need to be able to detect this case.
            .elementToElement({
            view: 'p',
            model: (viewElement, { writer }) => {
                if (viewElement.parent && viewElement.parent.is('element', 'li')) {
                    return writer.createElement(elementName, { listType: '' });
                }
                return null;
            },
            converterPriority: 'high'
        })
            .add(dispatcher => {
            dispatcher.on('element:li', listItemUpcastConverter());
            dispatcher.on('element:ul', listUpcastCleanList(), { priority: 'high' });
            dispatcher.on('element:ol', listUpcastCleanList(), { priority: 'high' });
        });
        if (!multiBlock) {
            editor.conversion.for('downcast')
                .elementToElement({
                model: 'listItem',
                view: 'p'
            });
        }
        editor.conversion.for('editingDowncast')
            .elementToElement({
            model: elementName,
            view: bogusParagraphCreator(attributeNames),
            converterPriority: 'high'
        })
            .add(dispatcher => {
            dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model));
            dispatcher.on('remove', listItemDowncastRemoveConverter(model.schema));
        });
        editor.conversion.for('dataDowncast')
            .elementToElement({
            model: elementName,
            view: bogusParagraphCreator(attributeNames, { dataPipeline: true }),
            converterPriority: 'high'
        })
            .add(dispatcher => {
            dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model, { dataPipeline: true }));
        });
        const modelToViewPositionMapper = createModelToViewPositionMapper(this._downcastStrategies, editor.editing.view);
        editor.editing.mapper.on('modelToViewPosition', modelToViewPositionMapper);
        editor.data.mapper.on('modelToViewPosition', modelToViewPositionMapper);
        this.listenTo(model.document, 'change:data', reconvertItemsOnDataChange(model, editor.editing, attributeNames, this), { priority: 'high' });
        // For LI verify if an ID of the attribute element is correct.
        this.on('checkAttributes:item', (evt, { viewElement, modelAttributes }) => {
            if (viewElement.id != modelAttributes.listItemId) {
                evt.return = true;
                evt.stop();
            }
        });
        // For UL and OL check if the name and ID of element is correct.
        this.on('checkAttributes:list', (evt, { viewElement, modelAttributes }) => {
            if (viewElement.name != getViewElementNameForListType(modelAttributes.listType) ||
                viewElement.id != getViewElementIdForListType(modelAttributes.listType, modelAttributes.listIndent)) {
                evt.return = true;
                evt.stop();
            }
        });
    }
    /**
     * Registers model post-fixers.
     */
    _setupModelPostFixing() {
        const model = this.editor.model;
        const attributeNames = this.getListAttributeNames();
        // Register list fixing.
        // First the low level handler.
        model.document.registerPostFixer(writer => modelChangePostFixer(model, writer, attributeNames, this));
        // Then the callbacks for the specific lists.
        // The indentation fixing must be the first one...
        this.on('postFixer', (evt, { listNodes, writer }) => {
            evt.return = fixListIndents(listNodes, writer) || evt.return;
        }, { priority: 'high' });
        // ...then the item ids... and after that other fixers that rely on the correct indentation and ids.
        this.on('postFixer', (evt, { listNodes, writer, seenIds }) => {
            evt.return = fixListItemIds(listNodes, seenIds, writer) || evt.return;
        }, { priority: 'high' });
    }
    /**
     * Integrates the feature with the clipboard via {@link module:engine/model/model~Model#insertContent} and
     * {@link module:engine/model/model~Model#getSelectedContent}.
     */
    _setupClipboardIntegration() {
        const model = this.editor.model;
        const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
        this.listenTo(model, 'insertContent', createModelIndentPasteFixer(model), { priority: 'high' });
        // To enhance the UX, the editor should not copy list attributes to the clipboard if the selection
        // started and ended in the same list item.
        //
        // If the selection was enclosed in a single list item, there is a good chance the user did not want it
        // copied as a list item but plain blocks.
        //
        // This avoids pasting orphaned list items instead of paragraphs, for instance, straight into the root.
        //
        //	                       ┌─────────────────────┬───────────────────┐
        //	                       │ Selection           │ Clipboard content │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ [* <Widget />]      │ <Widget />        │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ [* Foo]             │ Foo               │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ * Foo [bar] baz     │ bar               │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ * Fo[o              │ o                 │
        //	                       │   ba]r              │ ba                │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ * Fo[o              │ * o               │
        //	                       │ * ba]r              │ * ba              │
        //	                       ├─────────────────────┼───────────────────┤
        //	                       │ [* Foo              │ * Foo             │
        //	                       │  * bar]             │ * bar             │
        //	                       └─────────────────────┴───────────────────┘
        //
        // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969
        this.listenTo(clipboardPipeline, 'outputTransformation', (evt, data) => {
            model.change(writer => {
                // Remove last block if it's empty.
                const allContentChildren = Array.from(data.content.getChildren());
                const lastItem = allContentChildren[allContentChildren.length - 1];
                if (allContentChildren.length > 1 && lastItem.is('element') && lastItem.isEmpty) {
                    const contentChildrenExceptLastItem = allContentChildren.slice(0, -1);
                    if (contentChildrenExceptLastItem.every(isListItemBlock)) {
                        writer.remove(lastItem);
                    }
                }
                // Copy/cut only content of a list item (for drag-drop move the whole list item).
                if (data.method == 'copy' || data.method == 'cut') {
                    const allChildren = Array.from(data.content.getChildren());
                    const isSingleListItemSelected = isSingleListItem(allChildren);
                    if (isSingleListItemSelected) {
                        removeListAttributes(allChildren, writer);
                    }
                }
            });
        });
    }
    /**
     * Informs editor accessibility features about keystrokes brought by the plugin.
     */
    _setupAccessibilityIntegration() {
        const editor = this.editor;
        const t = editor.t;
        editor.accessibility.addKeystrokeInfoGroup({
            id: 'list',
            label: t('Keystrokes that can be used in a list'),
            keystrokes: [
                {
                    label: t('Increase list item indent'),
                    keystroke: 'Tab'
                },
                {
                    label: t('Decrease list item indent'),
                    keystroke: 'Shift+Tab'
                }
            ]
        });
    }
}
/**
 * Post-fixer that reacts to changes on document and fixes incorrect model states (invalid `listItemId` and `listIndent` values).
 *
 * In the example below, there is a correct list structure.
 * Then the middle element is removed so the list structure will become incorrect:
 *
 * ```xml
 * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
 * <paragraph listType="bulleted" listItemId="b" listIndent=1>Item 2</paragraph>   <--- this is removed.
 * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
 * ```
 *
 * The list structure after the middle element is removed:
 *
 * ```xml
 * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
 * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
 * ```
 *
 * Should become:
 *
 * ```xml
 * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
 * <paragraph listType="bulleted" listItemId="c" listIndent=1>Item 3</paragraph>   <--- note that indent got post-fixed.
 * ```
 *
 * @param model The data model.
 * @param writer The writer to do changes with.
 * @param attributeNames The list of all model list attributes (including registered strategies).
 * @param ListEditing The document list editing plugin.
 * @returns `true` if any change has been applied, `false` otherwise.
 */
function modelChangePostFixer(model, writer, attributeNames, listEditing) {
    const changes = model.document.differ.getChanges();
    const itemToListHead = new Map();
    const multiBlock = listEditing.editor.config.get('list.multiBlock');
    let applied = false;
    for (const entry of changes) {
        if (entry.type == 'insert' && entry.name != '$text') {
            const item = entry.position.nodeAfter;
            // Remove attributes in case of renamed element.
            if (!model.schema.checkAttribute(item, 'listItemId')) {
                for (const attributeName of Array.from(item.getAttributeKeys())) {
                    if (attributeNames.includes(attributeName)) {
                        writer.removeAttribute(attributeName, item);
                        applied = true;
                    }
                }
            }
            findAndAddListHeadToMap(entry.position, itemToListHead);
            // Insert of a non-list item - check if there is a list after it.
            if (!entry.attributes.has('listItemId')) {
                findAndAddListHeadToMap(entry.position.getShiftedBy(entry.length), itemToListHead);
            }
            // Check if there is no nested list.
            for (const { item: innerItem, previousPosition } of model.createRangeIn(item)) {
                if (isListItemBlock(innerItem)) {
                    findAndAddListHeadToMap(previousPosition, itemToListHead);
                }
            }
        }
        // Removed list item or block adjacent to a list.
        else if (entry.type == 'remove') {
            findAndAddListHeadToMap(entry.position, itemToListHead);
        }
        // Changed list item indent or type.
        else if (entry.type == 'attribute' && attributeNames.includes(entry.attributeKey)) {
            findAndAddListHeadToMap(entry.range.start, itemToListHead);
            if (entry.attributeNewValue === null) {
                findAndAddListHeadToMap(entry.range.start.getShiftedBy(1), itemToListHead);
            }
        }
        // Make sure that there is no left over listItem element without attributes or a block with list attributes that is not a listItem.
        if (!multiBlock && entry.type == 'attribute' && LIST_BASE_ATTRIBUTES.includes(entry.attributeKey)) {
            const element = entry.range.start.nodeAfter;
            if (entry.attributeNewValue === null && element && element.is('element', 'listItem')) {
                writer.rename(element, 'paragraph');
                applied = true;
            }
            else if (entry.attributeOldValue === null && element && element.is('element') && element.name != 'listItem') {
                writer.rename(element, 'listItem');
                applied = true;
            }
        }
    }
    // Make sure that IDs are not shared by split list.
    const seenIds = new Set();
    for (const listHead of itemToListHead.values()) {
        applied = listEditing.fire('postFixer', {
            listNodes: new ListBlocksIterable(listHead),
            listHead,
            writer,
            seenIds
        }) || applied;
    }
    return applied;
}
/**
 * A fixer for pasted content that includes list items.
 *
 * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
 *
 * Example:
 *
 * ```xml
 * <paragraph listType="bulleted" listItemId="a" listIndent="0">A</paragraph>
 * <paragraph listType="bulleted" listItemId="b" listIndent="1">B^</paragraph>
 * // At ^ paste:  <paragraph listType="numbered" listItemId="x" listIndent="0">X</paragraph>
 * //              <paragraph listType="numbered" listItemId="y" listIndent="1">Y</paragraph>
 * <paragraph listType="bulleted" listItemId="c" listIndent="2">C</paragraph>
 * ```
 *
 * Should become:
 *
 * ```xml
 * <paragraph listType="bulleted" listItemId="a" listIndent="0">A</paragraph>
 * <paragraph listType="bulleted" listItemId="b" listIndent="1">BX</paragraph>
 * <paragraph listType="bulleted" listItemId="y" listIndent="2">Y/paragraph>
 * <paragraph listType="bulleted" listItemId="c" listIndent="2">C</paragraph>
 * ```
 */
function createModelIndentPasteFixer(model) {
    return (evt, [content, selectable]) => {
        const items = content.is('documentFragment') ?
            Array.from(content.getChildren()) :
            [content];
        if (!items.length) {
            return;
        }
        const selection = selectable ?
            model.createSelection(selectable) :
            model.document.selection;
        const position = selection.getFirstPosition();
        // Get a reference list item. Attributes of the inserted list items will be fixed according to that item.
        let refItem;
        if (isListItemBlock(position.parent)) {
            refItem = position.parent;
        }
        else if (isListItemBlock(position.nodeBefore)) {
            refItem = position.nodeBefore;
        }
        else {
            return; // Content is not copied into a list.
        }
        model.change(writer => {
            const refType = refItem.getAttribute('listType');
            const refIndent = refItem.getAttribute('listIndent');
            const firstElementIndent = items[0].getAttribute('listIndent') || 0;
            const indentDiff = Math.max(refIndent - firstElementIndent, 0);
            for (const item of items) {
                const isListItem = isListItemBlock(item);
                if (refItem.is('element', 'listItem') && item.is('element', 'paragraph')) {
                    /**
                     * When paragraphs or a plain text list is pasted into a simple list, convert
                     * the `<paragraphs>' to `<listItem>' to avoid breaking the target list.
                     *
                     * See https://github.com/ckeditor/ckeditor5/issues/13826.
                     */
                    writer.rename(item, 'listItem');
                }
                writer.setAttributes({
                    listIndent: (isListItem ? item.getAttribute('listIndent') : 0) + indentDiff,
                    listItemId: isListItem ? item.getAttribute('listItemId') : ListItemUid.next(),
                    listType: refType
                }, item);
            }
        });
    };
}
/**
 * Decides whether the merge should be accompanied by the model's `deleteContent()`, for instance, to get rid of the inline
 * content in the selection or take advantage of the heuristics in `deleteContent()` that helps convert lists into paragraphs
 * in certain cases.
 */
function shouldMergeOnBlocksContentLevel(model, direction) {
    const selection = model.document.selection;
    if (!selection.isCollapsed) {
        return !getSelectedBlockObject(model);
    }
    if (direction === 'forward') {
        return true;
    }
    const firstPosition = selection.getFirstPosition();
    const positionParent = firstPosition.parent;
    const previousSibling = positionParent.previousSibling;
    if (model.schema.isObject(previousSibling)) {
        return false;
    }
    if (previousSibling.isEmpty) {
        return true;
    }
    return isSingleListItem([positionParent, previousSibling]);
}
