@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,11 +2,9 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module ui/panel/balloon/contextualballoon
8
7
  */
9
-
10
8
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11
9
  import BalloonPanelView from './balloonpanelview';
12
10
  import View from '../../view';
@@ -15,15 +13,11 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
15
13
  import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
16
14
  import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
17
15
  import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
18
-
19
16
  import prevIcon from '../../../theme/icons/previous-arrow.svg';
20
17
  import nextIcon from '../../../theme/icons/next-arrow.svg';
21
-
22
18
  import '../../../theme/components/panel/balloonrotator.css';
23
19
  import '../../../theme/components/panel/fakepanel.css';
24
-
25
- const toPx = toUnit( 'px' );
26
-
20
+ const toPx = toUnit('px');
27
21
  /**
28
22
  * Provides the common contextual balloon for the editor.
29
23
  *
@@ -61,473 +55,394 @@ const toPx = toUnit( 'px' );
61
55
  * @extends module:core/plugin~Plugin
62
56
  */
63
57
  export default class ContextualBalloon extends Plugin {
64
- /**
65
- * @inheritDoc
66
- */
67
- static get pluginName() {
68
- return 'ContextualBalloon';
69
- }
70
-
71
- /**
72
- * @inheritDoc
73
- */
74
- constructor( editor ) {
75
- super( editor );
76
-
77
- /**
78
- * The {@link module:utils/dom/position~Options#limiter position limiter}
79
- * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
80
- * or {@link #updatePosition}.
81
- *
82
- * By default, a function that obtains the farthest DOM
83
- * {@link module:engine/view/rooteditableelement~RootEditableElement}
84
- * of the {@link module:engine/view/document~Document#selection}.
85
- *
86
- * @member {module:utils/dom/position~Options#limiter} #positionLimiter
87
- */
88
- this.positionLimiter = () => {
89
- const view = this.editor.editing.view;
90
- const viewDocument = view.document;
91
- const editableElement = viewDocument.selection.editableElement;
92
-
93
- if ( editableElement ) {
94
- return view.domConverter.mapViewToDom( editableElement.root );
95
- }
96
-
97
- return null;
98
- };
99
-
100
- /**
101
- * The currently visible view or `null` when there are no views in any stack.
102
- *
103
- * @readonly
104
- * @observable
105
- * @member {module:ui/view~View|null} #visibleView
106
- */
107
- this.set( 'visibleView', null );
108
-
109
- /**
110
- * The common balloon panel view.
111
- *
112
- * @readonly
113
- * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
114
- */
115
- this.view = new BalloonPanelView( editor.locale );
116
- editor.ui.view.body.add( this.view );
117
- editor.ui.focusTracker.add( this.view.element );
118
-
119
- /**
120
- * The map of views and their stacks.
121
- *
122
- * @private
123
- * @type {Map.<module:ui/view~View,Set>}
124
- */
125
- this._viewToStack = new Map();
126
-
127
- /**
128
- * The map of IDs and stacks.
129
- *
130
- * @private
131
- * @type {Map.<String,Set>}
132
- */
133
- this._idToStack = new Map();
134
-
135
- /**
136
- * A total number of all stacks in the balloon.
137
- *
138
- * @private
139
- * @readonly
140
- * @observable
141
- * @member {Number} #_numberOfStacks
142
- */
143
- this.set( '_numberOfStacks', 0 );
144
-
145
- /**
146
- * A flag that controls the single view mode.
147
- *
148
- * @private
149
- * @readonly
150
- * @observable
151
- * @member {Boolean} #_singleViewMode
152
- */
153
- this.set( '_singleViewMode', false );
154
-
155
- /**
156
- * Rotator view embedded in the contextual balloon.
157
- * Displays the currently visible view in the balloon and provides navigation for switching stacks.
158
- *
159
- * @private
160
- * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
161
- */
162
- this._rotatorView = this._createRotatorView();
163
-
164
- /**
165
- * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
166
- *
167
- * @private
168
- * @type {module:ui/view~View}
169
- */
170
- this._fakePanelsView = this._createFakePanelsView();
171
- }
172
-
173
- /**
174
- * @inheritDoc
175
- */
176
- destroy() {
177
- super.destroy();
178
-
179
- this.view.destroy();
180
- this._rotatorView.destroy();
181
- this._fakePanelsView.destroy();
182
- }
183
-
184
- /**
185
- * Returns `true` when the given view is in one of the stacks. Otherwise returns `false`.
186
- *
187
- * @param {module:ui/view~View} view
188
- * @returns {Boolean}
189
- */
190
- hasView( view ) {
191
- return Array.from( this._viewToStack.keys() ).includes( view );
192
- }
193
-
194
- /**
195
- * Adds a new view to the stack and makes it visible if the current stack is visible
196
- * or it is the first view in the balloon.
197
- *
198
- * @param {Object} data The configuration of the view.
199
- * @param {String} [data.stackId='main'] The ID of the stack that the view is added to.
200
- * @param {module:ui/view~View} [data.view] The content of the balloon.
201
- * @param {module:utils/dom/position~Options} [data.position] Positioning options.
202
- * @param {String} [data.balloonClassName] An additional CSS class added to the {@link #view balloon} when visible.
203
- * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
204
- * @param {Boolean} [data.singleViewMode=false] Whether the view should be the only visible view even if other stacks were added.
205
- */
206
- add( data ) {
207
- if ( this.hasView( data.view ) ) {
208
- /**
209
- * Trying to add configuration of the same view more than once.
210
- *
211
- * @error contextualballoon-add-view-exist
212
- */
213
- throw new CKEditorError(
214
- 'contextualballoon-add-view-exist',
215
- [ this, data ]
216
- );
217
- }
218
-
219
- const stackId = data.stackId || 'main';
220
-
221
- // If new stack is added, creates it and show view from this stack.
222
- if ( !this._idToStack.has( stackId ) ) {
223
- this._idToStack.set( stackId, new Map( [ [ data.view, data ] ] ) );
224
- this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
225
- this._numberOfStacks = this._idToStack.size;
226
-
227
- if ( !this._visibleStack || data.singleViewMode ) {
228
- this.showStack( stackId );
229
- }
230
-
231
- return;
232
- }
233
-
234
- const stack = this._idToStack.get( stackId );
235
-
236
- if ( data.singleViewMode ) {
237
- this.showStack( stackId );
238
- }
239
-
240
- // Add new view to the stack.
241
- stack.set( data.view, data );
242
- this._viewToStack.set( data.view, stack );
243
-
244
- // And display it if is added to the currently visible stack.
245
- if ( stack === this._visibleStack ) {
246
- this._showView( data );
247
- }
248
- }
249
-
250
- /**
251
- * Removes the given view from the stack. If the removed view was visible,
252
- * the view preceding it in the stack will become visible instead.
253
- * When there is no view in the stack, the next stack will be displayed.
254
- * When there are no more stacks, the balloon will hide.
255
- *
256
- * @param {module:ui/view~View} view A view to be removed from the balloon.
257
- */
258
- remove( view ) {
259
- if ( !this.hasView( view ) ) {
260
- /**
261
- * Trying to remove the configuration of the view not defined in the stack.
262
- *
263
- * @error contextualballoon-remove-view-not-exist
264
- */
265
- throw new CKEditorError(
266
- 'contextualballoon-remove-view-not-exist',
267
- [ this, view ]
268
- );
269
- }
270
-
271
- const stack = this._viewToStack.get( view );
272
-
273
- if ( this._singleViewMode && this.visibleView === view ) {
274
- this._singleViewMode = false;
275
- }
276
-
277
- // When visible view will be removed we need to show a preceding view or next stack
278
- // if a view is the only view in the stack.
279
- if ( this.visibleView === view ) {
280
- if ( stack.size === 1 ) {
281
- if ( this._idToStack.size > 1 ) {
282
- this._showNextStack();
283
- } else {
284
- this.view.hide();
285
- this.visibleView = null;
286
- this._rotatorView.hideView();
287
- }
288
- } else {
289
- this._showView( Array.from( stack.values() )[ stack.size - 2 ] );
290
- }
291
- }
292
-
293
- if ( stack.size === 1 ) {
294
- this._idToStack.delete( this._getStackId( stack ) );
295
- this._numberOfStacks = this._idToStack.size;
296
- } else {
297
- stack.delete( view );
298
- }
299
-
300
- this._viewToStack.delete( view );
301
- }
302
-
303
- /**
304
- * Updates the position of the balloon using the position data of the first visible view in the stack.
305
- * When new position data is given, the position data of the currently visible view will be updated.
306
- *
307
- * @param {module:utils/dom/position~Options} [position] position options.
308
- */
309
- updatePosition( position ) {
310
- if ( position ) {
311
- this._visibleStack.get( this.visibleView ).position = position;
312
- }
313
-
314
- this.view.pin( this._getBalloonPosition() );
315
- this._fakePanelsView.updatePosition();
316
- }
317
-
318
- /**
319
- * Shows the last view from the stack of a given ID.
320
- *
321
- * @param {String} id
322
- */
323
- showStack( id ) {
324
- this.visibleStack = id;
325
- const stack = this._idToStack.get( id );
326
-
327
- if ( !stack ) {
328
- /**
329
- * Trying to show a stack that does not exist.
330
- *
331
- * @error contextualballoon-showstack-stack-not-exist
332
- */
333
- throw new CKEditorError(
334
- 'contextualballoon-showstack-stack-not-exist',
335
- this
336
- );
337
- }
338
-
339
- if ( this._visibleStack === stack ) {
340
- return;
341
- }
342
-
343
- this._showView( Array.from( stack.values() ).pop() );
344
- }
345
-
346
- /**
347
- * Returns the stack of the currently visible view.
348
- *
349
- * @private
350
- * @type {Set}
351
- */
352
- get _visibleStack() {
353
- return this._viewToStack.get( this.visibleView );
354
- }
355
-
356
- /**
357
- * Returns the ID of the given stack.
358
- *
359
- * @private
360
- * @param {Set} stack
361
- * @returns {String}
362
- */
363
- _getStackId( stack ) {
364
- const entry = Array.from( this._idToStack.entries() ).find( entry => entry[ 1 ] === stack );
365
-
366
- return entry[ 0 ];
367
- }
368
-
369
- /**
370
- * Shows the last view from the next stack.
371
- *
372
- * @private
373
- */
374
- _showNextStack() {
375
- const stacks = Array.from( this._idToStack.values() );
376
-
377
- let nextIndex = stacks.indexOf( this._visibleStack ) + 1;
378
-
379
- if ( !stacks[ nextIndex ] ) {
380
- nextIndex = 0;
381
- }
382
-
383
- this.showStack( this._getStackId( stacks[ nextIndex ] ) );
384
- }
385
-
386
- /**
387
- * Shows the last view from the previous stack.
388
- *
389
- * @private
390
- */
391
- _showPrevStack() {
392
- const stacks = Array.from( this._idToStack.values() );
393
-
394
- let nextIndex = stacks.indexOf( this._visibleStack ) - 1;
395
-
396
- if ( !stacks[ nextIndex ] ) {
397
- nextIndex = stacks.length - 1;
398
- }
399
-
400
- this.showStack( this._getStackId( stacks[ nextIndex ] ) );
401
- }
402
-
403
- /**
404
- * Creates a rotator view.
405
- *
406
- * @private
407
- * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
408
- */
409
- _createRotatorView() {
410
- const view = new RotatorView( this.editor.locale );
411
- const t = this.editor.locale.t;
412
-
413
- this.view.content.add( view );
414
-
415
- // Hide navigation when there is only a one stack & not in single view mode.
416
- view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
417
- return !isSingleViewMode && value > 1;
418
- } );
419
-
420
- // Update balloon position after toggling navigation.
421
- view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
422
-
423
- // Update stacks counter value.
424
- view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
425
- if ( numberOfStacks < 2 ) {
426
- return '';
427
- }
428
-
429
- const current = Array.from( this._idToStack.values() ).indexOf( this._visibleStack ) + 1;
430
-
431
- return t( '%0 of %1', [ current, numberOfStacks ] );
432
- } );
433
-
434
- view.buttonNextView.on( 'execute', () => {
435
- // When current view has a focus then move focus to the editable before removing it,
436
- // otherwise editor will lost focus.
437
- if ( view.focusTracker.isFocused ) {
438
- this.editor.editing.view.focus();
439
- }
440
-
441
- this._showNextStack();
442
- } );
443
-
444
- view.buttonPrevView.on( 'execute', () => {
445
- // When current view has a focus then move focus to the editable before removing it,
446
- // otherwise editor will lost focus.
447
- if ( view.focusTracker.isFocused ) {
448
- this.editor.editing.view.focus();
449
- }
450
-
451
- this._showPrevStack();
452
- } );
453
-
454
- return view;
455
- }
456
-
457
- /**
458
- * @private
459
- * @returns {module:ui/view~View}
460
- */
461
- _createFakePanelsView() {
462
- const view = new FakePanelsView( this.editor.locale, this.view );
463
-
464
- view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
465
- const showPanels = !isSingleViewMode && number >= 2;
466
-
467
- return showPanels ? Math.min( number - 1, 2 ) : 0;
468
- } );
469
-
470
- view.listenTo( this.view, 'change:top', () => view.updatePosition() );
471
- view.listenTo( this.view, 'change:left', () => view.updatePosition() );
472
-
473
- this.editor.ui.view.body.add( view );
474
-
475
- return view;
476
- }
477
-
478
- /**
479
- * Sets the view as the content of the balloon and attaches the balloon using position
480
- * options of the first view.
481
- *
482
- * @private
483
- * @param {Object} data Configuration.
484
- * @param {module:ui/view~View} [data.view] The view to show in the balloon.
485
- * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
486
- * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
487
- */
488
- _showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
489
- this.view.class = balloonClassName;
490
- this.view.withArrow = withArrow;
491
-
492
- this._rotatorView.showView( view );
493
- this.visibleView = view;
494
- this.view.pin( this._getBalloonPosition() );
495
- this._fakePanelsView.updatePosition();
496
-
497
- if ( singleViewMode ) {
498
- this._singleViewMode = true;
499
- }
500
- }
501
-
502
- /**
503
- * Returns position options of the last view in the stack.
504
- * This keeps the balloon in the same position when the view is changed.
505
- *
506
- * @private
507
- * @returns {module:utils/dom/position~Options}
508
- */
509
- _getBalloonPosition() {
510
- let position = Array.from( this._visibleStack.values() ).pop().position;
511
-
512
- if ( position ) {
513
- // Use the default limiter if none has been specified.
514
- if ( !position.limiter ) {
515
- // Don't modify the original options object.
516
- position = Object.assign( {}, position, {
517
- limiter: this.positionLimiter
518
- } );
519
- }
520
-
521
- // Don't modify the original options object.
522
- position = Object.assign( {}, position, {
523
- viewportOffsetConfig: this.editor.ui.viewportOffset
524
- } );
525
- }
526
-
527
- return position;
528
- }
58
+ /**
59
+ * @inheritDoc
60
+ */
61
+ constructor(editor) {
62
+ super(editor);
63
+ /**
64
+ * The {@link module:utils/dom/position~Options#limiter position limiter}
65
+ * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
66
+ * or {@link #updatePosition}.
67
+ *
68
+ * By default, a function that obtains the farthest DOM
69
+ * {@link module:engine/view/rooteditableelement~RootEditableElement}
70
+ * of the {@link module:engine/view/document~Document#selection}.
71
+ *
72
+ * @member {module:utils/dom/position~Options#limiter} #positionLimiter
73
+ */
74
+ this.positionLimiter = () => {
75
+ const view = this.editor.editing.view;
76
+ const viewDocument = view.document;
77
+ const editableElement = viewDocument.selection.editableElement;
78
+ if (editableElement) {
79
+ return view.domConverter.mapViewToDom(editableElement.root);
80
+ }
81
+ return null;
82
+ };
83
+ /**
84
+ * The currently visible view or `null` when there are no views in any stack.
85
+ *
86
+ * @readonly
87
+ * @observable
88
+ * @member {module:ui/view~View|null} #visibleView
89
+ */
90
+ this.set('visibleView', null);
91
+ /**
92
+ * The common balloon panel view.
93
+ *
94
+ * @readonly
95
+ * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
96
+ */
97
+ this.view = new BalloonPanelView(editor.locale);
98
+ editor.ui.view.body.add(this.view);
99
+ editor.ui.focusTracker.add(this.view.element);
100
+ /**
101
+ * The map of views and their stacks.
102
+ *
103
+ * @private
104
+ * @type {Map.<module:ui/view~View,Set>}
105
+ */
106
+ this._viewToStack = new Map();
107
+ /**
108
+ * The map of IDs and stacks.
109
+ *
110
+ * @private
111
+ * @type {Map.<String,Set>}
112
+ */
113
+ this._idToStack = new Map();
114
+ /**
115
+ * A total number of all stacks in the balloon.
116
+ *
117
+ * @private
118
+ * @readonly
119
+ * @observable
120
+ * @member {Number} #_numberOfStacks
121
+ */
122
+ this.set('_numberOfStacks', 0);
123
+ /**
124
+ * A flag that controls the single view mode.
125
+ *
126
+ * @private
127
+ * @readonly
128
+ * @observable
129
+ * @member {Boolean} #_singleViewMode
130
+ */
131
+ this.set('_singleViewMode', false);
132
+ /**
133
+ * Rotator view embedded in the contextual balloon.
134
+ * Displays the currently visible view in the balloon and provides navigation for switching stacks.
135
+ *
136
+ * @private
137
+ * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
138
+ */
139
+ this._rotatorView = this._createRotatorView();
140
+ /**
141
+ * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
142
+ *
143
+ * @private
144
+ * @type {module:ui/view~View}
145
+ */
146
+ this._fakePanelsView = this._createFakePanelsView();
147
+ }
148
+ /**
149
+ * @inheritDoc
150
+ */
151
+ static get pluginName() {
152
+ return 'ContextualBalloon';
153
+ }
154
+ /**
155
+ * @inheritDoc
156
+ */
157
+ destroy() {
158
+ super.destroy();
159
+ this.view.destroy();
160
+ this._rotatorView.destroy();
161
+ this._fakePanelsView.destroy();
162
+ }
163
+ /**
164
+ * Returns `true` when the given view is in one of the stacks. Otherwise returns `false`.
165
+ *
166
+ * @param {module:ui/view~View} view
167
+ * @returns {Boolean}
168
+ */
169
+ hasView(view) {
170
+ return Array.from(this._viewToStack.keys()).includes(view);
171
+ }
172
+ /**
173
+ * Adds a new view to the stack and makes it visible if the current stack is visible
174
+ * or it is the first view in the balloon.
175
+ *
176
+ * @param {Object} data The configuration of the view.
177
+ * @param {String} [data.stackId='main'] The ID of the stack that the view is added to.
178
+ * @param {module:ui/view~View} [data.view] The content of the balloon.
179
+ * @param {module:utils/dom/position~Options} [data.position] Positioning options.
180
+ * @param {String} [data.balloonClassName] An additional CSS class added to the {@link #view balloon} when visible.
181
+ * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
182
+ * @param {Boolean} [data.singleViewMode=false] Whether the view should be the only visible view even if other stacks were added.
183
+ */
184
+ add(data) {
185
+ if (this.hasView(data.view)) {
186
+ /**
187
+ * Trying to add configuration of the same view more than once.
188
+ *
189
+ * @error contextualballoon-add-view-exist
190
+ */
191
+ throw new CKEditorError('contextualballoon-add-view-exist', [this, data]);
192
+ }
193
+ const stackId = data.stackId || 'main';
194
+ // If new stack is added, creates it and show view from this stack.
195
+ if (!this._idToStack.has(stackId)) {
196
+ this._idToStack.set(stackId, new Map([[data.view, data]]));
197
+ this._viewToStack.set(data.view, this._idToStack.get(stackId));
198
+ this._numberOfStacks = this._idToStack.size;
199
+ if (!this._visibleStack || data.singleViewMode) {
200
+ this.showStack(stackId);
201
+ }
202
+ return;
203
+ }
204
+ const stack = this._idToStack.get(stackId);
205
+ if (data.singleViewMode) {
206
+ this.showStack(stackId);
207
+ }
208
+ // Add new view to the stack.
209
+ stack.set(data.view, data);
210
+ this._viewToStack.set(data.view, stack);
211
+ // And display it if is added to the currently visible stack.
212
+ if (stack === this._visibleStack) {
213
+ this._showView(data);
214
+ }
215
+ }
216
+ /**
217
+ * Removes the given view from the stack. If the removed view was visible,
218
+ * the view preceding it in the stack will become visible instead.
219
+ * When there is no view in the stack, the next stack will be displayed.
220
+ * When there are no more stacks, the balloon will hide.
221
+ *
222
+ * @param {module:ui/view~View} view A view to be removed from the balloon.
223
+ */
224
+ remove(view) {
225
+ if (!this.hasView(view)) {
226
+ /**
227
+ * Trying to remove the configuration of the view not defined in the stack.
228
+ *
229
+ * @error contextualballoon-remove-view-not-exist
230
+ */
231
+ throw new CKEditorError('contextualballoon-remove-view-not-exist', [this, view]);
232
+ }
233
+ const stack = this._viewToStack.get(view);
234
+ if (this._singleViewMode && this.visibleView === view) {
235
+ this._singleViewMode = false;
236
+ }
237
+ // When visible view will be removed we need to show a preceding view or next stack
238
+ // if a view is the only view in the stack.
239
+ if (this.visibleView === view) {
240
+ if (stack.size === 1) {
241
+ if (this._idToStack.size > 1) {
242
+ this._showNextStack();
243
+ }
244
+ else {
245
+ this.view.hide();
246
+ this.visibleView = null;
247
+ this._rotatorView.hideView();
248
+ }
249
+ }
250
+ else {
251
+ this._showView(Array.from(stack.values())[stack.size - 2]);
252
+ }
253
+ }
254
+ if (stack.size === 1) {
255
+ this._idToStack.delete(this._getStackId(stack));
256
+ this._numberOfStacks = this._idToStack.size;
257
+ }
258
+ else {
259
+ stack.delete(view);
260
+ }
261
+ this._viewToStack.delete(view);
262
+ }
263
+ /**
264
+ * Updates the position of the balloon using the position data of the first visible view in the stack.
265
+ * When new position data is given, the position data of the currently visible view will be updated.
266
+ *
267
+ * @param {module:utils/dom/position~Options} [position] position options.
268
+ */
269
+ updatePosition(position) {
270
+ if (position) {
271
+ this._visibleStack.get(this.visibleView).position = position;
272
+ }
273
+ this.view.pin(this._getBalloonPosition());
274
+ this._fakePanelsView.updatePosition();
275
+ }
276
+ /**
277
+ * Shows the last view from the stack of a given ID.
278
+ *
279
+ * @param {String} id
280
+ */
281
+ showStack(id) {
282
+ this.visibleStack = id;
283
+ const stack = this._idToStack.get(id);
284
+ if (!stack) {
285
+ /**
286
+ * Trying to show a stack that does not exist.
287
+ *
288
+ * @error contextualballoon-showstack-stack-not-exist
289
+ */
290
+ throw new CKEditorError('contextualballoon-showstack-stack-not-exist', this);
291
+ }
292
+ if (this._visibleStack === stack) {
293
+ return;
294
+ }
295
+ this._showView(Array.from(stack.values()).pop());
296
+ }
297
+ /**
298
+ * Returns the stack of the currently visible view.
299
+ *
300
+ * @private
301
+ * @type {Set}
302
+ */
303
+ get _visibleStack() {
304
+ return this._viewToStack.get(this.visibleView);
305
+ }
306
+ /**
307
+ * Returns the ID of the given stack.
308
+ *
309
+ * @private
310
+ * @param {Set} stack
311
+ * @returns {String}
312
+ */
313
+ _getStackId(stack) {
314
+ const entry = Array.from(this._idToStack.entries()).find(entry => entry[1] === stack);
315
+ return entry[0];
316
+ }
317
+ /**
318
+ * Shows the last view from the next stack.
319
+ *
320
+ * @private
321
+ */
322
+ _showNextStack() {
323
+ const stacks = Array.from(this._idToStack.values());
324
+ let nextIndex = stacks.indexOf(this._visibleStack) + 1;
325
+ if (!stacks[nextIndex]) {
326
+ nextIndex = 0;
327
+ }
328
+ this.showStack(this._getStackId(stacks[nextIndex]));
329
+ }
330
+ /**
331
+ * Shows the last view from the previous stack.
332
+ *
333
+ * @private
334
+ */
335
+ _showPrevStack() {
336
+ const stacks = Array.from(this._idToStack.values());
337
+ let nextIndex = stacks.indexOf(this._visibleStack) - 1;
338
+ if (!stacks[nextIndex]) {
339
+ nextIndex = stacks.length - 1;
340
+ }
341
+ this.showStack(this._getStackId(stacks[nextIndex]));
342
+ }
343
+ /**
344
+ * Creates a rotator view.
345
+ *
346
+ * @private
347
+ * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
348
+ */
349
+ _createRotatorView() {
350
+ const view = new RotatorView(this.editor.locale);
351
+ const t = this.editor.locale.t;
352
+ this.view.content.add(view);
353
+ // Hide navigation when there is only a one stack & not in single view mode.
354
+ view.bind('isNavigationVisible').to(this, '_numberOfStacks', this, '_singleViewMode', (value, isSingleViewMode) => {
355
+ return !isSingleViewMode && value > 1;
356
+ });
357
+ // Update balloon position after toggling navigation.
358
+ view.on('change:isNavigationVisible', () => (this.updatePosition()), { priority: 'low' });
359
+ // Update stacks counter value.
360
+ view.bind('counter').to(this, 'visibleView', this, '_numberOfStacks', (visibleView, numberOfStacks) => {
361
+ if (numberOfStacks < 2) {
362
+ return '';
363
+ }
364
+ const current = Array.from(this._idToStack.values()).indexOf(this._visibleStack) + 1;
365
+ return t('%0 of %1', [current, numberOfStacks]);
366
+ });
367
+ view.buttonNextView.on('execute', () => {
368
+ // When current view has a focus then move focus to the editable before removing it,
369
+ // otherwise editor will lost focus.
370
+ if (view.focusTracker.isFocused) {
371
+ this.editor.editing.view.focus();
372
+ }
373
+ this._showNextStack();
374
+ });
375
+ view.buttonPrevView.on('execute', () => {
376
+ // When current view has a focus then move focus to the editable before removing it,
377
+ // otherwise editor will lost focus.
378
+ if (view.focusTracker.isFocused) {
379
+ this.editor.editing.view.focus();
380
+ }
381
+ this._showPrevStack();
382
+ });
383
+ return view;
384
+ }
385
+ /**
386
+ * @private
387
+ * @returns {module:ui/view~View}
388
+ */
389
+ _createFakePanelsView() {
390
+ const view = new FakePanelsView(this.editor.locale, this.view);
391
+ view.bind('numberOfPanels').to(this, '_numberOfStacks', this, '_singleViewMode', (number, isSingleViewMode) => {
392
+ const showPanels = !isSingleViewMode && number >= 2;
393
+ return showPanels ? Math.min(number - 1, 2) : 0;
394
+ });
395
+ view.listenTo(this.view, 'change:top', () => view.updatePosition());
396
+ view.listenTo(this.view, 'change:left', () => view.updatePosition());
397
+ this.editor.ui.view.body.add(view);
398
+ return view;
399
+ }
400
+ /**
401
+ * Sets the view as the content of the balloon and attaches the balloon using position
402
+ * options of the first view.
403
+ *
404
+ * @private
405
+ * @param {Object} data Configuration.
406
+ * @param {module:ui/view~View} [data.view] The view to show in the balloon.
407
+ * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
408
+ * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
409
+ */
410
+ _showView({ view, balloonClassName = '', withArrow = true, singleViewMode = false }) {
411
+ this.view.class = balloonClassName;
412
+ this.view.withArrow = withArrow;
413
+ this._rotatorView.showView(view);
414
+ this.visibleView = view;
415
+ this.view.pin(this._getBalloonPosition());
416
+ this._fakePanelsView.updatePosition();
417
+ if (singleViewMode) {
418
+ this._singleViewMode = true;
419
+ }
420
+ }
421
+ /**
422
+ * Returns position options of the last view in the stack.
423
+ * This keeps the balloon in the same position when the view is changed.
424
+ *
425
+ * @private
426
+ * @returns {module:utils/dom/position~Options}
427
+ */
428
+ _getBalloonPosition() {
429
+ let position = Array.from(this._visibleStack.values()).pop().position;
430
+ if (position) {
431
+ // Use the default limiter if none has been specified.
432
+ if (!position.limiter) {
433
+ // Don't modify the original options object.
434
+ position = Object.assign({}, position, {
435
+ limiter: this.positionLimiter
436
+ });
437
+ }
438
+ // Don't modify the original options object.
439
+ position = Object.assign({}, position, {
440
+ viewportOffsetConfig: this.editor.ui.viewportOffset
441
+ });
442
+ }
443
+ return position;
444
+ }
529
445
  }
530
-
531
446
  /**
532
447
  * Rotator view is a helper class for the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon}.
533
448
  * It is used for displaying the last view from the current stack and providing navigation buttons for switching stacks.
@@ -536,269 +451,233 @@ export default class ContextualBalloon extends Plugin {
536
451
  * @extends module:ui/view~View
537
452
  */
538
453
  class RotatorView extends View {
539
- /**
540
- * @inheritDoc
541
- */
542
- constructor( locale ) {
543
- super( locale );
544
-
545
- const t = locale.t;
546
- const bind = this.bindTemplate;
547
-
548
- /**
549
- * Defines whether navigation is visible or not.
550
- *
551
- * @member {Boolean} #isNavigationVisible
552
- */
553
- this.set( 'isNavigationVisible', true );
554
-
555
- /**
556
- * Used for checking if a view is focused or not.
557
- *
558
- * @type {module:utils/focustracker~FocusTracker}
559
- */
560
- this.focusTracker = new FocusTracker();
561
-
562
- /**
563
- * Navigation button for switching the stack to the previous one.
564
- *
565
- * @type {module:ui/button/buttonview~ButtonView}
566
- */
567
- this.buttonPrevView = this._createButtonView( t( 'Previous' ), prevIcon );
568
-
569
- /**
570
- * Navigation button for switching the stack to the next one.
571
- *
572
- * @type {module:ui/button/buttonview~ButtonView}
573
- */
574
- this.buttonNextView = this._createButtonView( t( 'Next' ), nextIcon );
575
-
576
- /**
577
- * A collection of the child views that creates the rotator content.
578
- *
579
- * @readonly
580
- * @type {module:ui/viewcollection~ViewCollection}
581
- */
582
- this.content = this.createCollection();
583
-
584
- this.setTemplate( {
585
- tag: 'div',
586
- attributes: {
587
- class: [
588
- 'ck',
589
- 'ck-balloon-rotator'
590
- ],
591
- 'z-index': '-1'
592
- },
593
- children: [
594
- {
595
- tag: 'div',
596
- attributes: {
597
- class: [
598
- 'ck-balloon-rotator__navigation',
599
- bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
600
- ]
601
- },
602
- children: [
603
- this.buttonPrevView,
604
- {
605
- tag: 'span',
606
-
607
- attributes: {
608
- class: [
609
- 'ck-balloon-rotator__counter'
610
- ]
611
- },
612
-
613
- children: [
614
- {
615
- text: bind.to( 'counter' )
616
- }
617
- ]
618
- },
619
- this.buttonNextView
620
- ]
621
- },
622
- {
623
- tag: 'div',
624
- attributes: {
625
- class: 'ck-balloon-rotator__content'
626
- },
627
- children: this.content
628
- }
629
- ]
630
- } );
631
- }
632
-
633
- /**
634
- * @inheritDoc
635
- */
636
- render() {
637
- super.render();
638
-
639
- this.focusTracker.add( this.element );
640
- }
641
-
642
- /**
643
- * @inheritDoc
644
- */
645
- destroy() {
646
- super.destroy();
647
-
648
- this.focusTracker.destroy();
649
- }
650
-
651
- /**
652
- * Shows a given view.
653
- *
654
- * @param {module:ui/view~View} view The view to show.
655
- */
656
- showView( view ) {
657
- this.hideView();
658
- this.content.add( view );
659
- }
660
-
661
- /**
662
- * Hides the currently displayed view.
663
- */
664
- hideView() {
665
- this.content.clear();
666
- }
667
-
668
- /**
669
- * Creates a navigation button view.
670
- *
671
- * @private
672
- * @param {String} label The button label.
673
- * @param {String} icon The button icon.
674
- * @returns {module:ui/button/buttonview~ButtonView}
675
- */
676
- _createButtonView( label, icon ) {
677
- const view = new ButtonView( this.locale );
678
-
679
- view.set( {
680
- label,
681
- icon,
682
- tooltip: true
683
- } );
684
-
685
- return view;
686
- }
454
+ /**
455
+ * @inheritDoc
456
+ */
457
+ constructor(locale) {
458
+ super(locale);
459
+ const t = locale.t;
460
+ const bind = this.bindTemplate;
461
+ /**
462
+ * Defines whether navigation is visible or not.
463
+ *
464
+ * @member {Boolean} #isNavigationVisible
465
+ */
466
+ this.set('isNavigationVisible', true);
467
+ /**
468
+ * Used for checking if a view is focused or not.
469
+ *
470
+ * @type {module:utils/focustracker~FocusTracker}
471
+ */
472
+ this.focusTracker = new FocusTracker();
473
+ /**
474
+ * Navigation button for switching the stack to the previous one.
475
+ *
476
+ * @type {module:ui/button/buttonview~ButtonView}
477
+ */
478
+ this.buttonPrevView = this._createButtonView(t('Previous'), prevIcon);
479
+ /**
480
+ * Navigation button for switching the stack to the next one.
481
+ *
482
+ * @type {module:ui/button/buttonview~ButtonView}
483
+ */
484
+ this.buttonNextView = this._createButtonView(t('Next'), nextIcon);
485
+ /**
486
+ * A collection of the child views that creates the rotator content.
487
+ *
488
+ * @readonly
489
+ * @type {module:ui/viewcollection~ViewCollection}
490
+ */
491
+ this.content = this.createCollection();
492
+ this.setTemplate({
493
+ tag: 'div',
494
+ attributes: {
495
+ class: [
496
+ 'ck',
497
+ 'ck-balloon-rotator'
498
+ ],
499
+ 'z-index': '-1'
500
+ },
501
+ children: [
502
+ {
503
+ tag: 'div',
504
+ attributes: {
505
+ class: [
506
+ 'ck-balloon-rotator__navigation',
507
+ bind.to('isNavigationVisible', value => value ? '' : 'ck-hidden')
508
+ ]
509
+ },
510
+ children: [
511
+ this.buttonPrevView,
512
+ {
513
+ tag: 'span',
514
+ attributes: {
515
+ class: [
516
+ 'ck-balloon-rotator__counter'
517
+ ]
518
+ },
519
+ children: [
520
+ {
521
+ text: bind.to('counter')
522
+ }
523
+ ]
524
+ },
525
+ this.buttonNextView
526
+ ]
527
+ },
528
+ {
529
+ tag: 'div',
530
+ attributes: {
531
+ class: 'ck-balloon-rotator__content'
532
+ },
533
+ children: this.content
534
+ }
535
+ ]
536
+ });
537
+ }
538
+ /**
539
+ * @inheritDoc
540
+ */
541
+ render() {
542
+ super.render();
543
+ this.focusTracker.add(this.element);
544
+ }
545
+ /**
546
+ * @inheritDoc
547
+ */
548
+ destroy() {
549
+ super.destroy();
550
+ this.focusTracker.destroy();
551
+ }
552
+ /**
553
+ * Shows a given view.
554
+ *
555
+ * @param {module:ui/view~View} view The view to show.
556
+ */
557
+ showView(view) {
558
+ this.hideView();
559
+ this.content.add(view);
560
+ }
561
+ /**
562
+ * Hides the currently displayed view.
563
+ */
564
+ hideView() {
565
+ this.content.clear();
566
+ }
567
+ /**
568
+ * Creates a navigation button view.
569
+ *
570
+ * @private
571
+ * @param {String} label The button label.
572
+ * @param {String} icon The button icon.
573
+ * @returns {module:ui/button/buttonview~ButtonView}
574
+ */
575
+ _createButtonView(label, icon) {
576
+ const view = new ButtonView(this.locale);
577
+ view.set({
578
+ label,
579
+ icon,
580
+ tooltip: true
581
+ });
582
+ return view;
583
+ }
687
584
  }
688
-
689
585
  // Displays additional layers under the balloon when multiple stacks are added to the balloon.
690
586
  //
691
587
  // @private
692
588
  // @extends module:ui/view~View
693
589
  class FakePanelsView extends View {
694
- // @inheritDoc
695
- constructor( locale, balloonPanelView ) {
696
- super( locale );
697
-
698
- const bind = this.bindTemplate;
699
-
700
- // Fake panels top offset.
701
- //
702
- // @observable
703
- // @member {Number} #top
704
- this.set( 'top', 0 );
705
-
706
- // Fake panels left offset.
707
- //
708
- // @observable
709
- // @member {Number} #left
710
- this.set( 'left', 0 );
711
-
712
- // Fake panels height.
713
- //
714
- // @observable
715
- // @member {Number} #height
716
- this.set( 'height', 0 );
717
-
718
- // Fake panels width.
719
- //
720
- // @observable
721
- // @member {Number} #width
722
- this.set( 'width', 0 );
723
-
724
- // Number of rendered fake panels.
725
- //
726
- // @observable
727
- // @member {Number} #numberOfPanels
728
- this.set( 'numberOfPanels', 0 );
729
-
730
- // Collection of the child views which creates fake panel content.
731
- //
732
- // @readonly
733
- // @type {module:ui/viewcollection~ViewCollection}
734
- this.content = this.createCollection();
735
-
736
- // Context.
737
- //
738
- // @private
739
- // @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
740
- this._balloonPanelView = balloonPanelView;
741
-
742
- this.setTemplate( {
743
- tag: 'div',
744
- attributes: {
745
- class: [
746
- 'ck-fake-panel',
747
- bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
748
- ],
749
- style: {
750
- top: bind.to( 'top', toPx ),
751
- left: bind.to( 'left', toPx ),
752
- width: bind.to( 'width', toPx ),
753
- height: bind.to( 'height', toPx )
754
- }
755
- },
756
- children: this.content
757
- } );
758
-
759
- this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
760
- if ( next > prev ) {
761
- this._addPanels( next - prev );
762
- } else {
763
- this._removePanels( prev - next );
764
- }
765
-
766
- this.updatePosition();
767
- } );
768
- }
769
-
770
- // @private
771
- // @param {Number} number
772
- _addPanels( number ) {
773
- while ( number-- ) {
774
- const view = new View();
775
-
776
- view.setTemplate( { tag: 'div' } );
777
-
778
- this.content.add( view );
779
- this.registerChild( view );
780
- }
781
- }
782
-
783
- // @private
784
- // @param {Number} number
785
- _removePanels( number ) {
786
- while ( number-- ) {
787
- const view = this.content.last;
788
-
789
- this.content.remove( view );
790
- this.deregisterChild( view );
791
- view.destroy();
792
- }
793
- }
794
-
795
- // Updates coordinates of fake panels.
796
- updatePosition() {
797
- if ( this.numberOfPanels ) {
798
- const { top, left } = this._balloonPanelView;
799
- const { width, height } = new Rect( this._balloonPanelView.element );
800
-
801
- Object.assign( this, { top, left, width, height } );
802
- }
803
- }
590
+ // @inheritDoc
591
+ constructor(locale, balloonPanelView) {
592
+ super(locale);
593
+ const bind = this.bindTemplate;
594
+ // Fake panels top offset.
595
+ //
596
+ // @observable
597
+ // @member {Number} #top
598
+ this.set('top', 0);
599
+ // Fake panels left offset.
600
+ //
601
+ // @observable
602
+ // @member {Number} #left
603
+ this.set('left', 0);
604
+ // Fake panels height.
605
+ //
606
+ // @observable
607
+ // @member {Number} #height
608
+ this.set('height', 0);
609
+ // Fake panels width.
610
+ //
611
+ // @observable
612
+ // @member {Number} #width
613
+ this.set('width', 0);
614
+ // Number of rendered fake panels.
615
+ //
616
+ // @observable
617
+ // @member {Number} #numberOfPanels
618
+ this.set('numberOfPanels', 0);
619
+ // Collection of the child views which creates fake panel content.
620
+ //
621
+ // @readonly
622
+ // @type {module:ui/viewcollection~ViewCollection}
623
+ this.content = this.createCollection();
624
+ // Context.
625
+ //
626
+ // @private
627
+ // @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
628
+ this._balloonPanelView = balloonPanelView;
629
+ this.setTemplate({
630
+ tag: 'div',
631
+ attributes: {
632
+ class: [
633
+ 'ck-fake-panel',
634
+ bind.to('numberOfPanels', number => number ? '' : 'ck-hidden')
635
+ ],
636
+ style: {
637
+ top: bind.to('top', toPx),
638
+ left: bind.to('left', toPx),
639
+ width: bind.to('width', toPx),
640
+ height: bind.to('height', toPx)
641
+ }
642
+ },
643
+ children: this.content
644
+ });
645
+ this.on('change:numberOfPanels', (evt, name, next, prev) => {
646
+ if (next > prev) {
647
+ this._addPanels(next - prev);
648
+ }
649
+ else {
650
+ this._removePanels(prev - next);
651
+ }
652
+ this.updatePosition();
653
+ });
654
+ }
655
+ // @private
656
+ // @param {Number} number
657
+ _addPanels(number) {
658
+ while (number--) {
659
+ const view = new View();
660
+ view.setTemplate({ tag: 'div' });
661
+ this.content.add(view);
662
+ this.registerChild(view);
663
+ }
664
+ }
665
+ // @private
666
+ // @param {Number} number
667
+ _removePanels(number) {
668
+ while (number--) {
669
+ const view = this.content.last;
670
+ this.content.remove(view);
671
+ this.deregisterChild(view);
672
+ view.destroy();
673
+ }
674
+ }
675
+ // Updates coordinates of fake panels.
676
+ updatePosition() {
677
+ if (this.numberOfPanels) {
678
+ const { top, left } = this._balloonPanelView;
679
+ const { width, height } = new Rect(this._balloonPanelView.element);
680
+ Object.assign(this, { top, left, width, height });
681
+ }
682
+ }
804
683
  }