UNPKG

@ckeditor/ckeditor5-ui

Version:

The UI framework and standard UI library of CKEditor 5.

440 lines (439 loc) 14.8 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/dropdownview */ import View from '../view.js'; import { KeystrokeHandler, FocusTracker, getOptimalPosition } from '@ckeditor/ckeditor5-utils'; import '../../theme/components/dropdown/dropdown.css'; /** * The dropdown view class. It manages the dropdown button and dropdown panel. * * In most cases, the easiest way to create a dropdown is by using the {@link module:ui/dropdown/utils~createDropdown} * util: * * ```ts * const dropdown = createDropdown( locale ); * * // Configure dropdown's button properties: * dropdown.buttonView.set( { * label: 'A dropdown', * withText: true * } ); * * dropdown.render(); * * dropdown.panelView.element.textContent = 'Content of the panel'; * * // Will render a dropdown with a panel containing a "Content of the panel" text. * document.body.appendChild( dropdown.element ); * ``` * * If you want to add a richer content to the dropdown panel, you can use the {@link module:ui/dropdown/utils~addListToDropdown} * and {@link module:ui/dropdown/utils~addToolbarToDropdown} helpers. See more examples in * {@link module:ui/dropdown/utils~createDropdown} documentation. * * If you want to create a completely custom dropdown, then you can compose it manually: * * ```ts * const button = new DropdownButtonView( locale ); * const panel = new DropdownPanelView( locale ); * const dropdown = new DropdownView( locale, button, panel ); * * button.set( { * label: 'A dropdown', * withText: true * } ); * * dropdown.render(); * * panel.element.textContent = 'Content of the panel'; * * // Will render a dropdown with a panel containing a "Content of the panel" text. * document.body.appendChild( dropdown.element ); * ``` * * However, dropdown created this way will contain little behavior. You will need to implement handlers for actions * such as {@link module:ui/bindings/clickoutsidehandler~clickOutsideHandler clicking outside an open dropdown} * (which should close it) and support for arrow keys inside the panel. Therefore, unless you really know what * you do and you really need to do it, it is recommended to use the {@link module:ui/dropdown/utils~createDropdown} helper. */ class DropdownView extends View { /** * Button of the dropdown view. Clicking the button opens the {@link #panelView}. */ buttonView; /** * Panel of the dropdown. It opens when the {@link #buttonView} is * {@link module:ui/button/button~Button#event:execute executed} (i.e. clicked). * * Child views can be added to the panel's `children` collection: * * ```ts * dropdown.panelView.children.add( childView ); * ``` * * See {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#children} and * {@link module:ui/viewcollection~ViewCollection#add}. */ panelView; /** * Tracks information about the DOM focus in the dropdown. */ focusTracker; /** * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages * keystrokes of the dropdown: * * * <kbd>▼</kbd> opens the dropdown, * * <kbd>◀</kbd> and <kbd>Esc</kbd> closes the dropdown. */ keystrokes; /** * A child {@link module:ui/list/listview~ListView list view} of the dropdown located * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}. * * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}. */ listView; /** * A child toolbar of the dropdown located in the * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}. * * **Note**: Only supported when dropdown has a toolbar added using {@link module:ui/dropdown/utils~addToolbarToDropdown}. */ toolbarView; /** * A child menu component of the dropdown located * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}. * * **Note**: Only supported when dropdown has a menu added using {@link module:ui/dropdown/utils~addMenuToDropdown}. */ menuView; /** * Creates an instance of the dropdown. * * Also see {@link #render}. * * @param locale The localization services instance. */ constructor(locale, buttonView, panelView) { super(locale); const bind = this.bindTemplate; this.buttonView = buttonView; this.panelView = panelView; this.set('isOpen', false); this.set('isEnabled', true); this.set('class', undefined); this.set('id', undefined); this.set('panelPosition', 'auto'); // Toggle the visibility of the panel when the dropdown becomes open. this.panelView.bind('isVisible').to(this, 'isOpen'); this.keystrokes = new KeystrokeHandler(); this.focusTracker = new FocusTracker(); this.setTemplate({ tag: 'div', attributes: { class: [ 'ck', 'ck-dropdown', bind.to('class'), bind.if('isEnabled', 'ck-disabled', value => !value) ], id: bind.to('id'), 'aria-describedby': bind.to('ariaDescribedById') }, children: [ buttonView, panelView ] }); buttonView.extendTemplate({ attributes: { class: [ 'ck-dropdown__button' ], 'data-cke-tooltip-disabled': bind.to('isOpen') } }); } /** * @inheritDoc */ render() { super.render(); this.focusTracker.add(this.buttonView.element); this.focusTracker.add(this.panelView.element); // Toggle the dropdown when its button has been clicked. this.listenTo(this.buttonView, 'open', () => { this.isOpen = !this.isOpen; }); // Let the dropdown control the position of the panel. The position must // be updated every time the dropdown is open. this.on('change:isOpen', (evt, name, isOpen) => { if (!isOpen) { return; } // If "auto", find the best position of the panel to fit into the viewport. // Otherwise, simply assign the static position. if (this.panelPosition === 'auto') { const optimalPanelPosition = DropdownView._getOptimalPosition({ element: this.panelView.element, target: this.buttonView.element, fitInViewport: true, positions: this._panelPositions }); this.panelView.position = (optimalPanelPosition ? optimalPanelPosition.name : this._defaultPanelPositionName); } else { this.panelView.position = this.panelPosition; } }); // Listen for keystrokes coming from within #element. this.keystrokes.listenTo(this.element); const closeDropdown = (data, cancel) => { if (this.isOpen) { this.isOpen = false; cancel(); } }; // Open the dropdown panel using the arrow down key, just like with return or space. this.keystrokes.set('arrowdown', (data, cancel) => { // Don't open if the dropdown is disabled or already open. if (this.buttonView.isEnabled && !this.isOpen) { this.isOpen = true; cancel(); } }); // Block the right arrow key (until nested dropdowns are implemented). this.keystrokes.set('arrowright', (data, cancel) => { if (this.isOpen) { cancel(); } }); // Close the dropdown using the arrow left/escape key. this.keystrokes.set('arrowleft', closeDropdown); this.keystrokes.set('esc', closeDropdown); } /** * Focuses the {@link #buttonView}. */ focus() { this.buttonView.focus(); } /** * Returns {@link #panelView panel} positions to be used by the * {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`} * utility considering the direction of the language the UI of the editor is displayed in. */ get _panelPositions() { const { south, north, southEast, southWest, northEast, northWest, southMiddleEast, southMiddleWest, northMiddleEast, northMiddleWest } = DropdownView.defaultPanelPositions; if (this.locale.uiLanguageDirection !== 'rtl') { return [ southEast, southWest, southMiddleEast, southMiddleWest, south, northEast, northWest, northMiddleEast, northMiddleWest, north ]; } else { return [ southWest, southEast, southMiddleWest, southMiddleEast, south, northWest, northEast, northMiddleWest, northMiddleEast, north ]; } } /** * Returns the default position of the dropdown panel based on the direction of the UI language. * It is used when the {@link #panelPosition} is set to `'auto'` and the panel has not found a * suitable position to fit into the viewport. */ get _defaultPanelPositionName() { return this.locale.uiLanguageDirection === 'rtl' ? 'sw' : 'se'; } /** * A set of positioning functions used by the dropdown view to determine * the optimal position (i.e. fitting into the browser viewport) of its * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel} when * {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition} is set to 'auto'`. * * The available positioning functions are as follow: * * **South** * * * `south` * * ``` * [ Button ] * +-----------------+ * | Panel | * +-----------------+ * ``` * * * `southEast` * * ``` * [ Button ] * +-----------------+ * | Panel | * +-----------------+ * ``` * * * `southWest` * * ``` * [ Button ] * +-----------------+ * | Panel | * +-----------------+ * ``` * * * `southMiddleEast` * * ``` * [ Button ] * +-----------------+ * | Panel | * +-----------------+ * ``` * * * `southMiddleWest` * * ``` * [ Button ] * +-----------------+ * | Panel | * +-----------------+ * ``` * * **North** * * * `north` * * ``` * +-----------------+ * | Panel | * +-----------------+ * [ Button ] * ``` * * * `northEast` * * ``` * +-----------------+ * | Panel | * +-----------------+ * [ Button ] * ``` * * * `northWest` * * ``` * +-----------------+ * | Panel | * +-----------------+ * [ Button ] * ``` * * * `northMiddleEast` * * ``` * +-----------------+ * | Panel | * +-----------------+ * [ Button ] * ``` * * * `northMiddleWest` * * ``` * +-----------------+ * | Panel | * +-----------------+ * [ Button ] * ``` * * Positioning functions are compatible with {@link module:utils/dom/position~DomPoint}. * * The name that position function returns will be reflected in dropdown panel's class that * controls its placement. See {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition} * to learn more. */ static defaultPanelPositions = { south: (buttonRect, panelRect) => { return { top: buttonRect.bottom, left: buttonRect.left - (panelRect.width - buttonRect.width) / 2, name: 's' }; }, southEast: buttonRect => { return { top: buttonRect.bottom, left: buttonRect.left, name: 'se' }; }, southWest: (buttonRect, panelRect) => { return { top: buttonRect.bottom, left: buttonRect.left - panelRect.width + buttonRect.width, name: 'sw' }; }, southMiddleEast: (buttonRect, panelRect) => { return { top: buttonRect.bottom, left: buttonRect.left - (panelRect.width - buttonRect.width) / 4, name: 'sme' }; }, southMiddleWest: (buttonRect, panelRect) => { return { top: buttonRect.bottom, left: buttonRect.left - (panelRect.width - buttonRect.width) * 3 / 4, name: 'smw' }; }, north: (buttonRect, panelRect) => { return { top: buttonRect.top - panelRect.height, left: buttonRect.left - (panelRect.width - buttonRect.width) / 2, name: 'n' }; }, northEast: (buttonRect, panelRect) => { return { top: buttonRect.top - panelRect.height, left: buttonRect.left, name: 'ne' }; }, northWest: (buttonRect, panelRect) => { return { top: buttonRect.top - panelRect.height, left: buttonRect.left - panelRect.width + buttonRect.width, name: 'nw' }; }, northMiddleEast: (buttonRect, panelRect) => { return { top: buttonRect.top - panelRect.height, left: buttonRect.left - (panelRect.width - buttonRect.width) / 4, name: 'nme' }; }, northMiddleWest: (buttonRect, panelRect) => { return { top: buttonRect.top - panelRect.height, left: buttonRect.left - (panelRect.width - buttonRect.width) * 3 / 4, name: 'nmw' }; } }; /** * A function used to calculate the optimal position for the dropdown panel. */ static _getOptimalPosition = getOptimalPosition; } export default DropdownView;