File: //home/arjun/projects/buyercall/node_modules/@ckeditor/ckeditor5-ui/src/toolbar/toolbarview.js
/**
* @license Copyright (c) 2003-2022, 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';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import FocusCycler from '../focuscycler';
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
import ToolbarSeparatorView from './toolbarseparatorview';
import ToolbarLineBreakView from './toolbarlinebreakview';
import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
import preventDefault from '../bindings/preventdefault.js';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
import isVisible from '@ckeditor/ckeditor5-utils/src/dom/isvisible';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import normalizeToolbarConfig from './normalizetoolbarconfig';
import threeVerticalDots from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg';
import '../../theme/components/toolbar/toolbar.css';
/**
* The toolbar view class.
*
* @extends module:ui/view~View
* @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
*/
export default class ToolbarView extends View {
/**
* Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
*
* Also see {@link #render}.
*
* @param {module:utils/locale~Locale} locale The localization services instance.
* @param {module:ui/toolbar/toolbarview~ToolbarOptions} [options] Configuration options of the toolbar.
*/
constructor( locale, options ) {
super( locale );
const bind = this.bindTemplate;
const t = this.t;
/**
* A reference to the options object passed to the constructor.
*
* @readonly
* @member {module:ui/toolbar/toolbarview~ToolbarOptions}
*/
this.options = options || {};
/**
* Label used by assistive technologies to describe this toolbar element.
*
* @default 'Editor toolbar'
* @member {String} #ariaLabel
*/
this.set( 'ariaLabel', t( 'Editor toolbar' ) );
/**
* The maximum width of the toolbar element.
*
* **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the
* {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}
* option by changing the number of {@link #items} that will be displayed in the toolbar at a time.
*
* @observable
* @default 'auto'
* @member {String} #maxWidth
*/
this.set( 'maxWidth', 'auto' );
/**
* A collection of toolbar items (buttons, dropdowns, etc.).
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.items = this.createCollection();
/**
* Tracks information about the DOM focus in the toolbar.
*
* @readonly
* @member {module:utils/focustracker~FocusTracker}
*/
this.focusTracker = new FocusTracker();
/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
* to handle keyboard navigation in the toolbar.
*
* @readonly
* @member {module:utils/keystrokehandler~KeystrokeHandler}
*/
this.keystrokes = new KeystrokeHandler();
/**
* An additional CSS class added to the {@link #element}.
*
* @observable
* @member {String} #class
*/
this.set( 'class' );
/**
* When set true, makes the toolbar look compact with {@link #element}.
*
* @observable
* @default false
* @member {String} #isCompact
*/
this.set( 'isCompact', false );
/**
* A (child) view containing {@link #items toolbar items}.
*
* @readonly
* @member {module:ui/toolbar/toolbarview~ItemsView}
*/
this.itemsView = new ItemsView( locale );
/**
* A top–level collection aggregating building blocks of the toolbar.
*
* ┌───────────────── ToolbarView ─────────────────┐
* | ┌──────────────── #children ────────────────┐ |
* | | ┌──────────── #itemsView ───────────┐ | |
* | | | [ item1 ] [ item2 ] ... [ itemN ] | | |
* | | └──────────────────────────────────-┘ | |
* | └───────────────────────────────────────────┘ |
* └───────────────────────────────────────────────┘
*
* By default, it contains the {@link #itemsView} but it can be extended with additional
* UI elements when necessary.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.children = this.createCollection();
this.children.add( this.itemsView );
/**
* A collection of {@link #items} that take part in the focus cycling
* (i.e. navigation using the keyboard). Usually, it contains a subset of {@link #items} with
* some optional UI elements that also belong to the toolbar and should be focusable
* by the user.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.focusables = this.createCollection();
/**
* Controls the orientation of toolbar items. Only available when
* {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull dynamic items grouping}
* is **disabled**.
*
* @observable
* @member {Boolean} #isVertical
*/
/**
* Helps cycling over {@link #focusables focusable items} in the toolbar.
*
* @readonly
* @protected
* @member {module:ui/focuscycler~FocusCycler}
*/
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' )
}
},
children: this.children,
on: {
// https://github.com/ckeditor/ckeditor5-ui/issues/206
mousedown: preventDefault( this )
}
} );
/**
* An instance of the active toolbar behavior that shapes its look and functionality.
*
* See {@link module:ui/toolbar/toolbarview~ToolbarBehavior} to learn more.
*
* @protected
* @readonly
* @member {module:ui/toolbar/toolbarview~ToolbarBehavior}
*/
this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping( this ) : new StaticLayout( this );
}
/**
* @inheritDoc
*/
render() {
super.render();
// 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 {Array.<String>|Object} itemsOrConfig The toolbar items or the entire toolbar configuration object.
* @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
*/
fillFromConfig( itemsOrConfig, factory ) {
const config = normalizeToolbarConfig( itemsOrConfig );
const itemsToClean = config.items
.filter( ( name, idx, items ) => {
if ( name === '|' ) {
return true;
}
// Items listed in `config.removeItems` should not be added to the toolbar.
if ( config.removeItems.indexOf( name ) !== -1 ) {
return false;
}
if ( name === '-' ) {
// 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:
*
* 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 ( !factory.has( name ) ) {
/**
* 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 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:
*
* Array.from( editor.ui.componentFactory.names() );
*
* @error toolbarview-item-unavailable
* @param {String} name The name of the component.
*/
logWarning( 'toolbarview-item-unavailable', { name } );
return false;
}
return true;
} );
const itemsToAdd = this._cleanSeparators( itemsToClean )
// Instantiate toolbar items.
.map( name => {
if ( name === '|' ) {
return new ToolbarSeparatorView();
} else if ( name === '-' ) {
return new ToolbarLineBreakView();
}
return factory.create( name );
} );
this.items.addMany( itemsToAdd );
}
/**
* Remove leading, trailing, and duplicated separators (`-` and `|`).
*
* @private
* @param {Array.<String>} items
*/
_cleanSeparators( items ) {
const nonSeparatorPredicate = item => ( item !== '-' && item !== '|' );
const count = items.length;
// Find an index of the first item that is not a separator.
const firstCommandItem = items.findIndex( nonSeparatorPredicate );
// Search from the end of the list, then convert found index back to the original direction.
const lastCommandItem = count - items
.slice()
.reverse()
.findIndex( nonSeparatorPredicate );
return items
// Return items without the leading and trailing separators.
.slice( firstCommandItem, lastCommandItem )
// 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;
} );
}
/**
* Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change
* in the toolbar geometry.
*
* **Note**: This event is always fired **once** regardless of the number of items that were be
* grouped or ungrouped at a time.
*
* **Note**: This event is fired only if the items grouping functionality was enabled in
* the first place (see {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}).
*
* @event groupedItemsUpdate
*/
}
/**
* An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
* {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
*
* @private
* @extends module:ui/view~View
*/
class ItemsView extends View {
/**
* @inheritDoc
*/
constructor( locale ) {
super( locale );
/**
* A collection of items (buttons, dropdowns, etc.).
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
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.
*
* @private
* @implements module:ui/toolbar/toolbarview~ToolbarBehavior
*/
class StaticLayout {
/**
* Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
* behavior.
*
* @param {module:ui/toolbar/toolbarview~ToolbarView} 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 => item );
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 --------/ | |
* | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
* └───────────────────────────────────────────────────────────────────────────────────────────────┘
*
* @private
* @implements module:ui/toolbar/toolbarview~ToolbarBehavior
*/
class DynamicGrouping {
/**
* Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
* behavior.
*
* @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
* is added to.
*/
constructor( view ) {
/**
* A toolbar view this behavior belongs to.
*
* @readonly
* @member {module:ui/toolbar~ToolbarView}
*/
this.view = view;
/**
* A collection of toolbar children.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.viewChildren = view.children;
/**
* A collection of focusable toolbar elements.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.viewFocusables = view.focusables;
/**
* A view containing toolbar items.
*
* @readonly
* @member {module:ui/toolbar/toolbarview~ItemsView}
*/
this.viewItemsView = view.itemsView;
/**
* Toolbar focus tracker.
*
* @readonly
* @member {module:utils/focustracker~FocusTracker}
*/
this.viewFocusTracker = view.focusTracker;
/**
* Toolbar locale.
*
* @readonly
* @member {module:utils/locale~Locale}
*/
this.viewLocale = view.locale;
/**
* Toolbar element.
*
* @readonly
* @member {HTMLElement} #viewElement
*/
/**
* A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
* Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
* into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
* {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
*
* When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
* matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.ungroupedItems = view.createCollection();
/**
* A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
* A collection of the toolbar items that do not fit into a single row of the toolbar.
* Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
*
* When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
* this collection is empty.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.groupedItems = view.createCollection();
/**
* The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
* row of the toolbar. It is displayed on demand as the last of
* {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
* (nested) toolbar which displays items that would normally overflow.
*
* @readonly
* @member {module:ui/dropdown/dropdownview~DropdownView}
*/
this.groupedItemsDropdown = this._createGroupedItemsDropdown();
/**
* 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
* @member {module:utils/dom/resizeobserver~ResizeObserver}
*/
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
* @member {Number}
*/
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
* @member {Boolean}
*/
this.shouldUpdateGroupingOnNextResize = false;
// 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 "focuscycleable".
this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
// Make sure the #groupedItemsDropdown is also included in cycling when it appears.
view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
view.children.on( 'remove', this._updateFocusCycleableItems.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;
// 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 + changeData.added.length; currentIndex++ ) {
const addedItem = changeData.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 {module:ui/toolbar/toolbarview~ToolbarView} 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.
*
* @protected
*/
_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.
*
* @private
* @type {Boolean}
*/
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.
*
* @private
*/
_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.
*
* @private
*/
_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}.
*
* @private
*/
_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}.
*
* @private
*/
_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.
*
* @private
* @returns {module:ui/dropdown/dropdownview~DropdownView}
*/
_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, [] );
dropdown.buttonView.set( {
label: t( 'Show more items' ),
tooltip: true,
tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
icon: threeVerticalDots
} );
// 1:1 pass–through binding.
dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
return dropdown;
}
/**
* Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable 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.
*
* @private
*/
_updateFocusCycleableItems() {
this.viewFocusables.clear();
this.ungroupedItems.map( item => {
this.viewFocusables.add( item );
} );
if ( this.groupedItems.length ) {
this.viewFocusables.add( this.groupedItemsDropdown );
}
}
}
/**
* Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
*
* @interface module:ui/toolbar/toolbarview~ToolbarOptions
*/
/**
* When set to `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
* would normally wrap to the next line when there is not enough space to display them in a single row, for
* instance, if the parent container of the toolbar is narrow. For toolbars in absolutely positioned containers
* without width restrictions also the {@link module:ui/toolbar/toolbarview~ToolbarOptions#isFloating} option is required to be `true`.
*
* See also: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}.
*
* @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
*/
/**
* This option should be enabled for toolbars in absolutely positioned containers without width restrictions
* to enable automatic {@link module:ui/toolbar/toolbarview~ToolbarView#items} grouping.
* When this option is set to `true`, the items will stop wrapping to the next line
* and together with {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull},
* this will allow grouping them when there is not enough space in a single row.
*
* @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#isFloating
*/
/**
* A class interface defining the behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
*
* Toolbar behaviors extend its look and functionality and have an impact on the
* {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
* {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
* conditionally, e.g. depending on the configuration of the toolbar.
*
* @private
* @interface module:ui/toolbar/toolbarview~ToolbarBehavior
*/
/**
* Creates a new toolbar behavior instance.
*
* The instance is created in the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
* This is the right place to extend the {@link module:ui/toolbar/toolbarview~ToolbarView#template} of
* the toolbar, define extra toolbar properties, etc.
*
* @method #constructor
* @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior is added to.
*/
/**
* A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
* It can be used to, for example, customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
* is available.
*
* @readonly
* @member {Function} #render
* @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
*/
/**
* A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
* It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach
* event listeners, free up references, etc.
*
* @readonly
* @member {Function} #destroy
*/