@ckeditor/ckeditor5-ui 35.2.0 → 35.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +31 -23
  2. package/src/bindings/addkeyboardhandlingforgrid.js +45 -57
  3. package/src/bindings/clickoutsidehandler.js +15 -21
  4. package/src/bindings/injectcsstransitiondisabler.js +16 -20
  5. package/src/bindings/preventdefault.js +6 -8
  6. package/src/bindings/submithandler.js +5 -7
  7. package/src/button/button.js +5 -0
  8. package/src/button/buttonview.js +220 -259
  9. package/src/button/switchbuttonview.js +56 -71
  10. package/src/colorgrid/colorgridview.js +135 -197
  11. package/src/colorgrid/colortileview.js +37 -47
  12. package/src/colorgrid/utils.js +57 -66
  13. package/src/componentfactory.js +79 -93
  14. package/src/dropdown/button/dropdownbutton.js +5 -0
  15. package/src/dropdown/button/dropdownbuttonview.js +44 -57
  16. package/src/dropdown/button/splitbuttonview.js +159 -207
  17. package/src/dropdown/dropdownpanelfocusable.js +5 -0
  18. package/src/dropdown/dropdownpanelview.js +101 -112
  19. package/src/dropdown/dropdownview.js +396 -438
  20. package/src/dropdown/utils.js +164 -213
  21. package/src/editableui/editableuiview.js +125 -141
  22. package/src/editableui/inline/inlineeditableuiview.js +44 -54
  23. package/src/editorui/bodycollection.js +61 -75
  24. package/src/editorui/boxed/boxededitoruiview.js +91 -104
  25. package/src/editorui/editoruiview.js +30 -39
  26. package/src/focuscycler.js +214 -245
  27. package/src/formheader/formheaderview.js +58 -70
  28. package/src/icon/iconview.js +145 -111
  29. package/src/iframe/iframeview.js +37 -49
  30. package/src/index.js +0 -17
  31. package/src/input/inputview.js +170 -198
  32. package/src/inputnumber/inputnumberview.js +48 -56
  33. package/src/inputtext/inputtextview.js +14 -18
  34. package/src/label/labelview.js +44 -53
  35. package/src/labeledfield/labeledfieldview.js +212 -235
  36. package/src/labeledfield/utils.js +39 -57
  37. package/src/labeledinput/labeledinputview.js +190 -221
  38. package/src/list/listitemview.js +40 -50
  39. package/src/list/listseparatorview.js +15 -19
  40. package/src/list/listview.js +94 -115
  41. package/src/model.js +19 -25
  42. package/src/notification/notification.js +151 -202
  43. package/src/panel/balloon/balloonpanelview.js +535 -628
  44. package/src/panel/balloon/contextualballoon.js +611 -732
  45. package/src/panel/sticky/stickypanelview.js +238 -270
  46. package/src/template.js +1049 -1479
  47. package/src/toolbar/balloon/balloontoolbar.js +337 -424
  48. package/src/toolbar/block/blockbuttonview.js +32 -42
  49. package/src/toolbar/block/blocktoolbar.js +375 -477
  50. package/src/toolbar/normalizetoolbarconfig.js +17 -21
  51. package/src/toolbar/toolbarlinebreakview.js +15 -19
  52. package/src/toolbar/toolbarseparatorview.js +15 -19
  53. package/src/toolbar/toolbarview.js +866 -1053
  54. package/src/tooltipmanager.js +324 -353
  55. package/src/view.js +389 -430
  56. package/src/viewcollection.js +147 -178
  57. package/src/button/button.jsdoc +0 -165
  58. package/src/dropdown/button/dropdownbutton.jsdoc +0 -22
  59. package/src/dropdown/dropdownpanelfocusable.jsdoc +0 -27
@@ -2,11 +2,9 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module ui/toolbar/toolbarview
8
7
  */
9
-
10
8
  import View from '../view';
11
9
  import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
12
10
  import FocusCycler from '../focuscycler';
@@ -14,7 +12,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
14
12
  import ToolbarSeparatorView from './toolbarseparatorview';
15
13
  import ToolbarLineBreakView from './toolbarlinebreakview';
16
14
  import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
17
- import preventDefault from '../bindings/preventdefault.js';
15
+ import preventDefault from '../bindings/preventdefault';
18
16
  import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
19
17
  import isVisible from '@ckeditor/ckeditor5-utils/src/dom/isvisible';
20
18
  import global from '@ckeditor/ckeditor5-utils/src/dom/global';
@@ -22,23 +20,18 @@ import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
22
20
  import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
23
21
  import normalizeToolbarConfig from './normalizetoolbarconfig';
24
22
  import { isObject } from 'lodash-es';
25
-
26
- import threeVerticalDots from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg';
27
-
28
23
  import '../../theme/components/toolbar/toolbar.css';
29
-
30
24
  import { icons } from '@ckeditor/ckeditor5-core';
31
-
25
+ const { threeVerticalDots } = icons;
32
26
  const NESTED_TOOLBAR_ICONS = {
33
- alignLeft: icons.alignLeft,
34
- bold: icons.bold,
35
- importExport: icons.importExport,
36
- paragraph: icons.paragraph,
37
- plus: icons.plus,
38
- text: icons.text,
39
- threeVerticalDots: icons.threeVerticalDots
27
+ alignLeft: icons.alignLeft,
28
+ bold: icons.bold,
29
+ importExport: icons.importExport,
30
+ paragraph: icons.paragraph,
31
+ plus: icons.plus,
32
+ text: icons.text,
33
+ threeVerticalDots: icons.threeVerticalDots
40
34
  };
41
-
42
35
  /**
43
36
  * The toolbar view class.
44
37
  *
@@ -46,490 +39,417 @@ const NESTED_TOOLBAR_ICONS = {
46
39
  * @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
47
40
  */
48
41
  export default class ToolbarView extends View {
49
- /**
50
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
51
- *
52
- * Also see {@link #render}.
53
- *
54
- * @param {module:utils/locale~Locale} locale The localization services instance.
55
- * @param {module:ui/toolbar/toolbarview~ToolbarOptions} [options] Configuration options of the toolbar.
56
- */
57
- constructor( locale, options ) {
58
- super( locale );
59
-
60
- const bind = this.bindTemplate;
61
- const t = this.t;
62
-
63
- /**
64
- * A reference to the options object passed to the constructor.
65
- *
66
- * @readonly
67
- * @member {module:ui/toolbar/toolbarview~ToolbarOptions}
68
- */
69
- this.options = options || {};
70
-
71
- /**
72
- * Label used by assistive technologies to describe this toolbar element.
73
- *
74
- * @default 'Editor toolbar'
75
- * @member {String} #ariaLabel
76
- */
77
- this.set( 'ariaLabel', t( 'Editor toolbar' ) );
78
-
79
- /**
80
- * The maximum width of the toolbar element.
81
- *
82
- * **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the
83
- * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}
84
- * option by changing the number of {@link #items} that will be displayed in the toolbar at a time.
85
- *
86
- * @observable
87
- * @default 'auto'
88
- * @member {String} #maxWidth
89
- */
90
- this.set( 'maxWidth', 'auto' );
91
-
92
- /**
93
- * A collection of toolbar items (buttons, dropdowns, etc.).
94
- *
95
- * @readonly
96
- * @member {module:ui/viewcollection~ViewCollection}
97
- */
98
- this.items = this.createCollection();
99
-
100
- /**
101
- * Tracks information about the DOM focus in the toolbar.
102
- *
103
- * @readonly
104
- * @member {module:utils/focustracker~FocusTracker}
105
- */
106
- this.focusTracker = new FocusTracker();
107
-
108
- /**
109
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
110
- * to handle keyboard navigation in the toolbar.
111
- *
112
- * @readonly
113
- * @member {module:utils/keystrokehandler~KeystrokeHandler}
114
- */
115
- this.keystrokes = new KeystrokeHandler();
116
-
117
- /**
118
- * An additional CSS class added to the {@link #element}.
119
- *
120
- * @observable
121
- * @member {String} #class
122
- */
123
- this.set( 'class' );
124
-
125
- /**
126
- * When set true, makes the toolbar look compact with {@link #element}.
127
- *
128
- * @observable
129
- * @default false
130
- * @member {String} #isCompact
131
- */
132
- this.set( 'isCompact', false );
133
-
134
- /**
135
- * A (child) view containing {@link #items toolbar items}.
136
- *
137
- * @readonly
138
- * @member {module:ui/toolbar/toolbarview~ItemsView}
139
- */
140
- this.itemsView = new ItemsView( locale );
141
-
142
- /**
143
- * A top–level collection aggregating building blocks of the toolbar.
144
- *
145
- * ┌───────────────── ToolbarView ─────────────────┐
146
- * | ┌──────────────── #children ────────────────┐ |
147
- * | | ┌──────────── #itemsView ───────────┐ | |
148
- * | | | [ item1 ] [ item2 ] ... [ itemN ] | | |
149
- * | | └──────────────────────────────────-┘ | |
150
- * | └───────────────────────────────────────────┘ |
151
- * └───────────────────────────────────────────────┘
152
- *
153
- * By default, it contains the {@link #itemsView} but it can be extended with additional
154
- * UI elements when necessary.
155
- *
156
- * @readonly
157
- * @member {module:ui/viewcollection~ViewCollection}
158
- */
159
- this.children = this.createCollection();
160
- this.children.add( this.itemsView );
161
-
162
- /**
163
- * A collection of {@link #items} that take part in the focus cycling
164
- * (i.e. navigation using the keyboard). Usually, it contains a subset of {@link #items} with
165
- * some optional UI elements that also belong to the toolbar and should be focusable
166
- * by the user.
167
- *
168
- * @readonly
169
- * @member {module:ui/viewcollection~ViewCollection}
170
- */
171
- this.focusables = this.createCollection();
172
-
173
- /**
174
- * Controls the orientation of toolbar items. Only available when
175
- * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull dynamic items grouping}
176
- * is **disabled**.
177
- *
178
- * @observable
179
- * @member {Boolean} #isVertical
180
- */
181
-
182
- /**
183
- * Helps cycling over {@link #focusables focusable items} in the toolbar.
184
- *
185
- * @readonly
186
- * @protected
187
- * @member {module:ui/focuscycler~FocusCycler}
188
- */
189
-
190
- const isRtl = locale.uiLanguageDirection === 'rtl';
191
-
192
- this._focusCycler = new FocusCycler( {
193
- focusables: this.focusables,
194
- focusTracker: this.focusTracker,
195
- keystrokeHandler: this.keystrokes,
196
- actions: {
197
- // Navigate toolbar items backwards using the arrow[left,up] keys.
198
- focusPrevious: [ isRtl ? 'arrowright' : 'arrowleft', 'arrowup' ],
199
-
200
- // Navigate toolbar items forwards using the arrow[right,down] keys.
201
- focusNext: [ isRtl ? 'arrowleft' : 'arrowright', 'arrowdown' ]
202
- }
203
- } );
204
-
205
- const classes = [
206
- 'ck',
207
- 'ck-toolbar',
208
- bind.to( 'class' ),
209
- bind.if( 'isCompact', 'ck-toolbar_compact' )
210
- ];
211
-
212
- if ( this.options.shouldGroupWhenFull && this.options.isFloating ) {
213
- classes.push( 'ck-toolbar_floating' );
214
- }
215
-
216
- this.setTemplate( {
217
- tag: 'div',
218
- attributes: {
219
- class: classes,
220
- role: 'toolbar',
221
- 'aria-label': bind.to( 'ariaLabel' ),
222
- style: {
223
- maxWidth: bind.to( 'maxWidth' )
224
- }
225
- },
226
-
227
- children: this.children,
228
-
229
- on: {
230
- // https://github.com/ckeditor/ckeditor5-ui/issues/206
231
- mousedown: preventDefault( this )
232
- }
233
- } );
234
-
235
- /**
236
- * An instance of the active toolbar behavior that shapes its look and functionality.
237
- *
238
- * See {@link module:ui/toolbar/toolbarview~ToolbarBehavior} to learn more.
239
- *
240
- * @protected
241
- * @readonly
242
- * @member {module:ui/toolbar/toolbarview~ToolbarBehavior}
243
- */
244
- this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping( this ) : new StaticLayout( this );
245
- }
246
-
247
- /**
248
- * @inheritDoc
249
- */
250
- render() {
251
- super.render();
252
-
253
- // Children added before rendering should be known to the #focusTracker.
254
- for ( const item of this.items ) {
255
- this.focusTracker.add( item.element );
256
- }
257
-
258
- this.items.on( 'add', ( evt, item ) => {
259
- this.focusTracker.add( item.element );
260
- } );
261
-
262
- this.items.on( 'remove', ( evt, item ) => {
263
- this.focusTracker.remove( item.element );
264
- } );
265
-
266
- // Start listening for the keystrokes coming from #element.
267
- this.keystrokes.listenTo( this.element );
268
-
269
- this._behavior.render( this );
270
- }
271
-
272
- /**
273
- * @inheritDoc
274
- */
275
- destroy() {
276
- this._behavior.destroy();
277
- this.focusTracker.destroy();
278
- this.keystrokes.destroy();
279
-
280
- return super.destroy();
281
- }
282
-
283
- /**
284
- * Focuses the first focusable in {@link #focusables}.
285
- */
286
- focus() {
287
- this._focusCycler.focusFirst();
288
- }
289
-
290
- /**
291
- * Focuses the last focusable in {@link #focusables}.
292
- */
293
- focusLast() {
294
- this._focusCycler.focusLast();
295
- }
296
-
297
- /**
298
- * A utility that expands the plain toolbar configuration into
299
- * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
300
- *
301
- * @param {Array.<String>|Object} itemsOrConfig The toolbar items or the entire toolbar configuration object.
302
- * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
303
- * @param {Array.<String>} [removeItems] An array of items names to be removed from the configuration. When present, applies
304
- * to this toolbar and all nested ones as well.
305
- */
306
- fillFromConfig( itemsOrConfig, factory, removeItems ) {
307
- const config = normalizeToolbarConfig( itemsOrConfig );
308
- const normalizedRemoveItems = removeItems || config.removeItems;
309
- const itemsToAdd = this._cleanItemsConfiguration( config.items, factory, normalizedRemoveItems )
310
- .map( name => {
311
- if ( isObject( name ) ) {
312
- return this._createNestedToolbarDropdown( name, factory, normalizedRemoveItems );
313
- } else if ( name === '|' ) {
314
- return new ToolbarSeparatorView();
315
- } else if ( name === '-' ) {
316
- return new ToolbarLineBreakView();
317
- }
318
-
319
- return factory.create( name );
320
- } )
321
- .filter( item => item );
322
-
323
- this.items.addMany( itemsToAdd );
324
- }
325
-
326
- /**
327
- * Cleans up the {@link module:ui/toolbar/toolbarview~ToolbarView#items} of the toolbar by removing unwanted items and
328
- * duplicated (obsolete) separators or line breaks.
329
- *
330
- * @private
331
- * @param {Array.<String>} items The toolbar items configuration.
332
- * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
333
- * @param {Array.<String>} removeItems An array of items names to be removed from the configuration.
334
- * @returns {Array.<String>} Items after the clean-up.
335
- */
336
- _cleanItemsConfiguration( items, factory, removeItems ) {
337
- const filteredItems = items
338
- .filter( ( name, idx, items ) => {
339
- if ( name === '|' ) {
340
- return true;
341
- }
342
-
343
- // Items listed in `config.removeItems` should not be added to the toolbar.
344
- if ( removeItems.indexOf( name ) !== -1 ) {
345
- return false;
346
- }
347
-
348
- if ( name === '-' ) {
349
- // The toolbar line breaks must not be rendered when toolbar grouping is enabled.
350
- // (https://github.com/ckeditor/ckeditor5/issues/8582)
351
- if ( this.options.shouldGroupWhenFull ) {
352
- /**
353
- * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
354
- * is disabled in the toolbar configuration.
355
- * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
356
- *
357
- * const config = {
358
- * toolbar: {
359
- * items: [ ... ],
360
- * shouldNotGroupWhenFull: true
361
- * }
362
- * }
363
- *
364
- * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
365
- *
366
- * @error toolbarview-line-break-ignored-when-grouping-items
367
- */
368
- logWarning( 'toolbarview-line-break-ignored-when-grouping-items', items );
369
-
370
- return false;
371
- }
372
-
373
- return true;
374
- }
375
-
376
- // For the items that cannot be instantiated we are sending warning message. We also filter them out.
377
- if ( !isObject( name ) && !factory.has( name ) ) {
378
- /**
379
- * There was a problem processing the configuration of the toolbar. The item with the given
380
- * name does not exist so it was omitted when rendering the toolbar.
381
- *
382
- * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
383
- * to provide a toolbar item has not been loaded or there is a typo in the configuration.
384
- *
385
- * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
386
- * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
387
- *
388
- * You can use the following snippet to retrieve all available toolbar items:
389
- *
390
- * Array.from( editor.ui.componentFactory.names() );
391
- *
392
- * @error toolbarview-item-unavailable
393
- * @param {String} name The name of the component.
394
- */
395
- logWarning( 'toolbarview-item-unavailable', { name } );
396
-
397
- return false;
398
- }
399
-
400
- return true;
401
- } );
402
-
403
- return this._cleanSeparatorsAndLineBreaks( filteredItems );
404
- }
405
-
406
- /**
407
- * Remove leading, trailing, and duplicated separators (`-` and `|`).
408
- *
409
- * @private
410
- * @param {Array.<String>} items
411
- * @returns {Array.<String>} Toolbar items after the separator and line break clean-up.
412
- */
413
- _cleanSeparatorsAndLineBreaks( items ) {
414
- const nonSeparatorPredicate = item => ( item !== '-' && item !== '|' );
415
- const count = items.length;
416
-
417
- // Find an index of the first item that is not a separator.
418
- const firstCommandItemIndex = items.findIndex( nonSeparatorPredicate );
419
-
420
- // Items include separators only. There is no point in displaying them.
421
- if ( firstCommandItemIndex === -1 ) {
422
- return [];
423
- }
424
-
425
- // Search from the end of the list, then convert found index back to the original direction.
426
- const lastCommandItemIndex = count - items
427
- .slice()
428
- .reverse()
429
- .findIndex( nonSeparatorPredicate );
430
-
431
- return items
432
- // Return items without the leading and trailing separators.
433
- .slice( firstCommandItemIndex, lastCommandItemIndex )
434
- // Remove duplicated separators.
435
- .filter( ( name, idx, items ) => {
436
- // Filter only separators.
437
- if ( nonSeparatorPredicate( name ) ) {
438
- return true;
439
- }
440
- const isDuplicated = idx > 0 && items[ idx - 1 ] === name;
441
-
442
- return !isDuplicated;
443
- } );
444
- }
445
-
446
- /**
447
- * Creates a user-defined dropdown containing a toolbar with items.
448
- *
449
- * @private
450
- * @param {Object} definition A definition of the nested toolbar dropdown.
451
- * @param {String} definition.label A label of the dropdown.
452
- * @param {String|Boolean} [definition.icon] An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft',
453
- * 'paragraph' or an SVG string. When `false` is passed, no icon will be used.
454
- * @param {Boolean} [definition.withText=false] When set `true`, the label of the dropdown will be visible. See
455
- * {@link module:ui/button/buttonview~ButtonView#withText} to learn more.
456
- * @param {Boolean|String|Function} [definition.tooltip=true] A tooltip of the dropdown button. See
457
- * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more.
458
- * @param {module:ui/componentfactory~ComponentFactory} componentFactory Component factory used to create items
459
- * of the nested toolbar.
460
- * @returns {module:ui/dropdown/dropdownview~DropdownView}
461
- */
462
- _createNestedToolbarDropdown( definition, componentFactory, removeItems ) {
463
- let { label, icon, items, tooltip = true, withText = false } = definition;
464
-
465
- items = this._cleanItemsConfiguration( items, componentFactory, removeItems );
466
-
467
- // There is no point in rendering a dropdown without items.
468
- if ( !items.length ) {
469
- return null;
470
- }
471
-
472
- const locale = this.locale;
473
- const dropdownView = createDropdown( locale );
474
-
475
- if ( !label ) {
476
- /**
477
- * A dropdown definition in the toolbar configuration is missing a text label.
478
- *
479
- * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies.
480
- * Make sure the `label` property is set in your drop-down configuration:
481
- *
482
- * {
483
- * label: 'A human-readable label',
484
- * icon: '...',
485
- * items: [ ... ]
486
- * },
487
- *
488
- * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
489
- *
490
- * @error toolbarview-nested-toolbar-dropdown-missing-label
491
- */
492
- logWarning( 'toolbarview-nested-toolbar-dropdown-missing-label', definition );
493
- }
494
-
495
- dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown';
496
- dropdownView.buttonView.set( {
497
- label,
498
- tooltip,
499
- withText: !!withText
500
- } );
501
-
502
- // Allow disabling icon by passing false.
503
- if ( icon !== false ) {
504
- // A pre-defined icon picked by name, SVG string, a fallback (default) icon.
505
- dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon ] || icon || NESTED_TOOLBAR_ICONS.threeVerticalDots;
506
- }
507
- // If the icon is disabled, display the label automatically.
508
- else {
509
- dropdownView.buttonView.withText = true;
510
- }
511
-
512
- addToolbarToDropdown( dropdownView, [] );
513
-
514
- dropdownView.toolbarView.fillFromConfig( items, componentFactory, removeItems );
515
-
516
- return dropdownView;
517
- }
518
-
519
- /**
520
- * Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change
521
- * in the toolbar geometry.
522
- *
523
- * **Note**: This event is always fired **once** regardless of the number of items that were be
524
- * grouped or ungrouped at a time.
525
- *
526
- * **Note**: This event is fired only if the items grouping functionality was enabled in
527
- * the first place (see {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}).
528
- *
529
- * @event groupedItemsUpdate
530
- */
42
+ /**
43
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
44
+ *
45
+ * Also see {@link #render}.
46
+ *
47
+ * @param {module:utils/locale~Locale} locale The localization services instance.
48
+ * @param {module:ui/toolbar/toolbarview~ToolbarOptions} [options] Configuration options of the toolbar.
49
+ */
50
+ constructor(locale, options) {
51
+ super(locale);
52
+ const bind = this.bindTemplate;
53
+ const t = this.t;
54
+ /**
55
+ * A reference to the options object passed to the constructor.
56
+ *
57
+ * @readonly
58
+ * @member {module:ui/toolbar/toolbarview~ToolbarOptions}
59
+ */
60
+ this.options = options || {};
61
+ /**
62
+ * Label used by assistive technologies to describe this toolbar element.
63
+ *
64
+ * @default 'Editor toolbar'
65
+ * @member {String} #ariaLabel
66
+ */
67
+ this.set('ariaLabel', t('Editor toolbar'));
68
+ /**
69
+ * The maximum width of the toolbar element.
70
+ *
71
+ * **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the
72
+ * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}
73
+ * option by changing the number of {@link #items} that will be displayed in the toolbar at a time.
74
+ *
75
+ * @observable
76
+ * @default 'auto'
77
+ * @member {String} #maxWidth
78
+ */
79
+ this.set('maxWidth', 'auto');
80
+ /**
81
+ * A collection of toolbar items (buttons, dropdowns, etc.).
82
+ *
83
+ * @readonly
84
+ * @member {module:ui/viewcollection~ViewCollection}
85
+ */
86
+ this.items = this.createCollection();
87
+ /**
88
+ * Tracks information about the DOM focus in the toolbar.
89
+ *
90
+ * @readonly
91
+ * @member {module:utils/focustracker~FocusTracker}
92
+ */
93
+ this.focusTracker = new FocusTracker();
94
+ /**
95
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
96
+ * to handle keyboard navigation in the toolbar.
97
+ *
98
+ * @readonly
99
+ * @member {module:utils/keystrokehandler~KeystrokeHandler}
100
+ */
101
+ this.keystrokes = new KeystrokeHandler();
102
+ /**
103
+ * An additional CSS class added to the {@link #element}.
104
+ *
105
+ * @observable
106
+ * @member {String} #class
107
+ */
108
+ this.set('class', undefined);
109
+ /**
110
+ * When set true, makes the toolbar look compact with {@link #element}.
111
+ *
112
+ * @observable
113
+ * @default false
114
+ * @member {String} #isCompact
115
+ */
116
+ this.set('isCompact', false);
117
+ /**
118
+ * A (child) view containing {@link #items toolbar items}.
119
+ *
120
+ * @readonly
121
+ * @member {module:ui/toolbar/toolbarview~ItemsView}
122
+ */
123
+ this.itemsView = new ItemsView(locale);
124
+ /**
125
+ * A top–level collection aggregating building blocks of the toolbar.
126
+ *
127
+ * ┌───────────────── ToolbarView ─────────────────┐
128
+ * | ┌──────────────── #children ────────────────┐ |
129
+ * | | ┌──────────── #itemsView ───────────┐ | |
130
+ * | | | [ item1 ] [ item2 ] ... [ itemN ] | | |
131
+ * | | └──────────────────────────────────-┘ | |
132
+ * | └───────────────────────────────────────────┘ |
133
+ * └───────────────────────────────────────────────┘
134
+ *
135
+ * By default, it contains the {@link #itemsView} but it can be extended with additional
136
+ * UI elements when necessary.
137
+ *
138
+ * @readonly
139
+ * @member {module:ui/viewcollection~ViewCollection}
140
+ */
141
+ this.children = this.createCollection();
142
+ this.children.add(this.itemsView);
143
+ /**
144
+ * A collection of {@link #items} that take part in the focus cycling
145
+ * (i.e. navigation using the keyboard). Usually, it contains a subset of {@link #items} with
146
+ * some optional UI elements that also belong to the toolbar and should be focusable
147
+ * by the user.
148
+ *
149
+ * @readonly
150
+ * @member {module:ui/viewcollection~ViewCollection}
151
+ */
152
+ this.focusables = this.createCollection();
153
+ /**
154
+ * Controls the orientation of toolbar items. Only available when
155
+ * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull dynamic items grouping}
156
+ * is **disabled**.
157
+ *
158
+ * @observable
159
+ * @member {Boolean} #isVertical
160
+ */
161
+ /**
162
+ * Helps cycling over {@link #focusables focusable items} in the toolbar.
163
+ *
164
+ * @readonly
165
+ * @protected
166
+ * @member {module:ui/focuscycler~FocusCycler}
167
+ */
168
+ const isRtl = locale.uiLanguageDirection === 'rtl';
169
+ this._focusCycler = new FocusCycler({
170
+ focusables: this.focusables,
171
+ focusTracker: this.focusTracker,
172
+ keystrokeHandler: this.keystrokes,
173
+ actions: {
174
+ // Navigate toolbar items backwards using the arrow[left,up] keys.
175
+ focusPrevious: [isRtl ? 'arrowright' : 'arrowleft', 'arrowup'],
176
+ // Navigate toolbar items forwards using the arrow[right,down] keys.
177
+ focusNext: [isRtl ? 'arrowleft' : 'arrowright', 'arrowdown']
178
+ }
179
+ });
180
+ const classes = [
181
+ 'ck',
182
+ 'ck-toolbar',
183
+ bind.to('class'),
184
+ bind.if('isCompact', 'ck-toolbar_compact')
185
+ ];
186
+ if (this.options.shouldGroupWhenFull && this.options.isFloating) {
187
+ classes.push('ck-toolbar_floating');
188
+ }
189
+ this.setTemplate({
190
+ tag: 'div',
191
+ attributes: {
192
+ class: classes,
193
+ role: 'toolbar',
194
+ 'aria-label': bind.to('ariaLabel'),
195
+ style: {
196
+ maxWidth: bind.to('maxWidth')
197
+ }
198
+ },
199
+ children: this.children,
200
+ on: {
201
+ // https://github.com/ckeditor/ckeditor5-ui/issues/206
202
+ mousedown: preventDefault(this)
203
+ }
204
+ });
205
+ /**
206
+ * An instance of the active toolbar behavior that shapes its look and functionality.
207
+ *
208
+ * See {@link module:ui/toolbar/toolbarview~ToolbarBehavior} to learn more.
209
+ *
210
+ * @protected
211
+ * @readonly
212
+ * @member {module:ui/toolbar/toolbarview~ToolbarBehavior}
213
+ */
214
+ this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping(this) : new StaticLayout(this);
215
+ }
216
+ /**
217
+ * @inheritDoc
218
+ */
219
+ render() {
220
+ super.render();
221
+ // Children added before rendering should be known to the #focusTracker.
222
+ for (const item of this.items) {
223
+ this.focusTracker.add(item.element);
224
+ }
225
+ this.items.on('add', (evt, item) => {
226
+ this.focusTracker.add(item.element);
227
+ });
228
+ this.items.on('remove', (evt, item) => {
229
+ this.focusTracker.remove(item.element);
230
+ });
231
+ // Start listening for the keystrokes coming from #element.
232
+ this.keystrokes.listenTo(this.element);
233
+ this._behavior.render(this);
234
+ }
235
+ /**
236
+ * @inheritDoc
237
+ */
238
+ destroy() {
239
+ this._behavior.destroy();
240
+ this.focusTracker.destroy();
241
+ this.keystrokes.destroy();
242
+ return super.destroy();
243
+ }
244
+ /**
245
+ * Focuses the first focusable in {@link #focusables}.
246
+ */
247
+ focus() {
248
+ this._focusCycler.focusFirst();
249
+ }
250
+ /**
251
+ * Focuses the last focusable in {@link #focusables}.
252
+ */
253
+ focusLast() {
254
+ this._focusCycler.focusLast();
255
+ }
256
+ /**
257
+ * A utility that expands the plain toolbar configuration into
258
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
259
+ *
260
+ * @param {Array.<String>|Object} itemsOrConfig The toolbar items or the entire toolbar configuration object.
261
+ * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
262
+ * @param {Array.<String>} [removeItems] An array of items names to be removed from the configuration. When present, applies
263
+ * to this toolbar and all nested ones as well.
264
+ */
265
+ fillFromConfig(itemsOrConfig, factory, removeItems) {
266
+ const config = normalizeToolbarConfig(itemsOrConfig);
267
+ const normalizedRemoveItems = removeItems || config.removeItems;
268
+ const itemsToAdd = this._cleanItemsConfiguration(config.items, factory, normalizedRemoveItems)
269
+ .map(item => {
270
+ if (isObject(item)) {
271
+ return this._createNestedToolbarDropdown(item, factory, normalizedRemoveItems);
272
+ }
273
+ else if (item === '|') {
274
+ return new ToolbarSeparatorView();
275
+ }
276
+ else if (item === '-') {
277
+ return new ToolbarLineBreakView();
278
+ }
279
+ return factory.create(item);
280
+ })
281
+ .filter((item) => !!item);
282
+ this.items.addMany(itemsToAdd);
283
+ }
284
+ /**
285
+ * Cleans up the {@link module:ui/toolbar/toolbarview~ToolbarView#items} of the toolbar by removing unwanted items and
286
+ * duplicated (obsolete) separators or line breaks.
287
+ *
288
+ * @private
289
+ * @param {Array.<String>} items The toolbar items configuration.
290
+ * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
291
+ * @param {Array.<String>} removeItems An array of items names to be removed from the configuration.
292
+ * @returns {Array.<String>} Items after the clean-up.
293
+ */
294
+ _cleanItemsConfiguration(items, factory, removeItems) {
295
+ const filteredItems = items
296
+ .filter((item, idx, items) => {
297
+ if (item === '|') {
298
+ return true;
299
+ }
300
+ // Items listed in `config.removeItems` should not be added to the toolbar.
301
+ if (removeItems.indexOf(item) !== -1) {
302
+ return false;
303
+ }
304
+ if (item === '-') {
305
+ // The toolbar line breaks must not be rendered when toolbar grouping is enabled.
306
+ // (https://github.com/ckeditor/ckeditor5/issues/8582)
307
+ if (this.options.shouldGroupWhenFull) {
308
+ /**
309
+ * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
310
+ * is disabled in the toolbar configuration.
311
+ * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
312
+ *
313
+ * const config = {
314
+ * toolbar: {
315
+ * items: [ ... ],
316
+ * shouldNotGroupWhenFull: true
317
+ * }
318
+ * }
319
+ *
320
+ * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
321
+ *
322
+ * @error toolbarview-line-break-ignored-when-grouping-items
323
+ */
324
+ logWarning('toolbarview-line-break-ignored-when-grouping-items', items);
325
+ return false;
326
+ }
327
+ return true;
328
+ }
329
+ // For the items that cannot be instantiated we are sending warning message. We also filter them out.
330
+ if (!isObject(item) && !factory.has(item)) {
331
+ /**
332
+ * There was a problem processing the configuration of the toolbar. The item with the given
333
+ * name does not exist so it was omitted when rendering the toolbar.
334
+ *
335
+ * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
336
+ * to provide a toolbar item has not been loaded or there is a typo in the configuration.
337
+ *
338
+ * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
339
+ * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
340
+ *
341
+ * You can use the following snippet to retrieve all available toolbar items:
342
+ *
343
+ * Array.from( editor.ui.componentFactory.names() );
344
+ *
345
+ * @error toolbarview-item-unavailable
346
+ * @param {String|Object} item The name of the component or nested toolbar definition.
347
+ */
348
+ logWarning('toolbarview-item-unavailable', { item });
349
+ return false;
350
+ }
351
+ return true;
352
+ });
353
+ return this._cleanSeparatorsAndLineBreaks(filteredItems);
354
+ }
355
+ /**
356
+ * Remove leading, trailing, and duplicated separators (`-` and `|`).
357
+ *
358
+ * @private
359
+ * @param {Array.<String>} items
360
+ * @returns {Array.<String>} Toolbar items after the separator and line break clean-up.
361
+ */
362
+ _cleanSeparatorsAndLineBreaks(items) {
363
+ const nonSeparatorPredicate = (item) => (item !== '-' && item !== '|');
364
+ const count = items.length;
365
+ // Find an index of the first item that is not a separator.
366
+ const firstCommandItemIndex = items.findIndex(nonSeparatorPredicate);
367
+ // Items include separators only. There is no point in displaying them.
368
+ if (firstCommandItemIndex === -1) {
369
+ return [];
370
+ }
371
+ // Search from the end of the list, then convert found index back to the original direction.
372
+ const lastCommandItemIndex = count - items
373
+ .slice()
374
+ .reverse()
375
+ .findIndex(nonSeparatorPredicate);
376
+ return items
377
+ // Return items without the leading and trailing separators.
378
+ .slice(firstCommandItemIndex, lastCommandItemIndex)
379
+ // Remove duplicated separators.
380
+ .filter((name, idx, items) => {
381
+ // Filter only separators.
382
+ if (nonSeparatorPredicate(name)) {
383
+ return true;
384
+ }
385
+ const isDuplicated = idx > 0 && items[idx - 1] === name;
386
+ return !isDuplicated;
387
+ });
388
+ }
389
+ /**
390
+ * Creates a user-defined dropdown containing a toolbar with items.
391
+ *
392
+ * @private
393
+ * @param {Object} definition A definition of the nested toolbar dropdown.
394
+ * @param {String} definition.label A label of the dropdown.
395
+ * @param {String|Boolean} [definition.icon] An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft',
396
+ * 'paragraph' or an SVG string. When `false` is passed, no icon will be used.
397
+ * @param {Boolean} [definition.withText=false] When set `true`, the label of the dropdown will be visible. See
398
+ * {@link module:ui/button/buttonview~ButtonView#withText} to learn more.
399
+ * @param {Boolean|String|Function} [definition.tooltip=true] A tooltip of the dropdown button. See
400
+ * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more.
401
+ * @param {module:ui/componentfactory~ComponentFactory} componentFactory Component factory used to create items
402
+ * of the nested toolbar.
403
+ * @returns {module:ui/dropdown/dropdownview~DropdownView}
404
+ */
405
+ _createNestedToolbarDropdown(definition, componentFactory, removeItems) {
406
+ let { label, icon, items, tooltip = true, withText = false } = definition;
407
+ items = this._cleanItemsConfiguration(items, componentFactory, removeItems);
408
+ // There is no point in rendering a dropdown without items.
409
+ if (!items.length) {
410
+ return null;
411
+ }
412
+ const locale = this.locale;
413
+ const dropdownView = createDropdown(locale);
414
+ if (!label) {
415
+ /**
416
+ * A dropdown definition in the toolbar configuration is missing a text label.
417
+ *
418
+ * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies.
419
+ * Make sure the `label` property is set in your drop-down configuration:
420
+ *
421
+ * {
422
+ * label: 'A human-readable label',
423
+ * icon: '...',
424
+ * items: [ ... ]
425
+ * },
426
+ *
427
+ * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
428
+ *
429
+ * @error toolbarview-nested-toolbar-dropdown-missing-label
430
+ */
431
+ logWarning('toolbarview-nested-toolbar-dropdown-missing-label', definition);
432
+ }
433
+ dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown';
434
+ dropdownView.buttonView.set({
435
+ label,
436
+ tooltip,
437
+ withText: !!withText
438
+ });
439
+ // Allow disabling icon by passing false.
440
+ if (icon !== false) {
441
+ // A pre-defined icon picked by name, SVG string, a fallback (default) icon.
442
+ dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[icon] || icon || threeVerticalDots;
443
+ }
444
+ // If the icon is disabled, display the label automatically.
445
+ else {
446
+ dropdownView.buttonView.withText = true;
447
+ }
448
+ addToolbarToDropdown(dropdownView, []);
449
+ dropdownView.toolbarView.fillFromConfig(items, componentFactory, removeItems);
450
+ return dropdownView;
451
+ }
531
452
  }
532
-
533
453
  /**
534
454
  * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
535
455
  * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
@@ -538,33 +458,30 @@ export default class ToolbarView extends View {
538
458
  * @extends module:ui/view~View
539
459
  */
540
460
  class ItemsView extends View {
541
- /**
542
- * @inheritDoc
543
- */
544
- constructor( locale ) {
545
- super( locale );
546
-
547
- /**
548
- * A collection of items (buttons, dropdowns, etc.).
549
- *
550
- * @readonly
551
- * @member {module:ui/viewcollection~ViewCollection}
552
- */
553
- this.children = this.createCollection();
554
-
555
- this.setTemplate( {
556
- tag: 'div',
557
- attributes: {
558
- class: [
559
- 'ck',
560
- 'ck-toolbar__items'
561
- ]
562
- },
563
- children: this.children
564
- } );
565
- }
461
+ /**
462
+ * @inheritDoc
463
+ */
464
+ constructor(locale) {
465
+ super(locale);
466
+ /**
467
+ * A collection of items (buttons, dropdowns, etc.).
468
+ *
469
+ * @readonly
470
+ * @member {module:ui/viewcollection~ViewCollection}
471
+ */
472
+ this.children = this.createCollection();
473
+ this.setTemplate({
474
+ tag: 'div',
475
+ attributes: {
476
+ class: [
477
+ 'ck',
478
+ 'ck-toolbar__items'
479
+ ]
480
+ },
481
+ children: this.children
482
+ });
483
+ }
566
484
  }
567
-
568
485
  /**
569
486
  * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
570
487
  * At the same time, it also makes it possible to display a toolbar with a vertical layout
@@ -574,46 +491,39 @@ class ItemsView extends View {
574
491
  * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
575
492
  */
576
493
  class StaticLayout {
577
- /**
578
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
579
- * behavior.
580
- *
581
- * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
582
- * is added to.
583
- */
584
- constructor( view ) {
585
- const bind = view.bindTemplate;
586
-
587
- // Static toolbar can be vertical when needed.
588
- view.set( 'isVertical', false );
589
-
590
- // 1:1 pass–through binding, all ToolbarView#items are visible.
591
- view.itemsView.children.bindTo( view.items ).using( item => item );
592
-
593
- // 1:1 pass–through binding, all ToolbarView#items are focusable.
594
- view.focusables.bindTo( view.items ).using( item => item );
595
-
596
- view.extendTemplate( {
597
- attributes: {
598
- class: [
599
- // When vertical, the toolbar has an additional CSS class.
600
- bind.if( 'isVertical', 'ck-toolbar_vertical' )
601
- ]
602
- }
603
- } );
604
- }
605
-
606
- /**
607
- * @inheritDoc
608
- */
609
- render() {}
610
-
611
- /**
612
- * @inheritDoc
613
- */
614
- destroy() {}
494
+ /**
495
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
496
+ * behavior.
497
+ *
498
+ * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
499
+ * is added to.
500
+ */
501
+ constructor(view) {
502
+ const bind = view.bindTemplate;
503
+ // Static toolbar can be vertical when needed.
504
+ view.set('isVertical', false);
505
+ // 1:1 pass–through binding, all ToolbarView#items are visible.
506
+ view.itemsView.children.bindTo(view.items).using(item => item);
507
+ // 1:1 pass–through binding, all ToolbarView#items are focusable.
508
+ view.focusables.bindTo(view.items).using(item => item);
509
+ view.extendTemplate({
510
+ attributes: {
511
+ class: [
512
+ // When vertical, the toolbar has an additional CSS class.
513
+ bind.if('isVertical', 'ck-toolbar_vertical')
514
+ ]
515
+ }
516
+ });
517
+ }
518
+ /**
519
+ * @inheritDoc
520
+ */
521
+ render() { }
522
+ /**
523
+ * @inheritDoc
524
+ */
525
+ destroy() { }
615
526
  }
616
-
617
527
  /**
618
528
  * A toolbar behavior that makes the items respond to changes in the geometry.
619
529
  *
@@ -634,494 +544,399 @@ class StaticLayout {
634
544
  * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
635
545
  */
636
546
  class DynamicGrouping {
637
- /**
638
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
639
- * behavior.
640
- *
641
- * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
642
- * is added to.
643
- */
644
- constructor( view ) {
645
- /**
646
- * A toolbar view this behavior belongs to.
647
- *
648
- * @readonly
649
- * @member {module:ui/toolbar~ToolbarView}
650
- */
651
- this.view = view;
652
-
653
- /**
654
- * A collection of toolbar children.
655
- *
656
- * @readonly
657
- * @member {module:ui/viewcollection~ViewCollection}
658
- */
659
- this.viewChildren = view.children;
660
-
661
- /**
662
- * A collection of focusable toolbar elements.
663
- *
664
- * @readonly
665
- * @member {module:ui/viewcollection~ViewCollection}
666
- */
667
- this.viewFocusables = view.focusables;
668
-
669
- /**
670
- * A view containing toolbar items.
671
- *
672
- * @readonly
673
- * @member {module:ui/toolbar/toolbarview~ItemsView}
674
- */
675
- this.viewItemsView = view.itemsView;
676
-
677
- /**
678
- * Toolbar focus tracker.
679
- *
680
- * @readonly
681
- * @member {module:utils/focustracker~FocusTracker}
682
- */
683
- this.viewFocusTracker = view.focusTracker;
684
-
685
- /**
686
- * Toolbar locale.
687
- *
688
- * @readonly
689
- * @member {module:utils/locale~Locale}
690
- */
691
- this.viewLocale = view.locale;
692
-
693
- /**
694
- * Toolbar element.
695
- *
696
- * @readonly
697
- * @member {HTMLElement} #viewElement
698
- */
699
-
700
- /**
701
- * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
702
- * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
703
- * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
704
- * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
705
- *
706
- * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
707
- * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
708
- *
709
- * @readonly
710
- * @member {module:ui/viewcollection~ViewCollection}
711
- */
712
- this.ungroupedItems = view.createCollection();
713
-
714
- /**
715
- * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
716
- * A collection of the toolbar items that do not fit into a single row of the toolbar.
717
- * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
718
- *
719
- * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
720
- * this collection is empty.
721
- *
722
- * @readonly
723
- * @member {module:ui/viewcollection~ViewCollection}
724
- */
725
- this.groupedItems = view.createCollection();
726
-
727
- /**
728
- * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
729
- * row of the toolbar. It is displayed on demand as the last of
730
- * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
731
- * (nested) toolbar which displays items that would normally overflow.
732
- *
733
- * @readonly
734
- * @member {module:ui/dropdown/dropdownview~DropdownView}
735
- */
736
- this.groupedItemsDropdown = this._createGroupedItemsDropdown();
737
-
738
- /**
739
- * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
740
- * and manage items that do not fit into a single row.
741
- *
742
- * **Note:** Created in {@link #_enableGroupingOnResize}.
743
- *
744
- * @readonly
745
- * @member {module:utils/dom/resizeobserver~ResizeObserver}
746
- */
747
- this.resizeObserver = null;
748
-
749
- /**
750
- * A cached value of the horizontal padding style used by {@link #_updateGrouping}
751
- * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
752
- * a single toolbar line. This value can be reused between updates because it is unlikely that
753
- * the padding will change and re–using `Window.getComputedStyle()` is expensive.
754
- *
755
- * @readonly
756
- * @member {Number}
757
- */
758
- this.cachedPadding = null;
759
-
760
- /**
761
- * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
762
- * and should be executed immediately the next time the toolbar shows up.
763
- *
764
- * @readonly
765
- * @member {Boolean}
766
- */
767
- this.shouldUpdateGroupingOnNextResize = false;
768
-
769
- // Only those items that were not grouped are visible to the user.
770
- view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );
771
-
772
- // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
773
- this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
774
- this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
775
-
776
- // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
777
- view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
778
- view.children.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
779
-
780
- // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
781
- // represented in either grouped or ungrouped items at the right index.
782
- // In other words #items == concat( #ungroupedItems, #groupedItems )
783
- // (in length and order).
784
- view.items.on( 'change', ( evt, changeData ) => {
785
- const index = changeData.index;
786
-
787
- // Removing.
788
- for ( const removedItem of changeData.removed ) {
789
- if ( index >= this.ungroupedItems.length ) {
790
- this.groupedItems.remove( removedItem );
791
- } else {
792
- this.ungroupedItems.remove( removedItem );
793
- }
794
- }
795
-
796
- // Adding.
797
- for ( let currentIndex = index; currentIndex < index + changeData.added.length; currentIndex++ ) {
798
- const addedItem = changeData.added[ currentIndex - index ];
799
-
800
- if ( currentIndex > this.ungroupedItems.length ) {
801
- this.groupedItems.add( addedItem, currentIndex - this.ungroupedItems.length );
802
- } else {
803
- this.ungroupedItems.add( addedItem, currentIndex );
804
- }
805
- }
806
-
807
- // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
808
- // the toolbar to overflow.
809
- // Consequently if removed from grouped or ungrouped items, there is a chance
810
- // some new space is available and we could do some ungrouping.
811
- this._updateGrouping();
812
- } );
813
-
814
- view.extendTemplate( {
815
- attributes: {
816
- class: [
817
- // To group items dynamically, the toolbar needs a dedicated CSS class.
818
- 'ck-toolbar_grouping'
819
- ]
820
- }
821
- } );
822
- }
823
-
824
- /**
825
- * Enables dynamic items grouping based on the dimensions of the toolbar.
826
- *
827
- * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
828
- * is added to.
829
- */
830
- render( view ) {
831
- this.viewElement = view.element;
832
-
833
- this._enableGroupingOnResize();
834
- this._enableGroupingOnMaxWidthChange( view );
835
- }
836
-
837
- /**
838
- * Cleans up the internals used by this behavior.
839
- */
840
- destroy() {
841
- // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
842
- // so let's make sure it's actually destroyed along with the toolbar.
843
- this.groupedItemsDropdown.destroy();
844
-
845
- this.resizeObserver.destroy();
846
- }
847
-
848
- /**
849
- * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
850
- * and it will move them to the {@link #groupedItems} when it happens.
851
- *
852
- * At the same time, it will also check if there is enough space in the toolbar for the first of the
853
- * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
854
- * without the toolbar wrapping.
855
- *
856
- * @protected
857
- */
858
- _updateGrouping() {
859
- // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
860
- // for instance before #render(), or after render but without a parent or a parent detached
861
- // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
862
- // nothing else. This happens, for instance, when the toolbar is detached from DOM and
863
- // some logic adds or removes its #items.
864
- if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
865
- return;
866
- }
867
-
868
- // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
869
- // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
870
- // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
871
- // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
872
- // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
873
- if ( !isVisible( this.viewElement ) ) {
874
- this.shouldUpdateGroupingOnNextResize = true;
875
-
876
- return;
877
- }
878
-
879
- // Remember how many items were initially grouped so at the it is possible to figure out if the number
880
- // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
881
- const initialGroupedItemsCount = this.groupedItems.length;
882
- let wereItemsGrouped;
883
-
884
- // Group #items as long as some wrap to the next row. This will happen, for instance,
885
- // when the toolbar is getting narrow and there is not enough space to display all items in
886
- // a single row.
887
- while ( this._areItemsOverflowing ) {
888
- this._groupLastItem();
889
-
890
- wereItemsGrouped = true;
891
- }
892
-
893
- // If none were grouped now but there were some items already grouped before,
894
- // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
895
- // for instance, the toolbar is stretching and there's more space in it than before.
896
- if ( !wereItemsGrouped && this.groupedItems.length ) {
897
- // Ungroup items as long as none are overflowing or there are none to ungroup left.
898
- while ( this.groupedItems.length && !this._areItemsOverflowing ) {
899
- this._ungroupFirstItem();
900
- }
901
-
902
- // If the ungrouping ended up with some item wrapping to the next row,
903
- // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
904
- // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
905
- // clean–up is vital for the algorithm.
906
- if ( this._areItemsOverflowing ) {
907
- this._groupLastItem();
908
- }
909
- }
910
-
911
- if ( this.groupedItems.length !== initialGroupedItemsCount ) {
912
- this.view.fire( 'groupedItemsUpdate' );
913
- }
914
- }
915
-
916
- /**
917
- * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
918
- * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
919
- *
920
- * @private
921
- * @type {Boolean}
922
- */
923
- get _areItemsOverflowing() {
924
- // An empty toolbar cannot overflow.
925
- if ( !this.ungroupedItems.length ) {
926
- return false;
927
- }
928
-
929
- const element = this.viewElement;
930
- const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
931
- const lastChildRect = new Rect( element.lastChild );
932
- const toolbarRect = new Rect( element );
933
-
934
- if ( !this.cachedPadding ) {
935
- const computedStyle = global.window.getComputedStyle( element );
936
- const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
937
-
938
- // parseInt() is essential because of quirky floating point numbers logic and DOM.
939
- // If the padding turned out too big because of that, the grouped items dropdown would
940
- // always look (from the Rect perspective) like it overflows (while it's not).
941
- this.cachedPadding = Number.parseInt( computedStyle[ paddingProperty ] );
942
- }
943
-
944
- if ( uiLanguageDirection === 'ltr' ) {
945
- return lastChildRect.right > toolbarRect.right - this.cachedPadding;
946
- } else {
947
- return lastChildRect.left < toolbarRect.left + this.cachedPadding;
948
- }
949
- }
950
-
951
- /**
952
- * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
953
- * upon resize when there is little space available. Instead, the toolbar items are moved to the
954
- * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
955
- *
956
- * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
957
- * them in the dropdown if necessary. It will also observe the browser window for size changes in
958
- * the future and respond to them by grouping more items or reverting already grouped back, depending
959
- * on the visual space available.
960
- *
961
- * @private
962
- */
963
- _enableGroupingOnResize() {
964
- let previousWidth;
965
-
966
- // TODO: Consider debounce.
967
- this.resizeObserver = new ResizeObserver( this.viewElement, entry => {
968
- if ( !previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize ) {
969
- this.shouldUpdateGroupingOnNextResize = false;
970
-
971
- this._updateGrouping();
972
-
973
- previousWidth = entry.contentRect.width;
974
- }
975
- } );
976
-
977
- this._updateGrouping();
978
- }
979
-
980
- /**
981
- * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
982
- * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
983
- *
984
- * @private
985
- */
986
- _enableGroupingOnMaxWidthChange( view ) {
987
- view.on( 'change:maxWidth', () => {
988
- this._updateGrouping();
989
- } );
990
- }
991
-
992
- /**
993
- * When called, it will remove the last item from {@link #ungroupedItems} and move it back
994
- * to the {@link #groupedItems} collection.
995
- *
996
- * The opposite of {@link #_ungroupFirstItem}.
997
- *
998
- * @private
999
- */
1000
- _groupLastItem() {
1001
- if ( !this.groupedItems.length ) {
1002
- this.viewChildren.add( new ToolbarSeparatorView() );
1003
- this.viewChildren.add( this.groupedItemsDropdown );
1004
- this.viewFocusTracker.add( this.groupedItemsDropdown.element );
1005
- }
1006
-
1007
- this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
1008
- }
1009
-
1010
- /**
1011
- * Moves the very first item belonging to {@link #groupedItems} back
1012
- * to the {@link #ungroupedItems} collection.
1013
- *
1014
- * The opposite of {@link #_groupLastItem}.
1015
- *
1016
- * @private
1017
- */
1018
- _ungroupFirstItem() {
1019
- this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );
1020
-
1021
- if ( !this.groupedItems.length ) {
1022
- this.viewChildren.remove( this.groupedItemsDropdown );
1023
- this.viewChildren.remove( this.viewChildren.last );
1024
- this.viewFocusTracker.remove( this.groupedItemsDropdown.element );
1025
- }
1026
- }
1027
-
1028
- /**
1029
- * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
1030
- * collection when there is not enough space in the toolbar to display all items in a single row.
1031
- *
1032
- * @private
1033
- * @returns {module:ui/dropdown/dropdownview~DropdownView}
1034
- */
1035
- _createGroupedItemsDropdown() {
1036
- const locale = this.viewLocale;
1037
- const t = locale.t;
1038
- const dropdown = createDropdown( locale );
1039
-
1040
- dropdown.class = 'ck-toolbar__grouped-dropdown';
1041
-
1042
- // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
1043
- // (https://github.com/ckeditor/ckeditor5/issues/5608)
1044
- dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
1045
-
1046
- addToolbarToDropdown( dropdown, [] );
1047
-
1048
- dropdown.buttonView.set( {
1049
- label: t( 'Show more items' ),
1050
- tooltip: true,
1051
- tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
1052
- icon: threeVerticalDots
1053
- } );
1054
-
1055
- // 1:1 pass–through binding.
1056
- dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
1057
-
1058
- return dropdown;
1059
- }
1060
-
1061
- /**
1062
- * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
1063
- * collection so it represents the up–to–date state of the UI from the perspective of the user.
1064
- *
1065
- * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
1066
- * it must be subject to focus cycling in the toolbar.
1067
- *
1068
- * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
1069
- * to learn more about the purpose of this method.
1070
- *
1071
- * @private
1072
- */
1073
- _updateFocusCycleableItems() {
1074
- this.viewFocusables.clear();
1075
-
1076
- this.ungroupedItems.map( item => {
1077
- this.viewFocusables.add( item );
1078
- } );
1079
-
1080
- if ( this.groupedItems.length ) {
1081
- this.viewFocusables.add( this.groupedItemsDropdown );
1082
- }
1083
- }
547
+ /**
548
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
549
+ * behavior.
550
+ *
551
+ * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
552
+ * is added to.
553
+ */
554
+ constructor(view) {
555
+ /**
556
+ * A toolbar view this behavior belongs to.
557
+ *
558
+ * @readonly
559
+ * @member {module:ui/toolbar~ToolbarView}
560
+ */
561
+ this.view = view;
562
+ /**
563
+ * A collection of toolbar children.
564
+ *
565
+ * @readonly
566
+ * @member {module:ui/viewcollection~ViewCollection}
567
+ */
568
+ this.viewChildren = view.children;
569
+ /**
570
+ * A collection of focusable toolbar elements.
571
+ *
572
+ * @readonly
573
+ * @member {module:ui/viewcollection~ViewCollection}
574
+ */
575
+ this.viewFocusables = view.focusables;
576
+ /**
577
+ * A view containing toolbar items.
578
+ *
579
+ * @readonly
580
+ * @member {module:ui/toolbar/toolbarview~ItemsView}
581
+ */
582
+ this.viewItemsView = view.itemsView;
583
+ /**
584
+ * Toolbar focus tracker.
585
+ *
586
+ * @readonly
587
+ * @member {module:utils/focustracker~FocusTracker}
588
+ */
589
+ this.viewFocusTracker = view.focusTracker;
590
+ /**
591
+ * Toolbar locale.
592
+ *
593
+ * @readonly
594
+ * @member {module:utils/locale~Locale}
595
+ */
596
+ this.viewLocale = view.locale;
597
+ /**
598
+ * Toolbar element.
599
+ *
600
+ * @readonly
601
+ * @member {HTMLElement} #viewElement
602
+ */
603
+ /**
604
+ * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
605
+ * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
606
+ * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
607
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
608
+ *
609
+ * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
610
+ * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
611
+ *
612
+ * @readonly
613
+ * @member {module:ui/viewcollection~ViewCollection}
614
+ */
615
+ this.ungroupedItems = view.createCollection();
616
+ /**
617
+ * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
618
+ * A collection of the toolbar items that do not fit into a single row of the toolbar.
619
+ * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
620
+ *
621
+ * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
622
+ * this collection is empty.
623
+ *
624
+ * @readonly
625
+ * @member {module:ui/viewcollection~ViewCollection}
626
+ */
627
+ this.groupedItems = view.createCollection();
628
+ /**
629
+ * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
630
+ * row of the toolbar. It is displayed on demand as the last of
631
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
632
+ * (nested) toolbar which displays items that would normally overflow.
633
+ *
634
+ * @readonly
635
+ * @member {module:ui/dropdown/dropdownview~DropdownView}
636
+ */
637
+ this.groupedItemsDropdown = this._createGroupedItemsDropdown();
638
+ /**
639
+ * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
640
+ * and manage items that do not fit into a single row.
641
+ *
642
+ * **Note:** Created in {@link #_enableGroupingOnResize}.
643
+ *
644
+ * @readonly
645
+ * @member {module:utils/dom/resizeobserver~ResizeObserver}
646
+ */
647
+ this.resizeObserver = null;
648
+ /**
649
+ * A cached value of the horizontal padding style used by {@link #_updateGrouping}
650
+ * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
651
+ * a single toolbar line. This value can be reused between updates because it is unlikely that
652
+ * the padding will change and re–using `Window.getComputedStyle()` is expensive.
653
+ *
654
+ * @readonly
655
+ * @member {Number}
656
+ */
657
+ this.cachedPadding = null;
658
+ /**
659
+ * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
660
+ * and should be executed immediately the next time the toolbar shows up.
661
+ *
662
+ * @readonly
663
+ * @member {Boolean}
664
+ */
665
+ this.shouldUpdateGroupingOnNextResize = false;
666
+ // Only those items that were not grouped are visible to the user.
667
+ view.itemsView.children.bindTo(this.ungroupedItems).using(item => item);
668
+ // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
669
+ this.ungroupedItems.on('add', this._updateFocusCycleableItems.bind(this));
670
+ this.ungroupedItems.on('remove', this._updateFocusCycleableItems.bind(this));
671
+ // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
672
+ view.children.on('add', this._updateFocusCycleableItems.bind(this));
673
+ view.children.on('remove', this._updateFocusCycleableItems.bind(this));
674
+ // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
675
+ // represented in either grouped or ungrouped items at the right index.
676
+ // In other words #items == concat( #ungroupedItems, #groupedItems )
677
+ // (in length and order).
678
+ view.items.on('change', (evt, changeData) => {
679
+ const index = changeData.index;
680
+ const added = Array.from(changeData.added);
681
+ // Removing.
682
+ for (const removedItem of changeData.removed) {
683
+ if (index >= this.ungroupedItems.length) {
684
+ this.groupedItems.remove(removedItem);
685
+ }
686
+ else {
687
+ this.ungroupedItems.remove(removedItem);
688
+ }
689
+ }
690
+ // Adding.
691
+ for (let currentIndex = index; currentIndex < index + added.length; currentIndex++) {
692
+ const addedItem = added[currentIndex - index];
693
+ if (currentIndex > this.ungroupedItems.length) {
694
+ this.groupedItems.add(addedItem, currentIndex - this.ungroupedItems.length);
695
+ }
696
+ else {
697
+ this.ungroupedItems.add(addedItem, currentIndex);
698
+ }
699
+ }
700
+ // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
701
+ // the toolbar to overflow.
702
+ // Consequently if removed from grouped or ungrouped items, there is a chance
703
+ // some new space is available and we could do some ungrouping.
704
+ this._updateGrouping();
705
+ });
706
+ view.extendTemplate({
707
+ attributes: {
708
+ class: [
709
+ // To group items dynamically, the toolbar needs a dedicated CSS class.
710
+ 'ck-toolbar_grouping'
711
+ ]
712
+ }
713
+ });
714
+ }
715
+ /**
716
+ * Enables dynamic items grouping based on the dimensions of the toolbar.
717
+ *
718
+ * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
719
+ * is added to.
720
+ */
721
+ render(view) {
722
+ this.viewElement = view.element;
723
+ this._enableGroupingOnResize();
724
+ this._enableGroupingOnMaxWidthChange(view);
725
+ }
726
+ /**
727
+ * Cleans up the internals used by this behavior.
728
+ */
729
+ destroy() {
730
+ // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
731
+ // so let's make sure it's actually destroyed along with the toolbar.
732
+ this.groupedItemsDropdown.destroy();
733
+ this.resizeObserver.destroy();
734
+ }
735
+ /**
736
+ * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
737
+ * and it will move them to the {@link #groupedItems} when it happens.
738
+ *
739
+ * At the same time, it will also check if there is enough space in the toolbar for the first of the
740
+ * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
741
+ * without the toolbar wrapping.
742
+ *
743
+ * @protected
744
+ */
745
+ _updateGrouping() {
746
+ // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
747
+ // for instance before #render(), or after render but without a parent or a parent detached
748
+ // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
749
+ // nothing else. This happens, for instance, when the toolbar is detached from DOM and
750
+ // some logic adds or removes its #items.
751
+ if (!this.viewElement.ownerDocument.body.contains(this.viewElement)) {
752
+ return;
753
+ }
754
+ // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
755
+ // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
756
+ // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
757
+ // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
758
+ // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
759
+ if (!isVisible(this.viewElement)) {
760
+ this.shouldUpdateGroupingOnNextResize = true;
761
+ return;
762
+ }
763
+ // Remember how many items were initially grouped so at the it is possible to figure out if the number
764
+ // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
765
+ const initialGroupedItemsCount = this.groupedItems.length;
766
+ let wereItemsGrouped;
767
+ // Group #items as long as some wrap to the next row. This will happen, for instance,
768
+ // when the toolbar is getting narrow and there is not enough space to display all items in
769
+ // a single row.
770
+ while (this._areItemsOverflowing) {
771
+ this._groupLastItem();
772
+ wereItemsGrouped = true;
773
+ }
774
+ // If none were grouped now but there were some items already grouped before,
775
+ // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
776
+ // for instance, the toolbar is stretching and there's more space in it than before.
777
+ if (!wereItemsGrouped && this.groupedItems.length) {
778
+ // Ungroup items as long as none are overflowing or there are none to ungroup left.
779
+ while (this.groupedItems.length && !this._areItemsOverflowing) {
780
+ this._ungroupFirstItem();
781
+ }
782
+ // If the ungrouping ended up with some item wrapping to the next row,
783
+ // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
784
+ // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
785
+ // clean–up is vital for the algorithm.
786
+ if (this._areItemsOverflowing) {
787
+ this._groupLastItem();
788
+ }
789
+ }
790
+ if (this.groupedItems.length !== initialGroupedItemsCount) {
791
+ this.view.fire('groupedItemsUpdate');
792
+ }
793
+ }
794
+ /**
795
+ * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
796
+ * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
797
+ *
798
+ * @private
799
+ * @type {Boolean}
800
+ */
801
+ get _areItemsOverflowing() {
802
+ // An empty toolbar cannot overflow.
803
+ if (!this.ungroupedItems.length) {
804
+ return false;
805
+ }
806
+ const element = this.viewElement;
807
+ const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
808
+ const lastChildRect = new Rect(element.lastChild);
809
+ const toolbarRect = new Rect(element);
810
+ if (!this.cachedPadding) {
811
+ const computedStyle = global.window.getComputedStyle(element);
812
+ const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
813
+ // parseInt() is essential because of quirky floating point numbers logic and DOM.
814
+ // If the padding turned out too big because of that, the grouped items dropdown would
815
+ // always look (from the Rect perspective) like it overflows (while it's not).
816
+ this.cachedPadding = Number.parseInt(computedStyle[paddingProperty]);
817
+ }
818
+ if (uiLanguageDirection === 'ltr') {
819
+ return lastChildRect.right > toolbarRect.right - this.cachedPadding;
820
+ }
821
+ else {
822
+ return lastChildRect.left < toolbarRect.left + this.cachedPadding;
823
+ }
824
+ }
825
+ /**
826
+ * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
827
+ * upon resize when there is little space available. Instead, the toolbar items are moved to the
828
+ * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
829
+ *
830
+ * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
831
+ * them in the dropdown if necessary. It will also observe the browser window for size changes in
832
+ * the future and respond to them by grouping more items or reverting already grouped back, depending
833
+ * on the visual space available.
834
+ *
835
+ * @private
836
+ */
837
+ _enableGroupingOnResize() {
838
+ let previousWidth;
839
+ // TODO: Consider debounce.
840
+ this.resizeObserver = new ResizeObserver(this.viewElement, entry => {
841
+ if (!previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize) {
842
+ this.shouldUpdateGroupingOnNextResize = false;
843
+ this._updateGrouping();
844
+ previousWidth = entry.contentRect.width;
845
+ }
846
+ });
847
+ this._updateGrouping();
848
+ }
849
+ /**
850
+ * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
851
+ * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
852
+ *
853
+ * @private
854
+ */
855
+ _enableGroupingOnMaxWidthChange(view) {
856
+ view.on('change:maxWidth', () => {
857
+ this._updateGrouping();
858
+ });
859
+ }
860
+ /**
861
+ * When called, it will remove the last item from {@link #ungroupedItems} and move it back
862
+ * to the {@link #groupedItems} collection.
863
+ *
864
+ * The opposite of {@link #_ungroupFirstItem}.
865
+ *
866
+ * @private
867
+ */
868
+ _groupLastItem() {
869
+ if (!this.groupedItems.length) {
870
+ this.viewChildren.add(new ToolbarSeparatorView());
871
+ this.viewChildren.add(this.groupedItemsDropdown);
872
+ this.viewFocusTracker.add(this.groupedItemsDropdown.element);
873
+ }
874
+ this.groupedItems.add(this.ungroupedItems.remove(this.ungroupedItems.last), 0);
875
+ }
876
+ /**
877
+ * Moves the very first item belonging to {@link #groupedItems} back
878
+ * to the {@link #ungroupedItems} collection.
879
+ *
880
+ * The opposite of {@link #_groupLastItem}.
881
+ *
882
+ * @private
883
+ */
884
+ _ungroupFirstItem() {
885
+ this.ungroupedItems.add(this.groupedItems.remove(this.groupedItems.first));
886
+ if (!this.groupedItems.length) {
887
+ this.viewChildren.remove(this.groupedItemsDropdown);
888
+ this.viewChildren.remove(this.viewChildren.last);
889
+ this.viewFocusTracker.remove(this.groupedItemsDropdown.element);
890
+ }
891
+ }
892
+ /**
893
+ * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
894
+ * collection when there is not enough space in the toolbar to display all items in a single row.
895
+ *
896
+ * @private
897
+ * @returns {module:ui/dropdown/dropdownview~DropdownView}
898
+ */
899
+ _createGroupedItemsDropdown() {
900
+ const locale = this.viewLocale;
901
+ const t = locale.t;
902
+ const dropdown = createDropdown(locale);
903
+ dropdown.class = 'ck-toolbar__grouped-dropdown';
904
+ // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
905
+ // (https://github.com/ckeditor/ckeditor5/issues/5608)
906
+ dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
907
+ addToolbarToDropdown(dropdown, []);
908
+ dropdown.buttonView.set({
909
+ label: t('Show more items'),
910
+ tooltip: true,
911
+ tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
912
+ icon: threeVerticalDots
913
+ });
914
+ // 1:1 pass–through binding.
915
+ dropdown.toolbarView.items.bindTo(this.groupedItems).using(item => item);
916
+ return dropdown;
917
+ }
918
+ /**
919
+ * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
920
+ * collection so it represents the up–to–date state of the UI from the perspective of the user.
921
+ *
922
+ * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
923
+ * it must be subject to focus cycling in the toolbar.
924
+ *
925
+ * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
926
+ * to learn more about the purpose of this method.
927
+ *
928
+ * @private
929
+ */
930
+ _updateFocusCycleableItems() {
931
+ this.viewFocusables.clear();
932
+ this.ungroupedItems.map(item => {
933
+ this.viewFocusables.add(item);
934
+ });
935
+ if (this.groupedItems.length) {
936
+ this.viewFocusables.add(this.groupedItemsDropdown);
937
+ }
938
+ }
1084
939
  }
1085
-
1086
- /**
1087
- * Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
1088
- *
1089
- * @interface module:ui/toolbar/toolbarview~ToolbarOptions
1090
- */
1091
-
1092
- /**
1093
- * When set to `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
1094
- * would normally wrap to the next line when there is not enough space to display them in a single row, for
1095
- * instance, if the parent container of the toolbar is narrow. For toolbars in absolutely positioned containers
1096
- * without width restrictions also the {@link module:ui/toolbar/toolbarview~ToolbarOptions#isFloating} option is required to be `true`.
1097
- *
1098
- * See also: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}.
1099
- *
1100
- * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
1101
- */
1102
-
1103
- /**
1104
- * This option should be enabled for toolbars in absolutely positioned containers without width restrictions
1105
- * to enable automatic {@link module:ui/toolbar/toolbarview~ToolbarView#items} grouping.
1106
- * When this option is set to `true`, the items will stop wrapping to the next line
1107
- * and together with {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull},
1108
- * this will allow grouping them when there is not enough space in a single row.
1109
- *
1110
- * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#isFloating
1111
- */
1112
-
1113
- /**
1114
- * A class interface defining the behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
1115
- *
1116
- * Toolbar behaviors extend its look and functionality and have an impact on the
1117
- * {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
1118
- * {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
1119
- * conditionally, e.g. depending on the configuration of the toolbar.
1120
- *
1121
- * @private
1122
- * @interface module:ui/toolbar/toolbarview~ToolbarBehavior
1123
- */
1124
-
1125
940
  /**
1126
941
  * Creates a new toolbar behavior instance.
1127
942
  *
@@ -1132,7 +947,6 @@ class DynamicGrouping {
1132
947
  * @method #constructor
1133
948
  * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior is added to.
1134
949
  */
1135
-
1136
950
  /**
1137
951
  * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
1138
952
  * It can be used to, for example, customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
@@ -1142,7 +956,6 @@ class DynamicGrouping {
1142
956
  * @member {Function} #render
1143
957
  * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
1144
958
  */
1145
-
1146
959
  /**
1147
960
  * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
1148
961
  * It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach