@ckeditor/ckeditor5-ui
Version:
The UI framework and standard UI library of CKEditor 5.
216 lines (215 loc) • 7.7 kB
JavaScript
/**
* @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/dropdownmenunestedmenuview
*/
import { FocusTracker, KeystrokeHandler, global } from '@ckeditor/ckeditor5-utils';
import DropdownMenuButtonView from './dropdownmenubuttonview.js';
import DropdownMenuListView from './dropdownmenulistview.js';
import { DropdownMenuPanelPositioningFunctions } from './utils.js';
import { DropdownMenuBehaviors } from './dropdownmenubehaviors.js';
import View from '../../view.js';
import DropdownMenuNestedMenuPanelView from './dropdownmenunestedmenupanelview.js';
import '../../../theme/components/dropdown/menu/dropdownmenu.css';
/**
* Represents a nested menu view.
*/
class DropdownMenuNestedMenuView extends View {
/**
* An array of delegated events for the dropdown menu definition controller.
* These events are delegated to the dropdown menu element.
*/
// Due to some spaghetti code we need to delegate `change:isOpen`.
static DELEGATED_EVENTS = [
'mouseenter', 'execute', 'change:isOpen'
];
id;
/**
* Button of the menu view.
*/
buttonView;
/**
* Panel of the menu. It hosts children of the menu.
*/
panelView;
/**
* List of nested menu entries.
*/
listView;
/**
* Tracks information about the DOM focus in the menu.
*/
focusTracker;
/**
* Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages
* keystrokes of the menu.
*/
keystrokes;
_bodyCollection;
/**
* Creates a new instance of the DropdownMenuView class.
*
* @param locale
* @param bodyCollection
* @param id
* @param label The label for the dropdown menu button.
* @param parentMenuView The parent dropdown menu view, if any.
*/
constructor(locale, bodyCollection, id, label, parentMenuView) {
super(locale);
this._bodyCollection = bodyCollection;
this.id = id;
this.set({
isOpen: false,
isEnabled: true,
panelPosition: 'w',
class: undefined,
parentMenuView: null
});
this.keystrokes = new KeystrokeHandler();
this.focusTracker = new FocusTracker();
this.buttonView = new DropdownMenuButtonView(locale);
this.buttonView.delegate('mouseenter').to(this);
this.buttonView.bind('isOn', 'isEnabled').to(this, 'isOpen', 'isEnabled');
this.buttonView.label = label;
this.panelView = new DropdownMenuNestedMenuPanelView(locale);
this.panelView.isVisible = true;
this.listView = new DropdownMenuListView(locale);
this.listView.bind('ariaLabel').to(this.buttonView, 'label');
this.panelView.content.add(this.listView);
const bind = this.bindTemplate;
this.setTemplate({
tag: 'div',
attributes: {
class: [
'ck',
'ck-dropdown-menu-list__nested-menu',
bind.to('class'),
bind.if('isEnabled', 'ck-disabled', value => !value)
],
role: 'presentation'
},
children: [
this.buttonView
]
});
this.parentMenuView = parentMenuView;
if (this.parentMenuView) {
this._handleParentMenuView();
}
this._attachBehaviors();
}
/**
* An array of positioning functions used to determine the position of the dropdown menu panel.
* The order of the functions in the array determines the priority of the positions to be tried.
* The first function that returns a valid position will be used.
*
* @returns {Array<PositioningFunction>} An array of positioning functions.
* @internal
*/
get _panelPositions() {
const { westSouth, eastSouth, westNorth, eastNorth } = DropdownMenuPanelPositioningFunctions;
if (this.locale.uiLanguageDirection === 'ltr') {
return [eastSouth, eastNorth, westSouth, westNorth];
}
else {
return [westSouth, westNorth, eastSouth, eastNorth];
}
}
/**
* @inheritDoc
*/
render() {
super.render();
this.panelView.render();
this.focusTracker.add(this.buttonView.element);
this.focusTracker.add(this.panelView.element);
this.focusTracker.add(this.listView);
// Listen for keystrokes coming from within #element.
this.keystrokes.listenTo(this.element);
this._mountPanelOnOpen();
}
/**
* @inheritDoc
*/
destroy() {
this._removePanelFromBody();
this.panelView.destroy();
super.destroy();
}
/**
* @inheritDoc
*/
focus() {
this.buttonView.focus();
}
_handleParentMenuView() {
// Propagate events from this component to parent-menu.
this.delegate(...DropdownMenuNestedMenuView.DELEGATED_EVENTS).to(this.parentMenuView);
// Close this menu if its parent closes.
DropdownMenuBehaviors.closeOnParentClose(this, this.parentMenuView);
}
/**
* Attach all keyboard behaviors for the menu view.
*/
_attachBehaviors() {
DropdownMenuBehaviors.openOnButtonClick(this);
DropdownMenuBehaviors.openAndFocusOnEnterKeyPress(this);
DropdownMenuBehaviors.openOnArrowRightKey(this);
DropdownMenuBehaviors.closeOnEscKey(this);
DropdownMenuBehaviors.closeOnArrowLeftKey(this);
}
/**
* Mounts the portal view in the body when the menu is open and removes it when the menu is closed.
* Binds keystrokes to the portal view when the menu is open.
*/
_mountPanelOnOpen() {
const { panelView } = this;
this.on('change:isOpen', (evt, name, isOpen) => {
// Ensure that the event was triggered by this instance.
// TODO: Remove checking `evt.source` if `change:isOpen` is no longer delegated.
if (evt.source !== this) {
return;
}
// Removes the panel view from the body when the menu is closed.
if (!isOpen && this._bodyCollection.has(panelView)) {
this._removePanelFromBody();
return;
}
// Adds the panel view to the body when the menu is open.
if (isOpen && !this._bodyCollection.has(panelView)) {
this._addPanelToBody();
}
});
}
/**
* Removes the panel view from the editor's body and removes it from the focus tracker.
*/
_removePanelFromBody() {
const { panelView, keystrokes } = this;
if (this._bodyCollection.has(panelView)) {
this._bodyCollection.remove(panelView);
keystrokes.stopListening(panelView.element);
}
}
/**
* Adds the panel view to the editor's body and sets up event listeners.
*/
_addPanelToBody() {
const { panelView, buttonView, keystrokes } = this;
if (!this._bodyCollection.has(panelView)) {
this._bodyCollection.add(panelView);
keystrokes.listenTo(panelView.element);
panelView.pin({
positions: this._panelPositions,
limiter: global.document.body,
element: panelView.element,
target: buttonView.element,
fitInViewport: true
});
}
}
}
export default DropdownMenuNestedMenuView;