UNPKG

@ckeditor/ckeditor5-ui

Version:

The UI framework and standard UI library of CKEditor 5.

169 lines (168 loc) 6.89 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module ui/dropdown/menu/dropdownmenurootlistview */ import DropdownMenuListItemButtonView from './dropdownmenulistitembuttonview.js'; import DropdownMenuNestedMenuView from './dropdownmenunestedmenuview.js'; import DropdownMenuListView from './dropdownmenulistview.js'; import DropdownMenuListItemView from './dropdownmenulistitemview.js'; import { DropdownRootMenuBehaviors } from './dropdownmenubehaviors.js'; /** * Creates and manages a multi-level menu UI structure, suitable to be used inside dropdown components. * * This class creates a menu structure based on {@link module:ui/dropdown/menu/utils~DropdownMenuDefinition declarative definition} * passed in the constructor. * * Below is an example of a simple definition, that describes a menu with two sub-menus (Menu 1, Menu 2) and four buttons in total (Item A, * Item B, Item C, Item D): * * ```js * [ * { * id: 'menu_1', * menu: 'Menu 1', * children: [ * { id: 'menu_1_a', label: 'Item A' }, * { id: 'menu_1_b', label: 'Item B' } * ] * }, * { * id: 'menu_2', * menu: 'Menu 2', * children: [ * { id: 'menu_2_c', label: 'Item C' }, * ] * }, * { id: 'item_d', label: 'Item D' } * ] * ``` * * The menu is build using multiple view classes. The most important are: * * * {@link module:ui/dropdown/menu/dropdownmenunestedmenuview~DropdownMenuNestedMenuView `DropdownMenuNestedMenuView`} - "menu" - provides * a panel with a nested menu, and a button which opens the panel, * * {@link module:ui/dropdown/menu/dropdownmenulistitembuttonview~DropdownMenuListItemButtonView `DropdownMenuListItemButtonView`} - * "button" or "leaf button" (as opposed to buttons provided by `DropdownMenuNestedMenuView`) - should trigger some action when pressed. * * Instances of these two classes are created based on the data provided in definitions. They are assigned proper IDs and labels. * Other view classes build a proper DOM structure around menus and buttons. * * The `DropdownMenuNestedMenuView` instances provides panels, which may include further menus or buttons. These panels are added to * a `BodyCollection` view, which means they are appended outside the DOM editor and UI structure. * * When "leaf button" is pressed, it fires `execute` event which is delegated to `DropdownMenuRootListView` as `menu:execute` event. You * can listen to this event to perform an action: * * ```js * rootListView.on( 'menu:execute', evt => { * console.log( evt.source.id ); // E.g. will print 'menu_1_a' when 'Item A' is pressed. * } ); * ``` * * All menus and "leaf" buttons created from the definition can be easily accessed through {@link ~DropdownMenuRootListView#menus `menus`} * and {@link ~DropdownMenuRootListView#buttons `buttons`} properties. * * For performance reasons, the whole menu structure is created only when `DropdownMenuRootListView` is rendered for the first time. * * It is recommended to use this class together with {@link module:ui/dropdown/utils~addMenuToDropdown `addMenuToDropdown()` helper}. */ export default class DropdownMenuRootListView extends DropdownMenuListView { /** * The definitions object used to create the whole menu structure. */ _definition; /** * Cached array of all menus in the dropdown menu (including nested menus). */ _cachedMenus = []; /** * Cached array of all buttons in the dropdown menu (including buttons in nested menus). */ _cachedButtons = []; /** * Editor body collection into which nested menus panels will be appended. */ _bodyCollection; /** * Creates an instance of the DropdownMenuRootListView class. * * @param locale * @param bodyCollection * @param definition The definition object used to create the menu structure. */ constructor(locale, bodyCollection, definition) { super(locale); this._bodyCollection = bodyCollection; this._definition = definition; this.set('menuPanelClass', undefined); } /** * Returns the array of all menus in the dropdown menu (including nested menus). */ get menus() { return Array.from(this._cachedMenus.values()); } /** * Returns the array of all buttons in the dropdown menu (including buttons in nested menus). * * Note, that this includes only "leaf" buttons, as specified in the definition passed in constructor. Buttons created as a part of * the nested menus, that open nested menus when hovered or pressed, are not included. */ get buttons() { return Array.from(this._cachedButtons.values()); } /** * @inheritDoc */ render() { this._createStructure(this._definition, null); super.render(); DropdownRootMenuBehaviors.toggleMenusAndFocusItemsOnHover(this); DropdownRootMenuBehaviors.closeMenuWhenAnotherOnTheSameLevelOpens(this); } /** * Closes all nested menus. */ closeMenus() { this.menus.forEach(menu => { menu.isOpen = false; }); } /** * Recursively creates the whole view tree structure for the dropdown menu, according to the passed `definitions`. * * @private */ _createStructure(definitions, parentMenuView) { const items = []; for (const def of definitions) { let createdView; if ('menu' in def) { createdView = new DropdownMenuNestedMenuView(this.locale, this._bodyCollection, def.id, def.menu, parentMenuView); createdView.panelView.bind('class').to(this, 'menuPanelClass'); if (!parentMenuView) { createdView.delegate(...DropdownMenuNestedMenuView.DELEGATED_EVENTS).to(this, (name) => `menu:${name}`); } this._cachedMenus.push(createdView); this._createStructure(def.children, createdView); } else { createdView = new DropdownMenuListItemButtonView(this.locale, def.id, def.label); if (!parentMenuView) { createdView.delegate('execute').to(this, 'menu:execute'); } this._cachedButtons.push(createdView); } const listItemView = new DropdownMenuListItemView(this.locale, parentMenuView, createdView); if (!parentMenuView) { listItemView.delegate('mouseenter').to(this, 'menu:mouseenter'); } items.push(listItemView); } const targetListView = parentMenuView ? parentMenuView.listView : this; targetListView.items.addMany(items); } }