/**
 * @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 ui/toolbar/toolbarview
 */
import View from '../view.js';
import FocusCycler, { isFocusable } from '../focuscycler.js';
import ToolbarSeparatorView from './toolbarseparatorview.js';
import ToolbarLineBreakView from './toolbarlinebreakview.js';
import preventDefault from '../bindings/preventdefault.js';
import { createDropdown, addToolbarToDropdown } from '../dropdown/utils.js';
import normalizeToolbarConfig from './normalizetoolbarconfig.js';
import { FocusTracker, KeystrokeHandler, Rect, ResizeObserver, global, isVisible, logWarning } from '@ckeditor/ckeditor5-utils';
import { icons } from '@ckeditor/ckeditor5-core';
import { isObject } from 'lodash-es';
import '../../theme/components/toolbar/toolbar.css';
const { threeVerticalDots } = icons;
export const NESTED_TOOLBAR_ICONS = {
    alignLeft: icons.alignLeft,
    bold: icons.bold,
    importExport: icons.importExport,
    paragraph: icons.paragraph,
    plus: icons.plus,
    text: icons.text,
    threeVerticalDots: icons.threeVerticalDots,
    pilcrow: icons.pilcrow,
    dragIndicator: icons.dragIndicator
};
/**
 * The toolbar view class.
 */
export default class ToolbarView extends View {
    /**
     * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
     *
     * Also see {@link #render}.
     *
     * @param locale The localization services instance.
     * @param options Configuration options of the toolbar.
     */
    constructor(locale, options) {
        super(locale);
        const bind = this.bindTemplate;
        const t = this.t;
        this.options = options || {};
        this.set('ariaLabel', t('Editor toolbar'));
        this.set('maxWidth', 'auto');
        this.items = this.createCollection();
        this.focusTracker = new FocusTracker();
        this.keystrokes = new KeystrokeHandler();
        this.set('class', undefined);
        this.set('isCompact', false);
        this.itemsView = new ItemsView(locale);
        this.children = this.createCollection();
        this.children.add(this.itemsView);
        this.focusables = this.createCollection();
        const isRtl = locale.uiLanguageDirection === 'rtl';
        this._focusCycler = new FocusCycler({
            focusables: this.focusables,
            focusTracker: this.focusTracker,
            keystrokeHandler: this.keystrokes,
            actions: {
                // Navigate toolbar items backwards using the arrow[left,up] keys.
                focusPrevious: [isRtl ? 'arrowright' : 'arrowleft', 'arrowup'],
                // Navigate toolbar items forwards using the arrow[right,down] keys.
                focusNext: [isRtl ? 'arrowleft' : 'arrowright', 'arrowdown']
            }
        });
        const classes = [
            'ck',
            'ck-toolbar',
            bind.to('class'),
            bind.if('isCompact', 'ck-toolbar_compact')
        ];
        if (this.options.shouldGroupWhenFull && this.options.isFloating) {
            classes.push('ck-toolbar_floating');
        }
        this.setTemplate({
            tag: 'div',
            attributes: {
                class: classes,
                role: 'toolbar',
                'aria-label': bind.to('ariaLabel'),
                style: {
                    maxWidth: bind.to('maxWidth')
                },
                tabindex: -1
            },
            children: this.children,
            on: {
                // https://github.com/ckeditor/ckeditor5-ui/issues/206
                mousedown: preventDefault(this)
            }
        });
        this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping(this) : new StaticLayout(this);
    }
    /**
     * @inheritDoc
     */
    render() {
        super.render();
        this.focusTracker.add(this.element);
        // Children added before rendering should be known to the #focusTracker.
        for (const item of this.items) {
            this.focusTracker.add(item.element);
        }
        this.items.on('add', (evt, item) => {
            this.focusTracker.add(item.element);
        });
        this.items.on('remove', (evt, item) => {
            this.focusTracker.remove(item.element);
        });
        // Start listening for the keystrokes coming from #element.
        this.keystrokes.listenTo(this.element);
        this._behavior.render(this);
    }
    /**
     * @inheritDoc
     */
    destroy() {
        this._behavior.destroy();
        this.focusTracker.destroy();
        this.keystrokes.destroy();
        return super.destroy();
    }
    /**
     * Focuses the first focusable in {@link #focusables}.
     */
    focus() {
        this._focusCycler.focusFirst();
    }
    /**
     * Focuses the last focusable in {@link #focusables}.
     */
    focusLast() {
        this._focusCycler.focusLast();
    }
    /**
     * A utility that expands the plain toolbar configuration into
     * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
     *
     * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
     * @param factory A factory producing toolbar items.
     * @param removeItems An array of items names to be removed from the configuration. When present, applies
     * to this toolbar and all nested ones as well.
     */
    fillFromConfig(itemsOrConfig, factory, removeItems) {
        this.items.addMany(this._buildItemsFromConfig(itemsOrConfig, factory, removeItems));
    }
    /**
     * A utility that expands the plain toolbar configuration into a list of view items using a given component factory.
     *
     * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
     * @param factory A factory producing toolbar items.
     * @param removeItems An array of items names to be removed from the configuration. When present, applies
     * to this toolbar and all nested ones as well.
     */
    _buildItemsFromConfig(itemsOrConfig, factory, removeItems) {
        const config = normalizeToolbarConfig(itemsOrConfig);
        const normalizedRemoveItems = removeItems || config.removeItems;
        const itemsToAdd = this._cleanItemsConfiguration(config.items, factory, normalizedRemoveItems)
            .map(item => {
            if (isObject(item)) {
                return this._createNestedToolbarDropdown(item, factory, normalizedRemoveItems);
            }
            else if (item === '|') {
                return new ToolbarSeparatorView();
            }
            else if (item === '-') {
                return new ToolbarLineBreakView();
            }
            return factory.create(item);
        })
            .filter((item) => !!item);
        return itemsToAdd;
    }
    /**
     * Cleans up the {@link module:ui/toolbar/toolbarview~ToolbarView#items} of the toolbar by removing unwanted items and
     * duplicated (obsolete) separators or line breaks.
     *
     * @param items The toolbar items configuration.
     * @param factory A factory producing toolbar items.
     * @param removeItems An array of items names to be removed from the configuration.
     * @returns Items after the clean-up.
     */
    _cleanItemsConfiguration(items, factory, removeItems) {
        const filteredItems = items
            .filter((item, idx, items) => {
            if (item === '|') {
                return true;
            }
            // Items listed in `config.removeItems` should not be added to the toolbar.
            if (removeItems.indexOf(item) !== -1) {
                return false;
            }
            if (item === '-') {
                // The toolbar line breaks must not be rendered when toolbar grouping is enabled.
                // (https://github.com/ckeditor/ckeditor5/issues/8582)
                if (this.options.shouldGroupWhenFull) {
                    /**
                     * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
                     * is disabled in the toolbar configuration.
                     * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
                     *
                     * ```ts
                     * const config = {
                     * 	toolbar: {
                     * 		items: [ ... ],
                     * 		shouldNotGroupWhenFull: true
                     * 	}
                     * }
                     * ```
                     *
                     * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
                     *
                     * @error toolbarview-line-break-ignored-when-grouping-items
                     */
                    logWarning('toolbarview-line-break-ignored-when-grouping-items', items);
                    return false;
                }
                return true;
            }
            // For the items that cannot be instantiated we are sending warning message. We also filter them out.
            if (!isObject(item) && !factory.has(item)) {
                /**
                 * There was a problem processing the configuration of the toolbar. The item with the given
                 * name does not exist so it was omitted when rendering the toolbar.
                 *
                 * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
                 * to provide a toolbar item has not been loaded or there is a typo in the
                 * {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
                 *
                 * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
                 * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
                 *
                 * You can use the following snippet to retrieve all available toolbar items:
                 *
                 * ```ts
                 * Array.from( editor.ui.componentFactory.names() );
                 * ```
                 *
                 * @error toolbarview-item-unavailable
                 * @param item The name of the component or nested toolbar definition.
                 */
                logWarning('toolbarview-item-unavailable', { item });
                return false;
            }
            return true;
        });
        return this._cleanSeparatorsAndLineBreaks(filteredItems);
    }
    /**
     * Remove leading, trailing, and duplicated separators (`-` and `|`).
     *
     * @returns Toolbar items after the separator and line break clean-up.
     */
    _cleanSeparatorsAndLineBreaks(items) {
        const nonSeparatorPredicate = (item) => (item !== '-' && item !== '|');
        const count = items.length;
        // Find an index of the first item that is not a separator.
        const firstCommandItemIndex = items.findIndex(nonSeparatorPredicate);
        // Items include separators only. There is no point in displaying them.
        if (firstCommandItemIndex === -1) {
            return [];
        }
        // Search from the end of the list, then convert found index back to the original direction.
        const lastCommandItemIndex = count - items
            .slice()
            .reverse()
            .findIndex(nonSeparatorPredicate);
        return items
            // Return items without the leading and trailing separators.
            .slice(firstCommandItemIndex, lastCommandItemIndex)
            // Remove duplicated separators.
            .filter((name, idx, items) => {
            // Filter only separators.
            if (nonSeparatorPredicate(name)) {
                return true;
            }
            const isDuplicated = idx > 0 && items[idx - 1] === name;
            return !isDuplicated;
        });
    }
    /**
     * Creates a user-defined dropdown containing a toolbar with items.
     *
     * @param definition A definition of the nested toolbar dropdown.
     * @param definition.label A label of the dropdown.
     * @param definition.icon An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft',
     * 'paragraph' or an SVG string. When `false` is passed, no icon will be used.
     * @param definition.withText When set `true`, the label of the dropdown will be visible. See
     * {@link module:ui/button/buttonview~ButtonView#withText} to learn more.
     * @param definition.tooltip A tooltip of the dropdown button. See
     * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more. Defaults to `true`.
     * @param componentFactory Component factory used to create items
     * of the nested toolbar.
     */
    _createNestedToolbarDropdown(definition, componentFactory, removeItems) {
        let { label, icon, items, tooltip = true, withText = false } = definition;
        items = this._cleanItemsConfiguration(items, componentFactory, removeItems);
        // There is no point in rendering a dropdown without items.
        if (!items.length) {
            return null;
        }
        const locale = this.locale;
        const dropdownView = createDropdown(locale);
        if (!label) {
            /**
             * A dropdown definition in the toolbar configuration is missing a text label.
             *
             * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies.
             * Make sure the `label` property is set in your drop-down configuration:
             *
             * ```json
             * {
             * 	label: 'A human-readable label',
             * 	icon: '...',
             * 	items: [ ... ]
             * },
             * ```
             *
             * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
             *
             * @error toolbarview-nested-toolbar-dropdown-missing-label
             */
            logWarning('toolbarview-nested-toolbar-dropdown-missing-label', definition);
        }
        dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown';
        dropdownView.buttonView.set({
            label,
            tooltip,
            withText: !!withText
        });
        // Allow disabling icon by passing false.
        if (icon !== false) {
            // A pre-defined icon picked by name, SVG string, a fallback (default) icon.
            dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[icon] || icon || threeVerticalDots;
        }
        // If the icon is disabled, display the label automatically.
        else {
            dropdownView.buttonView.withText = true;
        }
        addToolbarToDropdown(dropdownView, () => (dropdownView.toolbarView._buildItemsFromConfig(items, componentFactory, removeItems)));
        return dropdownView;
    }
}
/**
 * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
 * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
 */
class ItemsView extends View {
    /**
     * @inheritDoc
     */
    constructor(locale) {
        super(locale);
        this.children = this.createCollection();
        this.setTemplate({
            tag: 'div',
            attributes: {
                class: [
                    'ck',
                    'ck-toolbar__items'
                ]
            },
            children: this.children
        });
    }
}
/**
 * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
 * At the same time, it also makes it possible to display a toolbar with a vertical layout
 * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
 */
class StaticLayout {
    /**
     * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
     * behavior.
     *
     * @param view An instance of the toolbar that this behavior is added to.
     */
    constructor(view) {
        const bind = view.bindTemplate;
        // Static toolbar can be vertical when needed.
        view.set('isVertical', false);
        // 1:1 pass–through binding, all ToolbarView#items are visible.
        view.itemsView.children.bindTo(view.items).using(item => item);
        // 1:1 pass–through binding, all ToolbarView#items are focusable.
        view.focusables.bindTo(view.items).using(item => isFocusable(item) ? item : null);
        view.extendTemplate({
            attributes: {
                class: [
                    // When vertical, the toolbar has an additional CSS class.
                    bind.if('isVertical', 'ck-toolbar_vertical')
                ]
            }
        });
    }
    /**
     * @inheritDoc
     */
    render() { }
    /**
     * @inheritDoc
     */
    destroy() { }
}
/**
 * A toolbar behavior that makes the items respond to changes in the geometry.
 *
 * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
 * that do not fit visually into a single row of the toolbar (due to limited space).
 * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
 *
 * ```
 *	┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
 *	| ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
 *	| |   ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐   | |
 *	| |   |       #ungroupedItems     | | ToolbarSeparatorView | |        #groupedItems       |   | |
 *	| |   └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘   | |
 *	| |                                  \---------- only when toolbar items overflow -------/    | |
 *	| └───────────────────────────────────────────────────────────────────────────────────────────┘ |
 *	└───────────────────────────────────────────────────────────────────────────────────────────────┘
 * ```
 */
class DynamicGrouping {
    /**
     * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
     * behavior.
     *
     * @param view An instance of the toolbar that this behavior is added to.
     */
    constructor(view) {
        /**
         * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
         * and manage items that do not fit into a single row.
         *
         * **Note:** Created in {@link #_enableGroupingOnResize}.
         *
         * @readonly
         */
        this.resizeObserver = null;
        /**
         * A cached value of the horizontal padding style used by {@link #_updateGrouping}
         * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
         * a single toolbar line. This value can be reused between updates because it is unlikely that
         * the padding will change and re–using `Window.getComputedStyle()` is expensive.
         *
         * @readonly
         */
        this.cachedPadding = null;
        /**
         * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
         * and should be executed immediately the next time the toolbar shows up.
         *
         * @readonly
         */
        this.shouldUpdateGroupingOnNextResize = false;
        this.view = view;
        this.viewChildren = view.children;
        this.viewFocusables = view.focusables;
        this.viewItemsView = view.itemsView;
        this.viewFocusTracker = view.focusTracker;
        this.viewLocale = view.locale;
        this.ungroupedItems = view.createCollection();
        this.groupedItems = view.createCollection();
        this.groupedItemsDropdown = this._createGroupedItemsDropdown();
        // Only those items that were not grouped are visible to the user.
        view.itemsView.children.bindTo(this.ungroupedItems).using(item => item);
        // Make sure all #items visible in the main space of the toolbar are "focuscyclable".
        this.ungroupedItems.on('change', this._updateFocusCyclableItems.bind(this));
        // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
        view.children.on('change', this._updateFocusCyclableItems.bind(this));
        // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
        // represented in either grouped or ungrouped items at the right index.
        // In other words #items == concat( #ungroupedItems, #groupedItems )
        // (in length and order).
        view.items.on('change', (evt, changeData) => {
            const index = changeData.index;
            const added = Array.from(changeData.added);
            // Removing.
            for (const removedItem of changeData.removed) {
                if (index >= this.ungroupedItems.length) {
                    this.groupedItems.remove(removedItem);
                }
                else {
                    this.ungroupedItems.remove(removedItem);
                }
            }
            // Adding.
            for (let currentIndex = index; currentIndex < index + added.length; currentIndex++) {
                const addedItem = added[currentIndex - index];
                if (currentIndex > this.ungroupedItems.length) {
                    this.groupedItems.add(addedItem, currentIndex - this.ungroupedItems.length);
                }
                else {
                    this.ungroupedItems.add(addedItem, currentIndex);
                }
            }
            // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
            // the toolbar to overflow.
            // Consequently if removed from grouped or ungrouped items, there is a chance
            // some new space is available and we could do some ungrouping.
            this._updateGrouping();
        });
        view.extendTemplate({
            attributes: {
                class: [
                    // To group items dynamically, the toolbar needs a dedicated CSS class.
                    'ck-toolbar_grouping'
                ]
            }
        });
    }
    /**
     * Enables dynamic items grouping based on the dimensions of the toolbar.
     *
     * @param view An instance of the toolbar that this behavior is added to.
     */
    render(view) {
        this.viewElement = view.element;
        this._enableGroupingOnResize();
        this._enableGroupingOnMaxWidthChange(view);
    }
    /**
     * Cleans up the internals used by this behavior.
     */
    destroy() {
        // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
        // so let's make sure it's actually destroyed along with the toolbar.
        this.groupedItemsDropdown.destroy();
        this.resizeObserver.destroy();
    }
    /**
     * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
     * and it will move them to the {@link #groupedItems} when it happens.
     *
     * At the same time, it will also check if there is enough space in the toolbar for the first of the
     * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
     * without the toolbar wrapping.
     */
    _updateGrouping() {
        // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
        // for instance before #render(), or after render but without a parent or a parent detached
        // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
        // nothing else. This happens, for instance, when the toolbar is detached from DOM and
        // some logic adds or removes its #items.
        if (!this.viewElement.ownerDocument.body.contains(this.viewElement)) {
            return;
        }
        // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
        // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
        // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
        // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
        // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
        if (!isVisible(this.viewElement)) {
            this.shouldUpdateGroupingOnNextResize = true;
            return;
        }
        // Remember how many items were initially grouped so at the it is possible to figure out if the number
        // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
        const initialGroupedItemsCount = this.groupedItems.length;
        let wereItemsGrouped;
        // Group #items as long as some wrap to the next row. This will happen, for instance,
        // when the toolbar is getting narrow and there is not enough space to display all items in
        // a single row.
        while (this._areItemsOverflowing) {
            this._groupLastItem();
            wereItemsGrouped = true;
        }
        // If none were grouped now but there were some items already grouped before,
        // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
        // for instance, the toolbar is stretching and there's more space in it than before.
        if (!wereItemsGrouped && this.groupedItems.length) {
            // Ungroup items as long as none are overflowing or there are none to ungroup left.
            while (this.groupedItems.length && !this._areItemsOverflowing) {
                this._ungroupFirstItem();
            }
            // If the ungrouping ended up with some item wrapping to the next row,
            // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
            // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
            // clean–up is vital for the algorithm.
            if (this._areItemsOverflowing) {
                this._groupLastItem();
            }
        }
        if (this.groupedItems.length !== initialGroupedItemsCount) {
            this.view.fire('groupedItemsUpdate');
        }
    }
    /**
     * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
     * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
     */
    get _areItemsOverflowing() {
        // An empty toolbar cannot overflow.
        if (!this.ungroupedItems.length) {
            return false;
        }
        const element = this.viewElement;
        const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
        const lastChildRect = new Rect(element.lastChild);
        const toolbarRect = new Rect(element);
        if (!this.cachedPadding) {
            const computedStyle = global.window.getComputedStyle(element);
            const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
            // parseInt() is essential because of quirky floating point numbers logic and DOM.
            // If the padding turned out too big because of that, the grouped items dropdown would
            // always look (from the Rect perspective) like it overflows (while it's not).
            this.cachedPadding = Number.parseInt(computedStyle[paddingProperty]);
        }
        if (uiLanguageDirection === 'ltr') {
            return lastChildRect.right > toolbarRect.right - this.cachedPadding;
        }
        else {
            return lastChildRect.left < toolbarRect.left + this.cachedPadding;
        }
    }
    /**
     * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
     * upon resize when there is little space available. Instead, the toolbar items are moved to the
     * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
     *
     * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
     * them in the dropdown if necessary. It will also observe the browser window for size changes in
     * the future and respond to them by grouping more items or reverting already grouped back, depending
     * on the visual space available.
     */
    _enableGroupingOnResize() {
        let previousWidth;
        // TODO: Consider debounce.
        this.resizeObserver = new ResizeObserver(this.viewElement, entry => {
            if (!previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize) {
                this.shouldUpdateGroupingOnNextResize = false;
                this._updateGrouping();
                previousWidth = entry.contentRect.width;
            }
        });
        this._updateGrouping();
    }
    /**
     * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
     * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
     */
    _enableGroupingOnMaxWidthChange(view) {
        view.on('change:maxWidth', () => {
            this._updateGrouping();
        });
    }
    /**
     * When called, it will remove the last item from {@link #ungroupedItems} and move it back
     * to the {@link #groupedItems} collection.
     *
     * The opposite of {@link #_ungroupFirstItem}.
     */
    _groupLastItem() {
        if (!this.groupedItems.length) {
            this.viewChildren.add(new ToolbarSeparatorView());
            this.viewChildren.add(this.groupedItemsDropdown);
            this.viewFocusTracker.add(this.groupedItemsDropdown.element);
        }
        this.groupedItems.add(this.ungroupedItems.remove(this.ungroupedItems.last), 0);
    }
    /**
     * Moves the very first item belonging to {@link #groupedItems} back
     * to the {@link #ungroupedItems} collection.
     *
     * The opposite of {@link #_groupLastItem}.
     */
    _ungroupFirstItem() {
        this.ungroupedItems.add(this.groupedItems.remove(this.groupedItems.first));
        if (!this.groupedItems.length) {
            this.viewChildren.remove(this.groupedItemsDropdown);
            this.viewChildren.remove(this.viewChildren.last);
            this.viewFocusTracker.remove(this.groupedItemsDropdown.element);
        }
    }
    /**
     * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
     * collection when there is not enough space in the toolbar to display all items in a single row.
     */
    _createGroupedItemsDropdown() {
        const locale = this.viewLocale;
        const t = locale.t;
        const dropdown = createDropdown(locale);
        dropdown.class = 'ck-toolbar__grouped-dropdown';
        // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
        // (https://github.com/ckeditor/ckeditor5/issues/5608)
        dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
        addToolbarToDropdown(dropdown, this.groupedItems);
        dropdown.buttonView.set({
            label: t('Show more items'),
            tooltip: true,
            tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
            icon: threeVerticalDots
        });
        return dropdown;
    }
    /**
     * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cyclable items}
     * collection so it represents the up–to–date state of the UI from the perspective of the user.
     *
     * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
     * it must be subject to focus cycling in the toolbar.
     *
     * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
     * to learn more about the purpose of this method.
     */
    _updateFocusCyclableItems() {
        this.viewFocusables.clear();
        this.ungroupedItems.map(item => {
            if (isFocusable(item)) {
                this.viewFocusables.add(item);
            }
        });
        if (this.groupedItems.length) {
            this.viewFocusables.add(this.groupedItemsDropdown);
        }
    }
}
