@ckeditor/ckeditor5-ui 35.2.1 → 35.3.1

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,33 +2,23 @@
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/block/blocktoolbar
8
7
  */
9
-
10
8
  /* global window */
11
-
12
9
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
13
10
  import pilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
14
-
15
11
  import BlockButtonView from './blockbuttonview';
16
12
  import BalloonPanelView from '../../panel/balloon/balloonpanelview';
17
13
  import ToolbarView from '../toolbarview';
18
-
19
14
  import clickOutsideHandler from '../../bindings/clickoutsidehandler';
20
-
21
15
  import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
22
16
  import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
23
17
  import normalizeToolbarConfig from '../normalizetoolbarconfig';
24
-
25
18
  import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
26
-
27
19
  import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
28
20
  import env from '@ckeditor/ckeditor5-utils/src/env';
29
-
30
- const toPx = toUnit( 'px' );
31
-
21
+ const toPx = toUnit('px');
32
22
  /**
33
23
  * The block toolbar plugin.
34
24
  *
@@ -71,470 +61,378 @@ const toPx = toUnit( 'px' );
71
61
  * @extends module:core/plugin~Plugin
72
62
  */
73
63
  export default class BlockToolbar extends Plugin {
74
- /**
75
- * @inheritDoc
76
- */
77
- static get pluginName() {
78
- return 'BlockToolbar';
79
- }
80
-
81
- /**
82
- * @inheritDoc
83
- */
84
- constructor( editor ) {
85
- super( editor );
86
-
87
- /**
88
- * A cached and normalized `config.blockToolbar` object.
89
- *
90
- * @type {module:core/editor/editorconfig~EditorConfig#blockToolbar}
91
- * @private
92
- */
93
- this._blockToolbarConfig = normalizeToolbarConfig( this.editor.config.get( 'blockToolbar' ) );
94
-
95
- /**
96
- * The toolbar view.
97
- *
98
- * @type {module:ui/toolbar/toolbarview~ToolbarView}
99
- */
100
- this.toolbarView = this._createToolbarView();
101
-
102
- /**
103
- * The balloon panel view, containing the {@link #toolbarView}.
104
- *
105
- * @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
106
- */
107
- this.panelView = this._createPanelView();
108
-
109
- /**
110
- * The button view that opens the {@link #toolbarView}.
111
- *
112
- * @type {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
113
- */
114
- this.buttonView = this._createButtonView();
115
-
116
- /**
117
- * An instance of the resize observer that allows to respond to changes in editable's geometry
118
- * so the toolbar can stay within its boundaries (and group toolbar items that do not fit).
119
- *
120
- * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the
121
- * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar configuration}.
122
- *
123
- * **Note:** Created in {@link #afterInit}.
124
- *
125
- * @protected
126
- * @member {module:utils/dom/resizeobserver~ResizeObserver}
127
- */
128
- this._resizeObserver = null;
129
-
130
- // Close the #panelView upon clicking outside of the plugin UI.
131
- clickOutsideHandler( {
132
- emitter: this.panelView,
133
- contextElements: [ this.panelView.element, this.buttonView.element ],
134
- activator: () => this.panelView.isVisible,
135
- callback: () => this._hidePanel()
136
- } );
137
- }
138
-
139
- /**
140
- * @inheritDoc
141
- */
142
- init() {
143
- const editor = this.editor;
144
-
145
- // Hides panel on a direct selection change.
146
- this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
147
- if ( data.directChange ) {
148
- this._hidePanel();
149
- }
150
- } );
151
-
152
- this.listenTo( editor.ui, 'update', () => this._updateButton() );
153
- // `low` priority is used because of https://github.com/ckeditor/ckeditor5-core/issues/133.
154
- this.listenTo( editor, 'change:isReadOnly', () => this._updateButton(), { priority: 'low' } );
155
- this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => this._updateButton() );
156
-
157
- // Reposition button on resize.
158
- this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
159
- if ( isVisible ) {
160
- // Keep correct position of button and panel on window#resize.
161
- this.buttonView.listenTo( window, 'resize', () => this._updateButton() );
162
- } else {
163
- // Stop repositioning button when is hidden.
164
- this.buttonView.stopListening( window, 'resize' );
165
-
166
- // Hide the panel when the button disappears.
167
- this._hidePanel();
168
- }
169
- } );
170
-
171
- // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
172
- editor.ui.addToolbar( this.toolbarView, {
173
- beforeFocus: () => this._showPanel(),
174
- afterBlur: () => this._hidePanel()
175
- } );
176
- }
177
-
178
- /**
179
- * Fills the toolbar with its items based on the configuration.
180
- *
181
- * **Note:** This needs to be done after all plugins are ready.
182
- *
183
- * @inheritDoc
184
- */
185
- afterInit() {
186
- const factory = this.editor.ui.componentFactory;
187
- const config = this._blockToolbarConfig;
188
-
189
- this.toolbarView.fillFromConfig( config, factory );
190
-
191
- // Hide panel before executing each button in the panel.
192
- for ( const item of this.toolbarView.items ) {
193
- item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
194
- }
195
-
196
- if ( !config.shouldNotGroupWhenFull ) {
197
- this.listenTo( this.editor, 'ready', () => {
198
- const editableElement = this.editor.ui.view.editable.element;
199
-
200
- // Set #toolbarView's max-width just after the initialization and update it on the editable resize.
201
- this._resizeObserver = new ResizeObserver( editableElement, () => {
202
- this.toolbarView.maxWidth = this._getToolbarMaxWidth();
203
- } );
204
- } );
205
- }
206
- }
207
-
208
- /**
209
- * @inheritDoc
210
- */
211
- destroy() {
212
- super.destroy();
213
-
214
- // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
215
- this.panelView.destroy();
216
- this.buttonView.destroy();
217
- this.toolbarView.destroy();
218
-
219
- if ( this._resizeObserver ) {
220
- this._resizeObserver.destroy();
221
- }
222
- }
223
-
224
- /**
225
- * Creates the {@link #toolbarView}.
226
- *
227
- * @private
228
- * @returns {module:ui/toolbar/toolbarview~ToolbarView}
229
- */
230
- _createToolbarView() {
231
- const t = this.editor.locale.t;
232
- const shouldGroupWhenFull = !this._blockToolbarConfig.shouldNotGroupWhenFull;
233
- const toolbarView = new ToolbarView( this.editor.locale, {
234
- shouldGroupWhenFull,
235
- isFloating: true
236
- } );
237
-
238
- toolbarView.ariaLabel = t( 'Editor block content toolbar' );
239
-
240
- // When toolbar lost focus then panel should hide.
241
- toolbarView.focusTracker.on( 'change:isFocused', ( evt, name, is ) => {
242
- if ( !is ) {
243
- this._hidePanel();
244
- }
245
- } );
246
-
247
- return toolbarView;
248
- }
249
-
250
- /**
251
- * Creates the {@link #panelView}.
252
- *
253
- * @private
254
- * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
255
- */
256
- _createPanelView() {
257
- const editor = this.editor;
258
- const panelView = new BalloonPanelView( editor.locale );
259
-
260
- panelView.content.add( this.toolbarView );
261
- panelView.class = 'ck-toolbar-container';
262
- editor.ui.view.body.add( panelView );
263
- editor.ui.focusTracker.add( panelView.element );
264
-
265
- // Close #panelView on `Esc` press.
266
- this.toolbarView.keystrokes.set( 'Esc', ( evt, cancel ) => {
267
- this._hidePanel( true );
268
- cancel();
269
- } );
270
-
271
- return panelView;
272
- }
273
-
274
- /**
275
- * Creates the {@link #buttonView}.
276
- *
277
- * @private
278
- * @returns {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
279
- */
280
- _createButtonView() {
281
- const editor = this.editor;
282
- const t = editor.t;
283
- const buttonView = new BlockButtonView( editor.locale );
284
- const bind = buttonView.bindTemplate;
285
-
286
- buttonView.set( {
287
- label: t( 'Edit block' ),
288
- icon: pilcrow,
289
- withText: false
290
- } );
291
-
292
- // Note that this piece over here overrides the default mousedown logic in ButtonView
293
- // to make it work with BlockToolbar. See the implementation of the ButtonView class to learn more.
294
- buttonView.extendTemplate( {
295
- on: {
296
- mousedown: bind.to( evt => {
297
- // On Safari we have to force the focus on a button on click as it's the only browser
298
- // that doesn't do that automatically. See #12115.
299
- if ( env.isSafari && this.panelView.isVisible ) {
300
- this.toolbarView.focus();
301
- }
302
-
303
- // Workaround to #12184, see https://github.com/ckeditor/ckeditor5/issues/12184#issuecomment-1199147964.
304
- evt.preventDefault();
305
- } )
306
- }
307
- } );
308
-
309
- // Bind the panelView observable properties to the buttonView.
310
- buttonView.bind( 'isOn' ).to( this.panelView, 'isVisible' );
311
- buttonView.bind( 'tooltip' ).to( this.panelView, 'isVisible', isVisible => !isVisible );
312
-
313
- // Toggle the panelView upon buttonView#execute.
314
- this.listenTo( buttonView, 'execute', () => {
315
- if ( !this.panelView.isVisible ) {
316
- this._showPanel();
317
- } else {
318
- this._hidePanel( true );
319
- }
320
- } );
321
-
322
- editor.ui.view.body.add( buttonView );
323
- editor.ui.focusTracker.add( buttonView.element );
324
-
325
- return buttonView;
326
- }
327
-
328
- /**
329
- * Shows or hides the button.
330
- * When all the conditions for displaying the button are matched, it shows the button. Hides otherwise.
331
- *
332
- * @private
333
- */
334
- _updateButton() {
335
- const editor = this.editor;
336
- const model = editor.model;
337
- const view = editor.editing.view;
338
-
339
- // Hides the button when the editor is not focused.
340
- if ( !editor.ui.focusTracker.isFocused ) {
341
- this._hideButton();
342
-
343
- return;
344
- }
345
-
346
- // Hides the button when the editor switches to the read-only mode.
347
- if ( editor.isReadOnly ) {
348
- this._hideButton();
349
-
350
- return;
351
- }
352
-
353
- // Get the first selected block, button will be attached to this element.
354
- const modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];
355
-
356
- // Hides the button when there is no enabled item in toolbar for the current block element.
357
- if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
358
- this._hideButton();
359
-
360
- return;
361
- }
362
-
363
- // Get DOM target element.
364
- const domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );
365
-
366
- // Show block button.
367
- this.buttonView.isVisible = true;
368
-
369
- // Attach block button to target DOM element.
370
- this._attachButtonToElement( domTarget );
371
-
372
- // When panel is opened then refresh it position to be properly aligned with block button.
373
- if ( this.panelView.isVisible ) {
374
- this._showPanel();
375
- }
376
- }
377
-
378
- /**
379
- * Hides the button.
380
- *
381
- * @private
382
- */
383
- _hideButton() {
384
- this.buttonView.isVisible = false;
385
- }
386
-
387
- /**
388
- * Shows the {@link #toolbarView} attached to the {@link #buttonView}.
389
- * If the toolbar is already visible, then it simply repositions it.
390
- *
391
- * @private
392
- */
393
- _showPanel() {
394
- // Usually, the only way to show the toolbar is by pressing the block button. It makes it impossible for
395
- // the toolbar to show up when the button is invisible (feature does not make sense for the selection then).
396
- // The toolbar navigation using Alt+F10 does not access the button but shows the panel directly using this method.
397
- // So we need to check whether this is possible first.
398
- if ( !this.buttonView.isVisible ) {
399
- return;
400
- }
401
-
402
- const wasVisible = this.panelView.isVisible;
403
-
404
- // So here's the thing: If there was no initial panelView#show() or these two were in different order, the toolbar
405
- // positioning will break in RTL editors. Weird, right? What you show know is that the toolbar
406
- // grouping works thanks to:
407
- //
408
- // * the ResizeObserver, which kicks in as soon as the toolbar shows up in DOM (becomes visible again).
409
- // * the observable ToolbarView#maxWidth, which triggers re-grouping when changed.
410
- //
411
- // Here are the possible scenarios:
412
- //
413
- // 1. (WRONG ❌) If the #maxWidth is set when the toolbar is invisible, it won't affect item grouping (no DOMRects, no grouping).
414
- // Then, when panelView.pin() is called, the position of the toolbar will be calculated for the old
415
- // items grouping state, and when finally ResizeObserver kicks in (hey, the toolbar is visible now, right?)
416
- // it will group/ungroup some items and the length of the toolbar will change. But since in RTL the toolbar
417
- // is attached on the right side and the positioning uses CSS "left", it will result in the toolbar shifting
418
- // to the left and being displayed in the wrong place.
419
- // 2. (WRONG ❌) If the panelView.pin() is called first and #maxWidth set next, then basically the story repeats. The balloon
420
- // calculates the position for the old toolbar grouping state, then the toolbar re-groups items and because
421
- // it is positioned using CSS "left" it will move.
422
- // 3. (RIGHT ✅) We show the panel first (the toolbar does re-grouping but it does not matter), then the #maxWidth
423
- // is set allowing the toolbar to re-group again and finally panelView.pin() does the positioning when the
424
- // items grouping state is stable and final.
425
- //
426
- // https://github.com/ckeditor/ckeditor5/issues/6449, https://github.com/ckeditor/ckeditor5/issues/6575
427
- this.panelView.show();
428
- this.toolbarView.maxWidth = this._getToolbarMaxWidth();
429
-
430
- this.panelView.pin( {
431
- target: this.buttonView.element,
432
- limiter: this.editor.ui.getEditableElement()
433
- } );
434
-
435
- if ( !wasVisible ) {
436
- this.toolbarView.items.get( 0 ).focus();
437
- }
438
- }
439
-
440
- /**
441
- * Hides the {@link #toolbarView}.
442
- *
443
- * @private
444
- * @param {Boolean} [focusEditable=false] When `true`, the editable will be focused after hiding the panel.
445
- */
446
- _hidePanel( focusEditable ) {
447
- this.panelView.isVisible = false;
448
-
449
- if ( focusEditable ) {
450
- this.editor.editing.view.focus();
451
- }
452
- }
453
-
454
- /**
455
- * Attaches the {@link #buttonView} to the target block of content.
456
- *
457
- * @protected
458
- * @param {HTMLElement} targetElement Target element.
459
- */
460
- _attachButtonToElement( targetElement ) {
461
- const contentStyles = window.getComputedStyle( targetElement );
462
-
463
- const editableRect = new Rect( this.editor.ui.getEditableElement() );
464
- const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
465
- // When line height is not an integer then thread it as "normal".
466
- // MDN says that 'normal' == ~1.2 on desktop browsers.
467
- const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;
468
-
469
- const position = getOptimalPosition( {
470
- element: this.buttonView.element,
471
- target: targetElement,
472
- positions: [
473
- ( contentRect, buttonRect ) => {
474
- let left;
475
-
476
- if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
477
- left = editableRect.left - buttonRect.width;
478
- } else {
479
- left = editableRect.right;
480
- }
481
-
482
- return {
483
- top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
484
- left
485
- };
486
- }
487
- ]
488
- } );
489
-
490
- this.buttonView.top = position.top;
491
- this.buttonView.left = position.left;
492
- }
493
-
494
- /**
495
- * Gets the {@link #toolbarView} max-width, based on
496
- * editable width plus distance between farthest edge of the {@link #buttonView} and the editable.
497
- *
498
- * @private
499
- * @returns {String} maxWidth A maximum width that toolbar can have, in pixels.
500
- */
501
- _getToolbarMaxWidth() {
502
- const editableElement = this.editor.ui.view.editable.element;
503
- const editableRect = new Rect( editableElement );
504
- const buttonRect = new Rect( this.buttonView.element );
505
- const isRTL = this.editor.locale.uiLanguageDirection === 'rtl';
506
- const offset = isRTL ? ( buttonRect.left - editableRect.right ) + buttonRect.width : editableRect.left - buttonRect.left;
507
-
508
- return toPx( editableRect.width + offset );
509
- }
64
+ /**
65
+ * @inheritDoc
66
+ */
67
+ constructor(editor) {
68
+ super(editor);
69
+ /**
70
+ * A cached and normalized `config.blockToolbar` object.
71
+ *
72
+ * @type {module:core/editor/editorconfig~EditorConfig#blockToolbar}
73
+ * @private
74
+ */
75
+ this._blockToolbarConfig = normalizeToolbarConfig(this.editor.config.get('blockToolbar'));
76
+ /**
77
+ * The toolbar view.
78
+ *
79
+ * @type {module:ui/toolbar/toolbarview~ToolbarView}
80
+ */
81
+ this.toolbarView = this._createToolbarView();
82
+ /**
83
+ * The balloon panel view, containing the {@link #toolbarView}.
84
+ *
85
+ * @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
86
+ */
87
+ this.panelView = this._createPanelView();
88
+ /**
89
+ * The button view that opens the {@link #toolbarView}.
90
+ *
91
+ * @type {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
92
+ */
93
+ this.buttonView = this._createButtonView();
94
+ /**
95
+ * An instance of the resize observer that allows to respond to changes in editable's geometry
96
+ * so the toolbar can stay within its boundaries (and group toolbar items that do not fit).
97
+ *
98
+ * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the
99
+ * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar configuration}.
100
+ *
101
+ * **Note:** Created in {@link #afterInit}.
102
+ *
103
+ * @protected
104
+ * @member {module:utils/dom/resizeobserver~ResizeObserver}
105
+ */
106
+ this._resizeObserver = null;
107
+ // Close the #panelView upon clicking outside of the plugin UI.
108
+ clickOutsideHandler({
109
+ emitter: this.panelView,
110
+ contextElements: [this.panelView.element, this.buttonView.element],
111
+ activator: () => this.panelView.isVisible,
112
+ callback: () => this._hidePanel()
113
+ });
114
+ }
115
+ /**
116
+ * @inheritDoc
117
+ */
118
+ static get pluginName() {
119
+ return 'BlockToolbar';
120
+ }
121
+ /**
122
+ * @inheritDoc
123
+ */
124
+ init() {
125
+ const editor = this.editor;
126
+ // Hides panel on a direct selection change.
127
+ this.listenTo(editor.model.document.selection, 'change:range', (evt, data) => {
128
+ if (data.directChange) {
129
+ this._hidePanel();
130
+ }
131
+ });
132
+ this.listenTo(editor.ui, 'update', () => this._updateButton());
133
+ // `low` priority is used because of https://github.com/ckeditor/ckeditor5-core/issues/133.
134
+ this.listenTo(editor, 'change:isReadOnly', () => this._updateButton(), { priority: 'low' });
135
+ this.listenTo(editor.ui.focusTracker, 'change:isFocused', () => this._updateButton());
136
+ // Reposition button on resize.
137
+ this.listenTo(this.buttonView, 'change:isVisible', (evt, name, isVisible) => {
138
+ if (isVisible) {
139
+ // Keep correct position of button and panel on window#resize.
140
+ this.buttonView.listenTo(window, 'resize', () => this._updateButton());
141
+ }
142
+ else {
143
+ // Stop repositioning button when is hidden.
144
+ this.buttonView.stopListening(window, 'resize');
145
+ // Hide the panel when the button disappears.
146
+ this._hidePanel();
147
+ }
148
+ });
149
+ // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
150
+ editor.ui.addToolbar(this.toolbarView, {
151
+ beforeFocus: () => this._showPanel(),
152
+ afterBlur: () => this._hidePanel()
153
+ });
154
+ }
155
+ /**
156
+ * Fills the toolbar with its items based on the configuration.
157
+ *
158
+ * **Note:** This needs to be done after all plugins are ready.
159
+ *
160
+ * @inheritDoc
161
+ */
162
+ afterInit() {
163
+ const factory = this.editor.ui.componentFactory;
164
+ const config = this._blockToolbarConfig;
165
+ this.toolbarView.fillFromConfig(config, factory);
166
+ // Hide panel before executing each button in the panel.
167
+ for (const item of this.toolbarView.items) {
168
+ item.on('execute', () => this._hidePanel(true), { priority: 'high' });
169
+ }
170
+ if (!config.shouldNotGroupWhenFull) {
171
+ this.listenTo(this.editor, 'ready', () => {
172
+ const editableElement = this.editor.ui.view.editable.element;
173
+ // Set #toolbarView's max-width just after the initialization and update it on the editable resize.
174
+ this._resizeObserver = new ResizeObserver(editableElement, () => {
175
+ this.toolbarView.maxWidth = this._getToolbarMaxWidth();
176
+ });
177
+ });
178
+ }
179
+ }
180
+ /**
181
+ * @inheritDoc
182
+ */
183
+ destroy() {
184
+ super.destroy();
185
+ // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
186
+ this.panelView.destroy();
187
+ this.buttonView.destroy();
188
+ this.toolbarView.destroy();
189
+ if (this._resizeObserver) {
190
+ this._resizeObserver.destroy();
191
+ }
192
+ }
193
+ /**
194
+ * Creates the {@link #toolbarView}.
195
+ *
196
+ * @private
197
+ * @returns {module:ui/toolbar/toolbarview~ToolbarView}
198
+ */
199
+ _createToolbarView() {
200
+ const t = this.editor.locale.t;
201
+ const shouldGroupWhenFull = !this._blockToolbarConfig.shouldNotGroupWhenFull;
202
+ const toolbarView = new ToolbarView(this.editor.locale, {
203
+ shouldGroupWhenFull,
204
+ isFloating: true
205
+ });
206
+ toolbarView.ariaLabel = t('Editor block content toolbar');
207
+ // When toolbar lost focus then panel should hide.
208
+ toolbarView.focusTracker.on('change:isFocused', (evt, name, is) => {
209
+ if (!is) {
210
+ this._hidePanel();
211
+ }
212
+ });
213
+ return toolbarView;
214
+ }
215
+ /**
216
+ * Creates the {@link #panelView}.
217
+ *
218
+ * @private
219
+ * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
220
+ */
221
+ _createPanelView() {
222
+ const editor = this.editor;
223
+ const panelView = new BalloonPanelView(editor.locale);
224
+ panelView.content.add(this.toolbarView);
225
+ panelView.class = 'ck-toolbar-container';
226
+ editor.ui.view.body.add(panelView);
227
+ editor.ui.focusTracker.add(panelView.element);
228
+ // Close #panelView on `Esc` press.
229
+ this.toolbarView.keystrokes.set('Esc', (evt, cancel) => {
230
+ this._hidePanel(true);
231
+ cancel();
232
+ });
233
+ return panelView;
234
+ }
235
+ /**
236
+ * Creates the {@link #buttonView}.
237
+ *
238
+ * @private
239
+ * @returns {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
240
+ */
241
+ _createButtonView() {
242
+ const editor = this.editor;
243
+ const t = editor.t;
244
+ const buttonView = new BlockButtonView(editor.locale);
245
+ const bind = buttonView.bindTemplate;
246
+ buttonView.set({
247
+ label: t('Edit block'),
248
+ icon: pilcrow,
249
+ withText: false
250
+ });
251
+ // Note that this piece over here overrides the default mousedown logic in ButtonView
252
+ // to make it work with BlockToolbar. See the implementation of the ButtonView class to learn more.
253
+ buttonView.extendTemplate({
254
+ on: {
255
+ mousedown: bind.to(evt => {
256
+ // On Safari we have to force the focus on a button on click as it's the only browser
257
+ // that doesn't do that automatically. See #12115.
258
+ if (env.isSafari && this.panelView.isVisible) {
259
+ this.toolbarView.focus();
260
+ }
261
+ // Workaround to #12184, see https://github.com/ckeditor/ckeditor5/issues/12184#issuecomment-1199147964.
262
+ evt.preventDefault();
263
+ })
264
+ }
265
+ });
266
+ // Bind the panelView observable properties to the buttonView.
267
+ buttonView.bind('isOn').to(this.panelView, 'isVisible');
268
+ buttonView.bind('tooltip').to(this.panelView, 'isVisible', isVisible => !isVisible);
269
+ // Toggle the panelView upon buttonView#execute.
270
+ this.listenTo(buttonView, 'execute', () => {
271
+ if (!this.panelView.isVisible) {
272
+ this._showPanel();
273
+ }
274
+ else {
275
+ this._hidePanel(true);
276
+ }
277
+ });
278
+ editor.ui.view.body.add(buttonView);
279
+ editor.ui.focusTracker.add(buttonView.element);
280
+ return buttonView;
281
+ }
282
+ /**
283
+ * Shows or hides the button.
284
+ * When all the conditions for displaying the button are matched, it shows the button. Hides otherwise.
285
+ *
286
+ * @private
287
+ */
288
+ _updateButton() {
289
+ const editor = this.editor;
290
+ const model = editor.model;
291
+ const view = editor.editing.view;
292
+ // Hides the button when the editor is not focused.
293
+ if (!editor.ui.focusTracker.isFocused) {
294
+ this._hideButton();
295
+ return;
296
+ }
297
+ // Hides the button when the editor switches to the read-only mode.
298
+ if (editor.isReadOnly) {
299
+ this._hideButton();
300
+ return;
301
+ }
302
+ // Get the first selected block, button will be attached to this element.
303
+ const modelTarget = Array.from(model.document.selection.getSelectedBlocks())[0];
304
+ // Hides the button when there is no enabled item in toolbar for the current block element.
305
+ if (!modelTarget || Array.from(this.toolbarView.items).every((item) => !item.isEnabled)) {
306
+ this._hideButton();
307
+ return;
308
+ }
309
+ // Get DOM target element.
310
+ const domTarget = view.domConverter.mapViewToDom(editor.editing.mapper.toViewElement(modelTarget));
311
+ // Show block button.
312
+ this.buttonView.isVisible = true;
313
+ // Attach block button to target DOM element.
314
+ this._attachButtonToElement(domTarget);
315
+ // When panel is opened then refresh it position to be properly aligned with block button.
316
+ if (this.panelView.isVisible) {
317
+ this._showPanel();
318
+ }
319
+ }
320
+ /**
321
+ * Hides the button.
322
+ *
323
+ * @private
324
+ */
325
+ _hideButton() {
326
+ this.buttonView.isVisible = false;
327
+ }
328
+ /**
329
+ * Shows the {@link #toolbarView} attached to the {@link #buttonView}.
330
+ * If the toolbar is already visible, then it simply repositions it.
331
+ *
332
+ * @private
333
+ */
334
+ _showPanel() {
335
+ // Usually, the only way to show the toolbar is by pressing the block button. It makes it impossible for
336
+ // the toolbar to show up when the button is invisible (feature does not make sense for the selection then).
337
+ // The toolbar navigation using Alt+F10 does not access the button but shows the panel directly using this method.
338
+ // So we need to check whether this is possible first.
339
+ if (!this.buttonView.isVisible) {
340
+ return;
341
+ }
342
+ const wasVisible = this.panelView.isVisible;
343
+ // So here's the thing: If there was no initial panelView#show() or these two were in different order, the toolbar
344
+ // positioning will break in RTL editors. Weird, right? What you show know is that the toolbar
345
+ // grouping works thanks to:
346
+ //
347
+ // * the ResizeObserver, which kicks in as soon as the toolbar shows up in DOM (becomes visible again).
348
+ // * the observable ToolbarView#maxWidth, which triggers re-grouping when changed.
349
+ //
350
+ // Here are the possible scenarios:
351
+ //
352
+ // 1. (WRONG ❌) If the #maxWidth is set when the toolbar is invisible, it won't affect item grouping (no DOMRects, no grouping).
353
+ // Then, when panelView.pin() is called, the position of the toolbar will be calculated for the old
354
+ // items grouping state, and when finally ResizeObserver kicks in (hey, the toolbar is visible now, right?)
355
+ // it will group/ungroup some items and the length of the toolbar will change. But since in RTL the toolbar
356
+ // is attached on the right side and the positioning uses CSS "left", it will result in the toolbar shifting
357
+ // to the left and being displayed in the wrong place.
358
+ // 2. (WRONG ❌) If the panelView.pin() is called first and #maxWidth set next, then basically the story repeats. The balloon
359
+ // calculates the position for the old toolbar grouping state, then the toolbar re-groups items and because
360
+ // it is positioned using CSS "left" it will move.
361
+ // 3. (RIGHT ✅) We show the panel first (the toolbar does re-grouping but it does not matter), then the #maxWidth
362
+ // is set allowing the toolbar to re-group again and finally panelView.pin() does the positioning when the
363
+ // items grouping state is stable and final.
364
+ //
365
+ // https://github.com/ckeditor/ckeditor5/issues/6449, https://github.com/ckeditor/ckeditor5/issues/6575
366
+ this.panelView.show();
367
+ this.toolbarView.maxWidth = this._getToolbarMaxWidth();
368
+ this.panelView.pin({
369
+ target: this.buttonView.element,
370
+ limiter: this.editor.ui.getEditableElement()
371
+ });
372
+ if (!wasVisible) {
373
+ this.toolbarView.items.get(0).focus();
374
+ }
375
+ }
376
+ /**
377
+ * Hides the {@link #toolbarView}.
378
+ *
379
+ * @private
380
+ * @param {Boolean} [focusEditable=false] When `true`, the editable will be focused after hiding the panel.
381
+ */
382
+ _hidePanel(focusEditable) {
383
+ this.panelView.isVisible = false;
384
+ if (focusEditable) {
385
+ this.editor.editing.view.focus();
386
+ }
387
+ }
388
+ /**
389
+ * Attaches the {@link #buttonView} to the target block of content.
390
+ *
391
+ * @protected
392
+ * @param {HTMLElement} targetElement Target element.
393
+ */
394
+ _attachButtonToElement(targetElement) {
395
+ const contentStyles = window.getComputedStyle(targetElement);
396
+ const editableRect = new Rect(this.editor.ui.getEditableElement());
397
+ const contentPaddingTop = parseInt(contentStyles.paddingTop, 10);
398
+ // When line height is not an integer then thread it as "normal".
399
+ // MDN says that 'normal' == ~1.2 on desktop browsers.
400
+ const contentLineHeight = parseInt(contentStyles.lineHeight, 10) || parseInt(contentStyles.fontSize, 10) * 1.2;
401
+ const position = getOptimalPosition({
402
+ element: this.buttonView.element,
403
+ target: targetElement,
404
+ positions: [
405
+ (contentRect, buttonRect) => {
406
+ let left;
407
+ if (this.editor.locale.uiLanguageDirection === 'ltr') {
408
+ left = editableRect.left - buttonRect.width;
409
+ }
410
+ else {
411
+ left = editableRect.right;
412
+ }
413
+ return {
414
+ top: contentRect.top + contentPaddingTop + (contentLineHeight - buttonRect.height) / 2,
415
+ left
416
+ };
417
+ }
418
+ ]
419
+ });
420
+ this.buttonView.top = position.top;
421
+ this.buttonView.left = position.left;
422
+ }
423
+ /**
424
+ * Gets the {@link #toolbarView} max-width, based on
425
+ * editable width plus distance between farthest edge of the {@link #buttonView} and the editable.
426
+ *
427
+ * @private
428
+ * @returns {String} maxWidth A maximum width that toolbar can have, in pixels.
429
+ */
430
+ _getToolbarMaxWidth() {
431
+ const editableElement = this.editor.ui.view.editable.element;
432
+ const editableRect = new Rect(editableElement);
433
+ const buttonRect = new Rect(this.buttonView.element);
434
+ const isRTL = this.editor.locale.uiLanguageDirection === 'rtl';
435
+ const offset = isRTL ? (buttonRect.left - editableRect.right) + buttonRect.width : editableRect.left - buttonRect.left;
436
+ return toPx(editableRect.width + offset);
437
+ }
510
438
  }
511
-
512
- /**
513
- * The block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}
514
- * feature.
515
- *
516
- * const config = {
517
- * blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'bulletedList', 'numberedList' ]
518
- * };
519
- *
520
- * You can also use `'|'` to create a separator between groups of items:
521
- *
522
- * const config = {
523
- * blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ]
524
- * };
525
- *
526
- * ## Configuring items grouping
527
- *
528
- * You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option:
529
- *
530
- * const config = {
531
- * blockToolbar: {
532
- * items: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ],
533
- * shouldNotGroupWhenFull: true
534
- * },
535
- * };
536
- *
537
- * Read more about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
538
- *
539
- * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar
540
- */