@ckeditor/ckeditor5-ui 40.0.0 → 40.1.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 (165) hide show
  1. package/LICENSE.md +3 -3
  2. package/lang/translations/pt-br.po +1 -1
  3. package/lang/translations/ug.po +26 -26
  4. package/package.json +3 -3
  5. package/src/arialiveannouncer.d.ts +94 -0
  6. package/src/arialiveannouncer.js +113 -0
  7. package/src/augmentation.d.ts +86 -86
  8. package/src/augmentation.js +5 -5
  9. package/src/autocomplete/autocompleteview.d.ts +81 -81
  10. package/src/autocomplete/autocompleteview.js +153 -146
  11. package/src/bindings/addkeyboardhandlingforgrid.d.ts +27 -27
  12. package/src/bindings/addkeyboardhandlingforgrid.js +107 -107
  13. package/src/bindings/clickoutsidehandler.d.ts +28 -28
  14. package/src/bindings/clickoutsidehandler.js +36 -36
  15. package/src/bindings/csstransitiondisablermixin.d.ts +40 -40
  16. package/src/bindings/csstransitiondisablermixin.js +55 -55
  17. package/src/bindings/injectcsstransitiondisabler.d.ts +59 -59
  18. package/src/bindings/injectcsstransitiondisabler.js +71 -71
  19. package/src/bindings/preventdefault.d.ts +33 -33
  20. package/src/bindings/preventdefault.js +34 -34
  21. package/src/bindings/submithandler.d.ts +57 -57
  22. package/src/bindings/submithandler.js +47 -47
  23. package/src/button/button.d.ts +172 -178
  24. package/src/button/button.js +5 -5
  25. package/src/button/buttonlabel.d.ts +34 -34
  26. package/src/button/buttonlabel.js +5 -5
  27. package/src/button/buttonlabelview.d.ts +31 -31
  28. package/src/button/buttonlabelview.js +42 -42
  29. package/src/button/buttonview.d.ts +181 -185
  30. package/src/button/buttonview.js +217 -219
  31. package/src/button/switchbuttonview.d.ts +45 -45
  32. package/src/button/switchbuttonview.js +75 -75
  33. package/src/colorgrid/colorgridview.d.ts +132 -132
  34. package/src/colorgrid/colorgridview.js +124 -124
  35. package/src/colorgrid/colortileview.d.ts +28 -28
  36. package/src/colorgrid/colortileview.js +40 -40
  37. package/src/colorgrid/utils.d.ts +47 -47
  38. package/src/colorgrid/utils.js +84 -84
  39. package/src/colorpicker/colorpickerview.d.ts +137 -137
  40. package/src/colorpicker/colorpickerview.js +270 -270
  41. package/src/colorpicker/utils.d.ts +43 -43
  42. package/src/colorpicker/utils.js +99 -99
  43. package/src/colorselector/colorgridsfragmentview.d.ts +194 -194
  44. package/src/colorselector/colorgridsfragmentview.js +289 -289
  45. package/src/colorselector/colorpickerfragmentview.d.ts +128 -128
  46. package/src/colorselector/colorpickerfragmentview.js +205 -205
  47. package/src/colorselector/colorselectorview.d.ts +242 -242
  48. package/src/colorselector/colorselectorview.js +256 -256
  49. package/src/colorselector/documentcolorcollection.d.ts +70 -70
  50. package/src/colorselector/documentcolorcollection.js +42 -42
  51. package/src/componentfactory.d.ts +81 -81
  52. package/src/componentfactory.js +104 -104
  53. package/src/dropdown/button/dropdownbutton.d.ts +25 -25
  54. package/src/dropdown/button/dropdownbutton.js +5 -5
  55. package/src/dropdown/button/dropdownbuttonview.d.ts +48 -48
  56. package/src/dropdown/button/dropdownbuttonview.js +66 -66
  57. package/src/dropdown/button/splitbuttonview.d.ts +161 -161
  58. package/src/dropdown/button/splitbuttonview.js +152 -152
  59. package/src/dropdown/dropdownpanelfocusable.d.ts +21 -21
  60. package/src/dropdown/dropdownpanelfocusable.js +5 -5
  61. package/src/dropdown/dropdownpanelview.d.ts +62 -62
  62. package/src/dropdown/dropdownpanelview.js +97 -97
  63. package/src/dropdown/dropdownview.d.ts +315 -315
  64. package/src/dropdown/dropdownview.js +379 -379
  65. package/src/dropdown/utils.d.ts +235 -235
  66. package/src/dropdown/utils.js +463 -458
  67. package/src/editableui/editableuiview.d.ts +72 -72
  68. package/src/editableui/editableuiview.js +112 -112
  69. package/src/editableui/inline/inlineeditableuiview.d.ts +40 -40
  70. package/src/editableui/inline/inlineeditableuiview.js +48 -48
  71. package/src/editorui/bodycollection.d.ts +55 -55
  72. package/src/editorui/bodycollection.js +84 -84
  73. package/src/editorui/boxed/boxededitoruiview.d.ts +40 -40
  74. package/src/editorui/boxed/boxededitoruiview.js +81 -81
  75. package/src/editorui/editorui.d.ts +288 -282
  76. package/src/editorui/editorui.js +412 -410
  77. package/src/editorui/editoruiview.d.ts +39 -39
  78. package/src/editorui/editoruiview.js +38 -38
  79. package/src/editorui/poweredby.d.ts +71 -71
  80. package/src/editorui/poweredby.js +276 -276
  81. package/src/focuscycler.d.ts +226 -226
  82. package/src/focuscycler.js +245 -245
  83. package/src/formheader/formheaderview.d.ts +59 -59
  84. package/src/formheader/formheaderview.js +69 -69
  85. package/src/highlightedtext/highlightedtextview.d.ts +38 -38
  86. package/src/highlightedtext/highlightedtextview.js +102 -102
  87. package/src/icon/iconview.d.ts +85 -85
  88. package/src/icon/iconview.js +114 -114
  89. package/src/iframe/iframeview.d.ts +50 -50
  90. package/src/iframe/iframeview.js +63 -63
  91. package/src/index.d.ts +73 -73
  92. package/src/index.js +70 -70
  93. package/src/input/inputbase.d.ts +107 -107
  94. package/src/input/inputbase.js +110 -110
  95. package/src/input/inputview.d.ts +36 -36
  96. package/src/input/inputview.js +24 -24
  97. package/src/inputnumber/inputnumberview.d.ts +49 -49
  98. package/src/inputnumber/inputnumberview.js +40 -40
  99. package/src/inputtext/inputtextview.d.ts +18 -18
  100. package/src/inputtext/inputtextview.js +27 -27
  101. package/src/label/labelview.d.ts +36 -36
  102. package/src/label/labelview.js +41 -41
  103. package/src/labeledfield/labeledfieldview.d.ts +187 -187
  104. package/src/labeledfield/labeledfieldview.js +157 -157
  105. package/src/labeledfield/utils.d.ts +123 -123
  106. package/src/labeledfield/utils.js +176 -176
  107. package/src/labeledinput/labeledinputview.d.ts +125 -125
  108. package/src/labeledinput/labeledinputview.js +125 -125
  109. package/src/list/listitemgroupview.d.ts +59 -51
  110. package/src/list/listitemgroupview.js +63 -75
  111. package/src/list/listitemview.d.ts +36 -36
  112. package/src/list/listitemview.js +42 -42
  113. package/src/list/listseparatorview.d.ts +18 -18
  114. package/src/list/listseparatorview.js +28 -28
  115. package/src/list/listview.d.ts +122 -122
  116. package/src/list/listview.js +187 -187
  117. package/src/model.d.ts +22 -22
  118. package/src/model.js +31 -31
  119. package/src/notification/notification.d.ts +211 -211
  120. package/src/notification/notification.js +187 -187
  121. package/src/panel/balloon/balloonpanelview.d.ts +685 -685
  122. package/src/panel/balloon/balloonpanelview.js +1010 -1010
  123. package/src/panel/balloon/contextualballoon.d.ts +299 -299
  124. package/src/panel/balloon/contextualballoon.js +572 -572
  125. package/src/panel/sticky/stickypanelview.d.ts +156 -156
  126. package/src/panel/sticky/stickypanelview.js +234 -234
  127. package/src/search/filteredview.d.ts +31 -31
  128. package/src/search/filteredview.js +5 -5
  129. package/src/search/searchinfoview.d.ts +45 -45
  130. package/src/search/searchinfoview.js +59 -59
  131. package/src/search/searchresultsview.d.ts +54 -54
  132. package/src/search/searchresultsview.js +65 -65
  133. package/src/search/text/searchtextqueryview.d.ts +76 -76
  134. package/src/search/text/searchtextqueryview.js +75 -75
  135. package/src/search/text/searchtextview.d.ts +219 -219
  136. package/src/search/text/searchtextview.js +201 -201
  137. package/src/spinner/spinnerview.d.ts +25 -25
  138. package/src/spinner/spinnerview.js +38 -38
  139. package/src/template.d.ts +942 -942
  140. package/src/template.js +1294 -1294
  141. package/src/textarea/textareaview.d.ts +88 -88
  142. package/src/textarea/textareaview.js +142 -140
  143. package/src/toolbar/balloon/balloontoolbar.d.ts +122 -122
  144. package/src/toolbar/balloon/balloontoolbar.js +300 -300
  145. package/src/toolbar/block/blockbuttonview.d.ts +35 -35
  146. package/src/toolbar/block/blockbuttonview.js +41 -41
  147. package/src/toolbar/block/blocktoolbar.d.ts +161 -161
  148. package/src/toolbar/block/blocktoolbar.js +395 -395
  149. package/src/toolbar/normalizetoolbarconfig.d.ts +40 -40
  150. package/src/toolbar/normalizetoolbarconfig.js +52 -51
  151. package/src/toolbar/toolbarlinebreakview.d.ts +18 -18
  152. package/src/toolbar/toolbarlinebreakview.js +28 -28
  153. package/src/toolbar/toolbarseparatorview.d.ts +18 -18
  154. package/src/toolbar/toolbarseparatorview.js +28 -28
  155. package/src/toolbar/toolbarview.d.ts +266 -266
  156. package/src/toolbar/toolbarview.js +719 -719
  157. package/src/tooltipmanager.d.ts +180 -180
  158. package/src/tooltipmanager.js +353 -353
  159. package/src/view.d.ts +422 -422
  160. package/src/view.js +396 -396
  161. package/src/viewcollection.d.ts +139 -139
  162. package/src/viewcollection.js +206 -206
  163. package/theme/components/arialiveannouncer/arialiveannouncer.css +10 -0
  164. package/theme/components/button/button.css +9 -1
  165. package/theme/components/formheader/formheader.css +0 -4
@@ -1,719 +1,719 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module ui/toolbar/toolbarview
7
- */
8
- import View from '../view';
9
- import FocusCycler from '../focuscycler';
10
- import ToolbarSeparatorView from './toolbarseparatorview';
11
- import ToolbarLineBreakView from './toolbarlinebreakview';
12
- import preventDefault from '../bindings/preventdefault';
13
- import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
14
- import normalizeToolbarConfig from './normalizetoolbarconfig';
15
- import { FocusTracker, KeystrokeHandler, Rect, ResizeObserver, global, isVisible, logWarning } from '@ckeditor/ckeditor5-utils';
16
- import { icons } from '@ckeditor/ckeditor5-core';
17
- import { isObject } from 'lodash-es';
18
- import '../../theme/components/toolbar/toolbar.css';
19
- const { threeVerticalDots } = icons;
20
- export const NESTED_TOOLBAR_ICONS = {
21
- alignLeft: icons.alignLeft,
22
- bold: icons.bold,
23
- importExport: icons.importExport,
24
- paragraph: icons.paragraph,
25
- plus: icons.plus,
26
- text: icons.text,
27
- threeVerticalDots: icons.threeVerticalDots,
28
- pilcrow: icons.pilcrow,
29
- dragIndicator: icons.dragIndicator
30
- };
31
- /**
32
- * The toolbar view class.
33
- */
34
- export default class ToolbarView extends View {
35
- /**
36
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
37
- *
38
- * Also see {@link #render}.
39
- *
40
- * @param locale The localization services instance.
41
- * @param options Configuration options of the toolbar.
42
- */
43
- constructor(locale, options) {
44
- super(locale);
45
- const bind = this.bindTemplate;
46
- const t = this.t;
47
- this.options = options || {};
48
- this.set('ariaLabel', t('Editor toolbar'));
49
- this.set('maxWidth', 'auto');
50
- this.items = this.createCollection();
51
- this.focusTracker = new FocusTracker();
52
- this.keystrokes = new KeystrokeHandler();
53
- this.set('class', undefined);
54
- this.set('isCompact', false);
55
- this.itemsView = new ItemsView(locale);
56
- this.children = this.createCollection();
57
- this.children.add(this.itemsView);
58
- this.focusables = this.createCollection();
59
- const isRtl = locale.uiLanguageDirection === 'rtl';
60
- this._focusCycler = new FocusCycler({
61
- focusables: this.focusables,
62
- focusTracker: this.focusTracker,
63
- keystrokeHandler: this.keystrokes,
64
- actions: {
65
- // Navigate toolbar items backwards using the arrow[left,up] keys.
66
- focusPrevious: [isRtl ? 'arrowright' : 'arrowleft', 'arrowup'],
67
- // Navigate toolbar items forwards using the arrow[right,down] keys.
68
- focusNext: [isRtl ? 'arrowleft' : 'arrowright', 'arrowdown']
69
- }
70
- });
71
- const classes = [
72
- 'ck',
73
- 'ck-toolbar',
74
- bind.to('class'),
75
- bind.if('isCompact', 'ck-toolbar_compact')
76
- ];
77
- if (this.options.shouldGroupWhenFull && this.options.isFloating) {
78
- classes.push('ck-toolbar_floating');
79
- }
80
- this.setTemplate({
81
- tag: 'div',
82
- attributes: {
83
- class: classes,
84
- role: 'toolbar',
85
- 'aria-label': bind.to('ariaLabel'),
86
- style: {
87
- maxWidth: bind.to('maxWidth')
88
- },
89
- tabindex: -1
90
- },
91
- children: this.children,
92
- on: {
93
- // https://github.com/ckeditor/ckeditor5-ui/issues/206
94
- mousedown: preventDefault(this)
95
- }
96
- });
97
- this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping(this) : new StaticLayout(this);
98
- }
99
- /**
100
- * @inheritDoc
101
- */
102
- render() {
103
- super.render();
104
- this.focusTracker.add(this.element);
105
- // Children added before rendering should be known to the #focusTracker.
106
- for (const item of this.items) {
107
- this.focusTracker.add(item.element);
108
- }
109
- this.items.on('add', (evt, item) => {
110
- this.focusTracker.add(item.element);
111
- });
112
- this.items.on('remove', (evt, item) => {
113
- this.focusTracker.remove(item.element);
114
- });
115
- // Start listening for the keystrokes coming from #element.
116
- this.keystrokes.listenTo(this.element);
117
- this._behavior.render(this);
118
- }
119
- /**
120
- * @inheritDoc
121
- */
122
- destroy() {
123
- this._behavior.destroy();
124
- this.focusTracker.destroy();
125
- this.keystrokes.destroy();
126
- return super.destroy();
127
- }
128
- /**
129
- * Focuses the first focusable in {@link #focusables}.
130
- */
131
- focus() {
132
- this._focusCycler.focusFirst();
133
- }
134
- /**
135
- * Focuses the last focusable in {@link #focusables}.
136
- */
137
- focusLast() {
138
- this._focusCycler.focusLast();
139
- }
140
- /**
141
- * A utility that expands the plain toolbar configuration into
142
- * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
143
- *
144
- * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
145
- * @param factory A factory producing toolbar items.
146
- * @param removeItems An array of items names to be removed from the configuration. When present, applies
147
- * to this toolbar and all nested ones as well.
148
- */
149
- fillFromConfig(itemsOrConfig, factory, removeItems) {
150
- this.items.addMany(this._buildItemsFromConfig(itemsOrConfig, factory, removeItems));
151
- }
152
- /**
153
- * A utility that expands the plain toolbar configuration into a list of view items using a given component factory.
154
- *
155
- * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
156
- * @param factory A factory producing toolbar items.
157
- * @param removeItems An array of items names to be removed from the configuration. When present, applies
158
- * to this toolbar and all nested ones as well.
159
- */
160
- _buildItemsFromConfig(itemsOrConfig, factory, removeItems) {
161
- const config = normalizeToolbarConfig(itemsOrConfig);
162
- const normalizedRemoveItems = removeItems || config.removeItems;
163
- const itemsToAdd = this._cleanItemsConfiguration(config.items, factory, normalizedRemoveItems)
164
- .map(item => {
165
- if (isObject(item)) {
166
- return this._createNestedToolbarDropdown(item, factory, normalizedRemoveItems);
167
- }
168
- else if (item === '|') {
169
- return new ToolbarSeparatorView();
170
- }
171
- else if (item === '-') {
172
- return new ToolbarLineBreakView();
173
- }
174
- return factory.create(item);
175
- })
176
- .filter((item) => !!item);
177
- return itemsToAdd;
178
- }
179
- /**
180
- * Cleans up the {@link module:ui/toolbar/toolbarview~ToolbarView#items} of the toolbar by removing unwanted items and
181
- * duplicated (obsolete) separators or line breaks.
182
- *
183
- * @param items The toolbar items configuration.
184
- * @param factory A factory producing toolbar items.
185
- * @param removeItems An array of items names to be removed from the configuration.
186
- * @returns Items after the clean-up.
187
- */
188
- _cleanItemsConfiguration(items, factory, removeItems) {
189
- const filteredItems = items
190
- .filter((item, idx, items) => {
191
- if (item === '|') {
192
- return true;
193
- }
194
- // Items listed in `config.removeItems` should not be added to the toolbar.
195
- if (removeItems.indexOf(item) !== -1) {
196
- return false;
197
- }
198
- if (item === '-') {
199
- // The toolbar line breaks must not be rendered when toolbar grouping is enabled.
200
- // (https://github.com/ckeditor/ckeditor5/issues/8582)
201
- if (this.options.shouldGroupWhenFull) {
202
- /**
203
- * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
204
- * is disabled in the toolbar configuration.
205
- * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
206
- *
207
- * ```ts
208
- * const config = {
209
- * toolbar: {
210
- * items: [ ... ],
211
- * shouldNotGroupWhenFull: true
212
- * }
213
- * }
214
- * ```
215
- *
216
- * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
217
- *
218
- * @error toolbarview-line-break-ignored-when-grouping-items
219
- */
220
- logWarning('toolbarview-line-break-ignored-when-grouping-items', items);
221
- return false;
222
- }
223
- return true;
224
- }
225
- // For the items that cannot be instantiated we are sending warning message. We also filter them out.
226
- if (!isObject(item) && !factory.has(item)) {
227
- /**
228
- * There was a problem processing the configuration of the toolbar. The item with the given
229
- * name does not exist so it was omitted when rendering the toolbar.
230
- *
231
- * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
232
- * to provide a toolbar item has not been loaded or there is a typo in the configuration.
233
- *
234
- * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
235
- * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
236
- *
237
- * You can use the following snippet to retrieve all available toolbar items:
238
- *
239
- * ```ts
240
- * Array.from( editor.ui.componentFactory.names() );
241
- * ```
242
- *
243
- * @error toolbarview-item-unavailable
244
- * @param item The name of the component or nested toolbar definition.
245
- */
246
- logWarning('toolbarview-item-unavailable', { item });
247
- return false;
248
- }
249
- return true;
250
- });
251
- return this._cleanSeparatorsAndLineBreaks(filteredItems);
252
- }
253
- /**
254
- * Remove leading, trailing, and duplicated separators (`-` and `|`).
255
- *
256
- * @returns Toolbar items after the separator and line break clean-up.
257
- */
258
- _cleanSeparatorsAndLineBreaks(items) {
259
- const nonSeparatorPredicate = (item) => (item !== '-' && item !== '|');
260
- const count = items.length;
261
- // Find an index of the first item that is not a separator.
262
- const firstCommandItemIndex = items.findIndex(nonSeparatorPredicate);
263
- // Items include separators only. There is no point in displaying them.
264
- if (firstCommandItemIndex === -1) {
265
- return [];
266
- }
267
- // Search from the end of the list, then convert found index back to the original direction.
268
- const lastCommandItemIndex = count - items
269
- .slice()
270
- .reverse()
271
- .findIndex(nonSeparatorPredicate);
272
- return items
273
- // Return items without the leading and trailing separators.
274
- .slice(firstCommandItemIndex, lastCommandItemIndex)
275
- // Remove duplicated separators.
276
- .filter((name, idx, items) => {
277
- // Filter only separators.
278
- if (nonSeparatorPredicate(name)) {
279
- return true;
280
- }
281
- const isDuplicated = idx > 0 && items[idx - 1] === name;
282
- return !isDuplicated;
283
- });
284
- }
285
- /**
286
- * Creates a user-defined dropdown containing a toolbar with items.
287
- *
288
- * @param definition A definition of the nested toolbar dropdown.
289
- * @param definition.label A label of the dropdown.
290
- * @param definition.icon An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft',
291
- * 'paragraph' or an SVG string. When `false` is passed, no icon will be used.
292
- * @param definition.withText When set `true`, the label of the dropdown will be visible. See
293
- * {@link module:ui/button/buttonview~ButtonView#withText} to learn more.
294
- * @param definition.tooltip A tooltip of the dropdown button. See
295
- * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more. Defaults to `true`.
296
- * @param componentFactory Component factory used to create items
297
- * of the nested toolbar.
298
- */
299
- _createNestedToolbarDropdown(definition, componentFactory, removeItems) {
300
- let { label, icon, items, tooltip = true, withText = false } = definition;
301
- items = this._cleanItemsConfiguration(items, componentFactory, removeItems);
302
- // There is no point in rendering a dropdown without items.
303
- if (!items.length) {
304
- return null;
305
- }
306
- const locale = this.locale;
307
- const dropdownView = createDropdown(locale);
308
- if (!label) {
309
- /**
310
- * A dropdown definition in the toolbar configuration is missing a text label.
311
- *
312
- * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies.
313
- * Make sure the `label` property is set in your drop-down configuration:
314
- *
315
- * ```json
316
- * {
317
- * label: 'A human-readable label',
318
- * icon: '...',
319
- * items: [ ... ]
320
- * },
321
- * ```
322
- *
323
- * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
324
- *
325
- * @error toolbarview-nested-toolbar-dropdown-missing-label
326
- */
327
- logWarning('toolbarview-nested-toolbar-dropdown-missing-label', definition);
328
- }
329
- dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown';
330
- dropdownView.buttonView.set({
331
- label,
332
- tooltip,
333
- withText: !!withText
334
- });
335
- // Allow disabling icon by passing false.
336
- if (icon !== false) {
337
- // A pre-defined icon picked by name, SVG string, a fallback (default) icon.
338
- dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[icon] || icon || threeVerticalDots;
339
- }
340
- // If the icon is disabled, display the label automatically.
341
- else {
342
- dropdownView.buttonView.withText = true;
343
- }
344
- addToolbarToDropdown(dropdownView, () => (dropdownView.toolbarView._buildItemsFromConfig(items, componentFactory, removeItems)));
345
- return dropdownView;
346
- }
347
- }
348
- /**
349
- * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
350
- * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
351
- */
352
- class ItemsView extends View {
353
- /**
354
- * @inheritDoc
355
- */
356
- constructor(locale) {
357
- super(locale);
358
- this.children = this.createCollection();
359
- this.setTemplate({
360
- tag: 'div',
361
- attributes: {
362
- class: [
363
- 'ck',
364
- 'ck-toolbar__items'
365
- ]
366
- },
367
- children: this.children
368
- });
369
- }
370
- }
371
- /**
372
- * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
373
- * At the same time, it also makes it possible to display a toolbar with a vertical layout
374
- * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
375
- */
376
- class StaticLayout {
377
- /**
378
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
379
- * behavior.
380
- *
381
- * @param view An instance of the toolbar that this behavior is added to.
382
- */
383
- constructor(view) {
384
- const bind = view.bindTemplate;
385
- // Static toolbar can be vertical when needed.
386
- view.set('isVertical', false);
387
- // 1:1 pass–through binding, all ToolbarView#items are visible.
388
- view.itemsView.children.bindTo(view.items).using(item => item);
389
- // 1:1 pass–through binding, all ToolbarView#items are focusable.
390
- view.focusables.bindTo(view.items).using(item => item);
391
- view.extendTemplate({
392
- attributes: {
393
- class: [
394
- // When vertical, the toolbar has an additional CSS class.
395
- bind.if('isVertical', 'ck-toolbar_vertical')
396
- ]
397
- }
398
- });
399
- }
400
- /**
401
- * @inheritDoc
402
- */
403
- render() { }
404
- /**
405
- * @inheritDoc
406
- */
407
- destroy() { }
408
- }
409
- /**
410
- * A toolbar behavior that makes the items respond to changes in the geometry.
411
- *
412
- * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
413
- * that do not fit visually into a single row of the toolbar (due to limited space).
414
- * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
415
- *
416
- * ```
417
- * ┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
418
- * | ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
419
- * | | ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐ | |
420
- * | | | #ungroupedItems | | ToolbarSeparatorView | | #groupedItems | | |
421
- * | | └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘ | |
422
- * | | \---------- only when toolbar items overflow -------/ | |
423
- * | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
424
- * └───────────────────────────────────────────────────────────────────────────────────────────────┘
425
- * ```
426
- */
427
- class DynamicGrouping {
428
- /**
429
- * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
430
- * behavior.
431
- *
432
- * @param view An instance of the toolbar that this behavior is added to.
433
- */
434
- constructor(view) {
435
- /**
436
- * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
437
- * and manage items that do not fit into a single row.
438
- *
439
- * **Note:** Created in {@link #_enableGroupingOnResize}.
440
- *
441
- * @readonly
442
- */
443
- this.resizeObserver = null;
444
- /**
445
- * A cached value of the horizontal padding style used by {@link #_updateGrouping}
446
- * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
447
- * a single toolbar line. This value can be reused between updates because it is unlikely that
448
- * the padding will change and re–using `Window.getComputedStyle()` is expensive.
449
- *
450
- * @readonly
451
- */
452
- this.cachedPadding = null;
453
- /**
454
- * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
455
- * and should be executed immediately the next time the toolbar shows up.
456
- *
457
- * @readonly
458
- */
459
- this.shouldUpdateGroupingOnNextResize = false;
460
- this.view = view;
461
- this.viewChildren = view.children;
462
- this.viewFocusables = view.focusables;
463
- this.viewItemsView = view.itemsView;
464
- this.viewFocusTracker = view.focusTracker;
465
- this.viewLocale = view.locale;
466
- this.ungroupedItems = view.createCollection();
467
- this.groupedItems = view.createCollection();
468
- this.groupedItemsDropdown = this._createGroupedItemsDropdown();
469
- // Only those items that were not grouped are visible to the user.
470
- view.itemsView.children.bindTo(this.ungroupedItems).using(item => item);
471
- // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
472
- this.ungroupedItems.on('change', this._updateFocusCycleableItems.bind(this));
473
- // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
474
- view.children.on('change', this._updateFocusCycleableItems.bind(this));
475
- // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
476
- // represented in either grouped or ungrouped items at the right index.
477
- // In other words #items == concat( #ungroupedItems, #groupedItems )
478
- // (in length and order).
479
- view.items.on('change', (evt, changeData) => {
480
- const index = changeData.index;
481
- const added = Array.from(changeData.added);
482
- // Removing.
483
- for (const removedItem of changeData.removed) {
484
- if (index >= this.ungroupedItems.length) {
485
- this.groupedItems.remove(removedItem);
486
- }
487
- else {
488
- this.ungroupedItems.remove(removedItem);
489
- }
490
- }
491
- // Adding.
492
- for (let currentIndex = index; currentIndex < index + added.length; currentIndex++) {
493
- const addedItem = added[currentIndex - index];
494
- if (currentIndex > this.ungroupedItems.length) {
495
- this.groupedItems.add(addedItem, currentIndex - this.ungroupedItems.length);
496
- }
497
- else {
498
- this.ungroupedItems.add(addedItem, currentIndex);
499
- }
500
- }
501
- // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
502
- // the toolbar to overflow.
503
- // Consequently if removed from grouped or ungrouped items, there is a chance
504
- // some new space is available and we could do some ungrouping.
505
- this._updateGrouping();
506
- });
507
- view.extendTemplate({
508
- attributes: {
509
- class: [
510
- // To group items dynamically, the toolbar needs a dedicated CSS class.
511
- 'ck-toolbar_grouping'
512
- ]
513
- }
514
- });
515
- }
516
- /**
517
- * Enables dynamic items grouping based on the dimensions of the toolbar.
518
- *
519
- * @param view An instance of the toolbar that this behavior is added to.
520
- */
521
- render(view) {
522
- this.viewElement = view.element;
523
- this._enableGroupingOnResize();
524
- this._enableGroupingOnMaxWidthChange(view);
525
- }
526
- /**
527
- * Cleans up the internals used by this behavior.
528
- */
529
- destroy() {
530
- // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
531
- // so let's make sure it's actually destroyed along with the toolbar.
532
- this.groupedItemsDropdown.destroy();
533
- this.resizeObserver.destroy();
534
- }
535
- /**
536
- * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
537
- * and it will move them to the {@link #groupedItems} when it happens.
538
- *
539
- * At the same time, it will also check if there is enough space in the toolbar for the first of the
540
- * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
541
- * without the toolbar wrapping.
542
- */
543
- _updateGrouping() {
544
- // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
545
- // for instance before #render(), or after render but without a parent or a parent detached
546
- // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
547
- // nothing else. This happens, for instance, when the toolbar is detached from DOM and
548
- // some logic adds or removes its #items.
549
- if (!this.viewElement.ownerDocument.body.contains(this.viewElement)) {
550
- return;
551
- }
552
- // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
553
- // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
554
- // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
555
- // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
556
- // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
557
- if (!isVisible(this.viewElement)) {
558
- this.shouldUpdateGroupingOnNextResize = true;
559
- return;
560
- }
561
- // Remember how many items were initially grouped so at the it is possible to figure out if the number
562
- // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
563
- const initialGroupedItemsCount = this.groupedItems.length;
564
- let wereItemsGrouped;
565
- // Group #items as long as some wrap to the next row. This will happen, for instance,
566
- // when the toolbar is getting narrow and there is not enough space to display all items in
567
- // a single row.
568
- while (this._areItemsOverflowing) {
569
- this._groupLastItem();
570
- wereItemsGrouped = true;
571
- }
572
- // If none were grouped now but there were some items already grouped before,
573
- // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
574
- // for instance, the toolbar is stretching and there's more space in it than before.
575
- if (!wereItemsGrouped && this.groupedItems.length) {
576
- // Ungroup items as long as none are overflowing or there are none to ungroup left.
577
- while (this.groupedItems.length && !this._areItemsOverflowing) {
578
- this._ungroupFirstItem();
579
- }
580
- // If the ungrouping ended up with some item wrapping to the next row,
581
- // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
582
- // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
583
- // clean–up is vital for the algorithm.
584
- if (this._areItemsOverflowing) {
585
- this._groupLastItem();
586
- }
587
- }
588
- if (this.groupedItems.length !== initialGroupedItemsCount) {
589
- this.view.fire('groupedItemsUpdate');
590
- }
591
- }
592
- /**
593
- * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
594
- * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
595
- */
596
- get _areItemsOverflowing() {
597
- // An empty toolbar cannot overflow.
598
- if (!this.ungroupedItems.length) {
599
- return false;
600
- }
601
- const element = this.viewElement;
602
- const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
603
- const lastChildRect = new Rect(element.lastChild);
604
- const toolbarRect = new Rect(element);
605
- if (!this.cachedPadding) {
606
- const computedStyle = global.window.getComputedStyle(element);
607
- const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
608
- // parseInt() is essential because of quirky floating point numbers logic and DOM.
609
- // If the padding turned out too big because of that, the grouped items dropdown would
610
- // always look (from the Rect perspective) like it overflows (while it's not).
611
- this.cachedPadding = Number.parseInt(computedStyle[paddingProperty]);
612
- }
613
- if (uiLanguageDirection === 'ltr') {
614
- return lastChildRect.right > toolbarRect.right - this.cachedPadding;
615
- }
616
- else {
617
- return lastChildRect.left < toolbarRect.left + this.cachedPadding;
618
- }
619
- }
620
- /**
621
- * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
622
- * upon resize when there is little space available. Instead, the toolbar items are moved to the
623
- * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
624
- *
625
- * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
626
- * them in the dropdown if necessary. It will also observe the browser window for size changes in
627
- * the future and respond to them by grouping more items or reverting already grouped back, depending
628
- * on the visual space available.
629
- */
630
- _enableGroupingOnResize() {
631
- let previousWidth;
632
- // TODO: Consider debounce.
633
- this.resizeObserver = new ResizeObserver(this.viewElement, entry => {
634
- if (!previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize) {
635
- this.shouldUpdateGroupingOnNextResize = false;
636
- this._updateGrouping();
637
- previousWidth = entry.contentRect.width;
638
- }
639
- });
640
- this._updateGrouping();
641
- }
642
- /**
643
- * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
644
- * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
645
- */
646
- _enableGroupingOnMaxWidthChange(view) {
647
- view.on('change:maxWidth', () => {
648
- this._updateGrouping();
649
- });
650
- }
651
- /**
652
- * When called, it will remove the last item from {@link #ungroupedItems} and move it back
653
- * to the {@link #groupedItems} collection.
654
- *
655
- * The opposite of {@link #_ungroupFirstItem}.
656
- */
657
- _groupLastItem() {
658
- if (!this.groupedItems.length) {
659
- this.viewChildren.add(new ToolbarSeparatorView());
660
- this.viewChildren.add(this.groupedItemsDropdown);
661
- this.viewFocusTracker.add(this.groupedItemsDropdown.element);
662
- }
663
- this.groupedItems.add(this.ungroupedItems.remove(this.ungroupedItems.last), 0);
664
- }
665
- /**
666
- * Moves the very first item belonging to {@link #groupedItems} back
667
- * to the {@link #ungroupedItems} collection.
668
- *
669
- * The opposite of {@link #_groupLastItem}.
670
- */
671
- _ungroupFirstItem() {
672
- this.ungroupedItems.add(this.groupedItems.remove(this.groupedItems.first));
673
- if (!this.groupedItems.length) {
674
- this.viewChildren.remove(this.groupedItemsDropdown);
675
- this.viewChildren.remove(this.viewChildren.last);
676
- this.viewFocusTracker.remove(this.groupedItemsDropdown.element);
677
- }
678
- }
679
- /**
680
- * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
681
- * collection when there is not enough space in the toolbar to display all items in a single row.
682
- */
683
- _createGroupedItemsDropdown() {
684
- const locale = this.viewLocale;
685
- const t = locale.t;
686
- const dropdown = createDropdown(locale);
687
- dropdown.class = 'ck-toolbar__grouped-dropdown';
688
- // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
689
- // (https://github.com/ckeditor/ckeditor5/issues/5608)
690
- dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
691
- addToolbarToDropdown(dropdown, this.groupedItems);
692
- dropdown.buttonView.set({
693
- label: t('Show more items'),
694
- tooltip: true,
695
- tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
696
- icon: threeVerticalDots
697
- });
698
- return dropdown;
699
- }
700
- /**
701
- * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
702
- * collection so it represents the up–to–date state of the UI from the perspective of the user.
703
- *
704
- * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
705
- * it must be subject to focus cycling in the toolbar.
706
- *
707
- * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
708
- * to learn more about the purpose of this method.
709
- */
710
- _updateFocusCycleableItems() {
711
- this.viewFocusables.clear();
712
- this.ungroupedItems.map(item => {
713
- this.viewFocusables.add(item);
714
- });
715
- if (this.groupedItems.length) {
716
- this.viewFocusables.add(this.groupedItemsDropdown);
717
- }
718
- }
719
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/toolbar/toolbarview
7
+ */
8
+ import View from '../view';
9
+ import FocusCycler from '../focuscycler';
10
+ import ToolbarSeparatorView from './toolbarseparatorview';
11
+ import ToolbarLineBreakView from './toolbarlinebreakview';
12
+ import preventDefault from '../bindings/preventdefault';
13
+ import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
14
+ import normalizeToolbarConfig from './normalizetoolbarconfig';
15
+ import { FocusTracker, KeystrokeHandler, Rect, ResizeObserver, global, isVisible, logWarning } from '@ckeditor/ckeditor5-utils';
16
+ import { icons } from '@ckeditor/ckeditor5-core';
17
+ import { isObject } from 'lodash-es';
18
+ import '../../theme/components/toolbar/toolbar.css';
19
+ const { threeVerticalDots } = icons;
20
+ export const NESTED_TOOLBAR_ICONS = {
21
+ alignLeft: icons.alignLeft,
22
+ bold: icons.bold,
23
+ importExport: icons.importExport,
24
+ paragraph: icons.paragraph,
25
+ plus: icons.plus,
26
+ text: icons.text,
27
+ threeVerticalDots: icons.threeVerticalDots,
28
+ pilcrow: icons.pilcrow,
29
+ dragIndicator: icons.dragIndicator
30
+ };
31
+ /**
32
+ * The toolbar view class.
33
+ */
34
+ export default class ToolbarView extends View {
35
+ /**
36
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
37
+ *
38
+ * Also see {@link #render}.
39
+ *
40
+ * @param locale The localization services instance.
41
+ * @param options Configuration options of the toolbar.
42
+ */
43
+ constructor(locale, options) {
44
+ super(locale);
45
+ const bind = this.bindTemplate;
46
+ const t = this.t;
47
+ this.options = options || {};
48
+ this.set('ariaLabel', t('Editor toolbar'));
49
+ this.set('maxWidth', 'auto');
50
+ this.items = this.createCollection();
51
+ this.focusTracker = new FocusTracker();
52
+ this.keystrokes = new KeystrokeHandler();
53
+ this.set('class', undefined);
54
+ this.set('isCompact', false);
55
+ this.itemsView = new ItemsView(locale);
56
+ this.children = this.createCollection();
57
+ this.children.add(this.itemsView);
58
+ this.focusables = this.createCollection();
59
+ const isRtl = locale.uiLanguageDirection === 'rtl';
60
+ this._focusCycler = new FocusCycler({
61
+ focusables: this.focusables,
62
+ focusTracker: this.focusTracker,
63
+ keystrokeHandler: this.keystrokes,
64
+ actions: {
65
+ // Navigate toolbar items backwards using the arrow[left,up] keys.
66
+ focusPrevious: [isRtl ? 'arrowright' : 'arrowleft', 'arrowup'],
67
+ // Navigate toolbar items forwards using the arrow[right,down] keys.
68
+ focusNext: [isRtl ? 'arrowleft' : 'arrowright', 'arrowdown']
69
+ }
70
+ });
71
+ const classes = [
72
+ 'ck',
73
+ 'ck-toolbar',
74
+ bind.to('class'),
75
+ bind.if('isCompact', 'ck-toolbar_compact')
76
+ ];
77
+ if (this.options.shouldGroupWhenFull && this.options.isFloating) {
78
+ classes.push('ck-toolbar_floating');
79
+ }
80
+ this.setTemplate({
81
+ tag: 'div',
82
+ attributes: {
83
+ class: classes,
84
+ role: 'toolbar',
85
+ 'aria-label': bind.to('ariaLabel'),
86
+ style: {
87
+ maxWidth: bind.to('maxWidth')
88
+ },
89
+ tabindex: -1
90
+ },
91
+ children: this.children,
92
+ on: {
93
+ // https://github.com/ckeditor/ckeditor5-ui/issues/206
94
+ mousedown: preventDefault(this)
95
+ }
96
+ });
97
+ this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping(this) : new StaticLayout(this);
98
+ }
99
+ /**
100
+ * @inheritDoc
101
+ */
102
+ render() {
103
+ super.render();
104
+ this.focusTracker.add(this.element);
105
+ // Children added before rendering should be known to the #focusTracker.
106
+ for (const item of this.items) {
107
+ this.focusTracker.add(item.element);
108
+ }
109
+ this.items.on('add', (evt, item) => {
110
+ this.focusTracker.add(item.element);
111
+ });
112
+ this.items.on('remove', (evt, item) => {
113
+ this.focusTracker.remove(item.element);
114
+ });
115
+ // Start listening for the keystrokes coming from #element.
116
+ this.keystrokes.listenTo(this.element);
117
+ this._behavior.render(this);
118
+ }
119
+ /**
120
+ * @inheritDoc
121
+ */
122
+ destroy() {
123
+ this._behavior.destroy();
124
+ this.focusTracker.destroy();
125
+ this.keystrokes.destroy();
126
+ return super.destroy();
127
+ }
128
+ /**
129
+ * Focuses the first focusable in {@link #focusables}.
130
+ */
131
+ focus() {
132
+ this._focusCycler.focusFirst();
133
+ }
134
+ /**
135
+ * Focuses the last focusable in {@link #focusables}.
136
+ */
137
+ focusLast() {
138
+ this._focusCycler.focusLast();
139
+ }
140
+ /**
141
+ * A utility that expands the plain toolbar configuration into
142
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
143
+ *
144
+ * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
145
+ * @param factory A factory producing toolbar items.
146
+ * @param removeItems An array of items names to be removed from the configuration. When present, applies
147
+ * to this toolbar and all nested ones as well.
148
+ */
149
+ fillFromConfig(itemsOrConfig, factory, removeItems) {
150
+ this.items.addMany(this._buildItemsFromConfig(itemsOrConfig, factory, removeItems));
151
+ }
152
+ /**
153
+ * A utility that expands the plain toolbar configuration into a list of view items using a given component factory.
154
+ *
155
+ * @param itemsOrConfig The toolbar items or the entire toolbar configuration object.
156
+ * @param factory A factory producing toolbar items.
157
+ * @param removeItems An array of items names to be removed from the configuration. When present, applies
158
+ * to this toolbar and all nested ones as well.
159
+ */
160
+ _buildItemsFromConfig(itemsOrConfig, factory, removeItems) {
161
+ const config = normalizeToolbarConfig(itemsOrConfig);
162
+ const normalizedRemoveItems = removeItems || config.removeItems;
163
+ const itemsToAdd = this._cleanItemsConfiguration(config.items, factory, normalizedRemoveItems)
164
+ .map(item => {
165
+ if (isObject(item)) {
166
+ return this._createNestedToolbarDropdown(item, factory, normalizedRemoveItems);
167
+ }
168
+ else if (item === '|') {
169
+ return new ToolbarSeparatorView();
170
+ }
171
+ else if (item === '-') {
172
+ return new ToolbarLineBreakView();
173
+ }
174
+ return factory.create(item);
175
+ })
176
+ .filter((item) => !!item);
177
+ return itemsToAdd;
178
+ }
179
+ /**
180
+ * Cleans up the {@link module:ui/toolbar/toolbarview~ToolbarView#items} of the toolbar by removing unwanted items and
181
+ * duplicated (obsolete) separators or line breaks.
182
+ *
183
+ * @param items The toolbar items configuration.
184
+ * @param factory A factory producing toolbar items.
185
+ * @param removeItems An array of items names to be removed from the configuration.
186
+ * @returns Items after the clean-up.
187
+ */
188
+ _cleanItemsConfiguration(items, factory, removeItems) {
189
+ const filteredItems = items
190
+ .filter((item, idx, items) => {
191
+ if (item === '|') {
192
+ return true;
193
+ }
194
+ // Items listed in `config.removeItems` should not be added to the toolbar.
195
+ if (removeItems.indexOf(item) !== -1) {
196
+ return false;
197
+ }
198
+ if (item === '-') {
199
+ // The toolbar line breaks must not be rendered when toolbar grouping is enabled.
200
+ // (https://github.com/ckeditor/ckeditor5/issues/8582)
201
+ if (this.options.shouldGroupWhenFull) {
202
+ /**
203
+ * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
204
+ * is disabled in the toolbar configuration.
205
+ * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
206
+ *
207
+ * ```ts
208
+ * const config = {
209
+ * toolbar: {
210
+ * items: [ ... ],
211
+ * shouldNotGroupWhenFull: true
212
+ * }
213
+ * }
214
+ * ```
215
+ *
216
+ * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
217
+ *
218
+ * @error toolbarview-line-break-ignored-when-grouping-items
219
+ */
220
+ logWarning('toolbarview-line-break-ignored-when-grouping-items', items);
221
+ return false;
222
+ }
223
+ return true;
224
+ }
225
+ // For the items that cannot be instantiated we are sending warning message. We also filter them out.
226
+ if (!isObject(item) && !factory.has(item)) {
227
+ /**
228
+ * There was a problem processing the configuration of the toolbar. The item with the given
229
+ * name does not exist so it was omitted when rendering the toolbar.
230
+ *
231
+ * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
232
+ * to provide a toolbar item has not been loaded or there is a typo in the configuration.
233
+ *
234
+ * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
235
+ * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
236
+ *
237
+ * You can use the following snippet to retrieve all available toolbar items:
238
+ *
239
+ * ```ts
240
+ * Array.from( editor.ui.componentFactory.names() );
241
+ * ```
242
+ *
243
+ * @error toolbarview-item-unavailable
244
+ * @param item The name of the component or nested toolbar definition.
245
+ */
246
+ logWarning('toolbarview-item-unavailable', { item });
247
+ return false;
248
+ }
249
+ return true;
250
+ });
251
+ return this._cleanSeparatorsAndLineBreaks(filteredItems);
252
+ }
253
+ /**
254
+ * Remove leading, trailing, and duplicated separators (`-` and `|`).
255
+ *
256
+ * @returns Toolbar items after the separator and line break clean-up.
257
+ */
258
+ _cleanSeparatorsAndLineBreaks(items) {
259
+ const nonSeparatorPredicate = (item) => (item !== '-' && item !== '|');
260
+ const count = items.length;
261
+ // Find an index of the first item that is not a separator.
262
+ const firstCommandItemIndex = items.findIndex(nonSeparatorPredicate);
263
+ // Items include separators only. There is no point in displaying them.
264
+ if (firstCommandItemIndex === -1) {
265
+ return [];
266
+ }
267
+ // Search from the end of the list, then convert found index back to the original direction.
268
+ const lastCommandItemIndex = count - items
269
+ .slice()
270
+ .reverse()
271
+ .findIndex(nonSeparatorPredicate);
272
+ return items
273
+ // Return items without the leading and trailing separators.
274
+ .slice(firstCommandItemIndex, lastCommandItemIndex)
275
+ // Remove duplicated separators.
276
+ .filter((name, idx, items) => {
277
+ // Filter only separators.
278
+ if (nonSeparatorPredicate(name)) {
279
+ return true;
280
+ }
281
+ const isDuplicated = idx > 0 && items[idx - 1] === name;
282
+ return !isDuplicated;
283
+ });
284
+ }
285
+ /**
286
+ * Creates a user-defined dropdown containing a toolbar with items.
287
+ *
288
+ * @param definition A definition of the nested toolbar dropdown.
289
+ * @param definition.label A label of the dropdown.
290
+ * @param definition.icon An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft',
291
+ * 'paragraph' or an SVG string. When `false` is passed, no icon will be used.
292
+ * @param definition.withText When set `true`, the label of the dropdown will be visible. See
293
+ * {@link module:ui/button/buttonview~ButtonView#withText} to learn more.
294
+ * @param definition.tooltip A tooltip of the dropdown button. See
295
+ * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more. Defaults to `true`.
296
+ * @param componentFactory Component factory used to create items
297
+ * of the nested toolbar.
298
+ */
299
+ _createNestedToolbarDropdown(definition, componentFactory, removeItems) {
300
+ let { label, icon, items, tooltip = true, withText = false } = definition;
301
+ items = this._cleanItemsConfiguration(items, componentFactory, removeItems);
302
+ // There is no point in rendering a dropdown without items.
303
+ if (!items.length) {
304
+ return null;
305
+ }
306
+ const locale = this.locale;
307
+ const dropdownView = createDropdown(locale);
308
+ if (!label) {
309
+ /**
310
+ * A dropdown definition in the toolbar configuration is missing a text label.
311
+ *
312
+ * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies.
313
+ * Make sure the `label` property is set in your drop-down configuration:
314
+ *
315
+ * ```json
316
+ * {
317
+ * label: 'A human-readable label',
318
+ * icon: '...',
319
+ * items: [ ... ]
320
+ * },
321
+ * ```
322
+ *
323
+ * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
324
+ *
325
+ * @error toolbarview-nested-toolbar-dropdown-missing-label
326
+ */
327
+ logWarning('toolbarview-nested-toolbar-dropdown-missing-label', definition);
328
+ }
329
+ dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown';
330
+ dropdownView.buttonView.set({
331
+ label,
332
+ tooltip,
333
+ withText: !!withText
334
+ });
335
+ // Allow disabling icon by passing false.
336
+ if (icon !== false) {
337
+ // A pre-defined icon picked by name, SVG string, a fallback (default) icon.
338
+ dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[icon] || icon || threeVerticalDots;
339
+ }
340
+ // If the icon is disabled, display the label automatically.
341
+ else {
342
+ dropdownView.buttonView.withText = true;
343
+ }
344
+ addToolbarToDropdown(dropdownView, () => (dropdownView.toolbarView._buildItemsFromConfig(items, componentFactory, removeItems)));
345
+ return dropdownView;
346
+ }
347
+ }
348
+ /**
349
+ * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
350
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
351
+ */
352
+ class ItemsView extends View {
353
+ /**
354
+ * @inheritDoc
355
+ */
356
+ constructor(locale) {
357
+ super(locale);
358
+ this.children = this.createCollection();
359
+ this.setTemplate({
360
+ tag: 'div',
361
+ attributes: {
362
+ class: [
363
+ 'ck',
364
+ 'ck-toolbar__items'
365
+ ]
366
+ },
367
+ children: this.children
368
+ });
369
+ }
370
+ }
371
+ /**
372
+ * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
373
+ * At the same time, it also makes it possible to display a toolbar with a vertical layout
374
+ * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
375
+ */
376
+ class StaticLayout {
377
+ /**
378
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
379
+ * behavior.
380
+ *
381
+ * @param view An instance of the toolbar that this behavior is added to.
382
+ */
383
+ constructor(view) {
384
+ const bind = view.bindTemplate;
385
+ // Static toolbar can be vertical when needed.
386
+ view.set('isVertical', false);
387
+ // 1:1 pass–through binding, all ToolbarView#items are visible.
388
+ view.itemsView.children.bindTo(view.items).using(item => item);
389
+ // 1:1 pass–through binding, all ToolbarView#items are focusable.
390
+ view.focusables.bindTo(view.items).using(item => item);
391
+ view.extendTemplate({
392
+ attributes: {
393
+ class: [
394
+ // When vertical, the toolbar has an additional CSS class.
395
+ bind.if('isVertical', 'ck-toolbar_vertical')
396
+ ]
397
+ }
398
+ });
399
+ }
400
+ /**
401
+ * @inheritDoc
402
+ */
403
+ render() { }
404
+ /**
405
+ * @inheritDoc
406
+ */
407
+ destroy() { }
408
+ }
409
+ /**
410
+ * A toolbar behavior that makes the items respond to changes in the geometry.
411
+ *
412
+ * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
413
+ * that do not fit visually into a single row of the toolbar (due to limited space).
414
+ * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
415
+ *
416
+ * ```
417
+ * ┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
418
+ * | ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
419
+ * | | ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐ | |
420
+ * | | | #ungroupedItems | | ToolbarSeparatorView | | #groupedItems | | |
421
+ * | | └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘ | |
422
+ * | | \---------- only when toolbar items overflow -------/ | |
423
+ * | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
424
+ * └───────────────────────────────────────────────────────────────────────────────────────────────┘
425
+ * ```
426
+ */
427
+ class DynamicGrouping {
428
+ /**
429
+ * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
430
+ * behavior.
431
+ *
432
+ * @param view An instance of the toolbar that this behavior is added to.
433
+ */
434
+ constructor(view) {
435
+ /**
436
+ * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
437
+ * and manage items that do not fit into a single row.
438
+ *
439
+ * **Note:** Created in {@link #_enableGroupingOnResize}.
440
+ *
441
+ * @readonly
442
+ */
443
+ this.resizeObserver = null;
444
+ /**
445
+ * A cached value of the horizontal padding style used by {@link #_updateGrouping}
446
+ * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
447
+ * a single toolbar line. This value can be reused between updates because it is unlikely that
448
+ * the padding will change and re–using `Window.getComputedStyle()` is expensive.
449
+ *
450
+ * @readonly
451
+ */
452
+ this.cachedPadding = null;
453
+ /**
454
+ * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
455
+ * and should be executed immediately the next time the toolbar shows up.
456
+ *
457
+ * @readonly
458
+ */
459
+ this.shouldUpdateGroupingOnNextResize = false;
460
+ this.view = view;
461
+ this.viewChildren = view.children;
462
+ this.viewFocusables = view.focusables;
463
+ this.viewItemsView = view.itemsView;
464
+ this.viewFocusTracker = view.focusTracker;
465
+ this.viewLocale = view.locale;
466
+ this.ungroupedItems = view.createCollection();
467
+ this.groupedItems = view.createCollection();
468
+ this.groupedItemsDropdown = this._createGroupedItemsDropdown();
469
+ // Only those items that were not grouped are visible to the user.
470
+ view.itemsView.children.bindTo(this.ungroupedItems).using(item => item);
471
+ // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
472
+ this.ungroupedItems.on('change', this._updateFocusCycleableItems.bind(this));
473
+ // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
474
+ view.children.on('change', this._updateFocusCycleableItems.bind(this));
475
+ // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
476
+ // represented in either grouped or ungrouped items at the right index.
477
+ // In other words #items == concat( #ungroupedItems, #groupedItems )
478
+ // (in length and order).
479
+ view.items.on('change', (evt, changeData) => {
480
+ const index = changeData.index;
481
+ const added = Array.from(changeData.added);
482
+ // Removing.
483
+ for (const removedItem of changeData.removed) {
484
+ if (index >= this.ungroupedItems.length) {
485
+ this.groupedItems.remove(removedItem);
486
+ }
487
+ else {
488
+ this.ungroupedItems.remove(removedItem);
489
+ }
490
+ }
491
+ // Adding.
492
+ for (let currentIndex = index; currentIndex < index + added.length; currentIndex++) {
493
+ const addedItem = added[currentIndex - index];
494
+ if (currentIndex > this.ungroupedItems.length) {
495
+ this.groupedItems.add(addedItem, currentIndex - this.ungroupedItems.length);
496
+ }
497
+ else {
498
+ this.ungroupedItems.add(addedItem, currentIndex);
499
+ }
500
+ }
501
+ // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
502
+ // the toolbar to overflow.
503
+ // Consequently if removed from grouped or ungrouped items, there is a chance
504
+ // some new space is available and we could do some ungrouping.
505
+ this._updateGrouping();
506
+ });
507
+ view.extendTemplate({
508
+ attributes: {
509
+ class: [
510
+ // To group items dynamically, the toolbar needs a dedicated CSS class.
511
+ 'ck-toolbar_grouping'
512
+ ]
513
+ }
514
+ });
515
+ }
516
+ /**
517
+ * Enables dynamic items grouping based on the dimensions of the toolbar.
518
+ *
519
+ * @param view An instance of the toolbar that this behavior is added to.
520
+ */
521
+ render(view) {
522
+ this.viewElement = view.element;
523
+ this._enableGroupingOnResize();
524
+ this._enableGroupingOnMaxWidthChange(view);
525
+ }
526
+ /**
527
+ * Cleans up the internals used by this behavior.
528
+ */
529
+ destroy() {
530
+ // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
531
+ // so let's make sure it's actually destroyed along with the toolbar.
532
+ this.groupedItemsDropdown.destroy();
533
+ this.resizeObserver.destroy();
534
+ }
535
+ /**
536
+ * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
537
+ * and it will move them to the {@link #groupedItems} when it happens.
538
+ *
539
+ * At the same time, it will also check if there is enough space in the toolbar for the first of the
540
+ * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
541
+ * without the toolbar wrapping.
542
+ */
543
+ _updateGrouping() {
544
+ // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
545
+ // for instance before #render(), or after render but without a parent or a parent detached
546
+ // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
547
+ // nothing else. This happens, for instance, when the toolbar is detached from DOM and
548
+ // some logic adds or removes its #items.
549
+ if (!this.viewElement.ownerDocument.body.contains(this.viewElement)) {
550
+ return;
551
+ }
552
+ // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
553
+ // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
554
+ // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
555
+ // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
556
+ // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
557
+ if (!isVisible(this.viewElement)) {
558
+ this.shouldUpdateGroupingOnNextResize = true;
559
+ return;
560
+ }
561
+ // Remember how many items were initially grouped so at the it is possible to figure out if the number
562
+ // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
563
+ const initialGroupedItemsCount = this.groupedItems.length;
564
+ let wereItemsGrouped;
565
+ // Group #items as long as some wrap to the next row. This will happen, for instance,
566
+ // when the toolbar is getting narrow and there is not enough space to display all items in
567
+ // a single row.
568
+ while (this._areItemsOverflowing) {
569
+ this._groupLastItem();
570
+ wereItemsGrouped = true;
571
+ }
572
+ // If none were grouped now but there were some items already grouped before,
573
+ // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
574
+ // for instance, the toolbar is stretching and there's more space in it than before.
575
+ if (!wereItemsGrouped && this.groupedItems.length) {
576
+ // Ungroup items as long as none are overflowing or there are none to ungroup left.
577
+ while (this.groupedItems.length && !this._areItemsOverflowing) {
578
+ this._ungroupFirstItem();
579
+ }
580
+ // If the ungrouping ended up with some item wrapping to the next row,
581
+ // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
582
+ // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
583
+ // clean–up is vital for the algorithm.
584
+ if (this._areItemsOverflowing) {
585
+ this._groupLastItem();
586
+ }
587
+ }
588
+ if (this.groupedItems.length !== initialGroupedItemsCount) {
589
+ this.view.fire('groupedItemsUpdate');
590
+ }
591
+ }
592
+ /**
593
+ * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
594
+ * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
595
+ */
596
+ get _areItemsOverflowing() {
597
+ // An empty toolbar cannot overflow.
598
+ if (!this.ungroupedItems.length) {
599
+ return false;
600
+ }
601
+ const element = this.viewElement;
602
+ const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
603
+ const lastChildRect = new Rect(element.lastChild);
604
+ const toolbarRect = new Rect(element);
605
+ if (!this.cachedPadding) {
606
+ const computedStyle = global.window.getComputedStyle(element);
607
+ const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
608
+ // parseInt() is essential because of quirky floating point numbers logic and DOM.
609
+ // If the padding turned out too big because of that, the grouped items dropdown would
610
+ // always look (from the Rect perspective) like it overflows (while it's not).
611
+ this.cachedPadding = Number.parseInt(computedStyle[paddingProperty]);
612
+ }
613
+ if (uiLanguageDirection === 'ltr') {
614
+ return lastChildRect.right > toolbarRect.right - this.cachedPadding;
615
+ }
616
+ else {
617
+ return lastChildRect.left < toolbarRect.left + this.cachedPadding;
618
+ }
619
+ }
620
+ /**
621
+ * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
622
+ * upon resize when there is little space available. Instead, the toolbar items are moved to the
623
+ * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
624
+ *
625
+ * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
626
+ * them in the dropdown if necessary. It will also observe the browser window for size changes in
627
+ * the future and respond to them by grouping more items or reverting already grouped back, depending
628
+ * on the visual space available.
629
+ */
630
+ _enableGroupingOnResize() {
631
+ let previousWidth;
632
+ // TODO: Consider debounce.
633
+ this.resizeObserver = new ResizeObserver(this.viewElement, entry => {
634
+ if (!previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize) {
635
+ this.shouldUpdateGroupingOnNextResize = false;
636
+ this._updateGrouping();
637
+ previousWidth = entry.contentRect.width;
638
+ }
639
+ });
640
+ this._updateGrouping();
641
+ }
642
+ /**
643
+ * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
644
+ * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
645
+ */
646
+ _enableGroupingOnMaxWidthChange(view) {
647
+ view.on('change:maxWidth', () => {
648
+ this._updateGrouping();
649
+ });
650
+ }
651
+ /**
652
+ * When called, it will remove the last item from {@link #ungroupedItems} and move it back
653
+ * to the {@link #groupedItems} collection.
654
+ *
655
+ * The opposite of {@link #_ungroupFirstItem}.
656
+ */
657
+ _groupLastItem() {
658
+ if (!this.groupedItems.length) {
659
+ this.viewChildren.add(new ToolbarSeparatorView());
660
+ this.viewChildren.add(this.groupedItemsDropdown);
661
+ this.viewFocusTracker.add(this.groupedItemsDropdown.element);
662
+ }
663
+ this.groupedItems.add(this.ungroupedItems.remove(this.ungroupedItems.last), 0);
664
+ }
665
+ /**
666
+ * Moves the very first item belonging to {@link #groupedItems} back
667
+ * to the {@link #ungroupedItems} collection.
668
+ *
669
+ * The opposite of {@link #_groupLastItem}.
670
+ */
671
+ _ungroupFirstItem() {
672
+ this.ungroupedItems.add(this.groupedItems.remove(this.groupedItems.first));
673
+ if (!this.groupedItems.length) {
674
+ this.viewChildren.remove(this.groupedItemsDropdown);
675
+ this.viewChildren.remove(this.viewChildren.last);
676
+ this.viewFocusTracker.remove(this.groupedItemsDropdown.element);
677
+ }
678
+ }
679
+ /**
680
+ * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
681
+ * collection when there is not enough space in the toolbar to display all items in a single row.
682
+ */
683
+ _createGroupedItemsDropdown() {
684
+ const locale = this.viewLocale;
685
+ const t = locale.t;
686
+ const dropdown = createDropdown(locale);
687
+ dropdown.class = 'ck-toolbar__grouped-dropdown';
688
+ // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
689
+ // (https://github.com/ckeditor/ckeditor5/issues/5608)
690
+ dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
691
+ addToolbarToDropdown(dropdown, this.groupedItems);
692
+ dropdown.buttonView.set({
693
+ label: t('Show more items'),
694
+ tooltip: true,
695
+ tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
696
+ icon: threeVerticalDots
697
+ });
698
+ return dropdown;
699
+ }
700
+ /**
701
+ * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
702
+ * collection so it represents the up–to–date state of the UI from the perspective of the user.
703
+ *
704
+ * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
705
+ * it must be subject to focus cycling in the toolbar.
706
+ *
707
+ * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
708
+ * to learn more about the purpose of this method.
709
+ */
710
+ _updateFocusCycleableItems() {
711
+ this.viewFocusables.clear();
712
+ this.ungroupedItems.map(item => {
713
+ this.viewFocusables.add(item);
714
+ });
715
+ if (this.groupedItems.length) {
716
+ this.viewFocusables.add(this.groupedItemsDropdown);
717
+ }
718
+ }
719
+ }