@ckeditor/ckeditor5-widget 35.2.0 → 35.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,18 +2,15 @@
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 widget/widgettoolbarrepository
8
7
  */
9
-
10
8
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11
9
  import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
12
10
  import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
13
11
  import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
14
12
  import { isWidget } from './utils';
15
13
  import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
16
-
17
14
  /**
18
15
  * Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
19
16
  * toolbar rendering process and exposes a concise API.
@@ -39,300 +36,251 @@ import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckedito
39
36
  * }
40
37
  */
41
38
  export default class WidgetToolbarRepository extends Plugin {
42
- /**
43
- * @inheritDoc
44
- */
45
- static get requires() {
46
- return [ ContextualBalloon ];
47
- }
48
-
49
- /**
50
- * @inheritDoc
51
- */
52
- static get pluginName() {
53
- return 'WidgetToolbarRepository';
54
- }
55
-
56
- /**
57
- * @inheritDoc
58
- */
59
- init() {
60
- const editor = this.editor;
61
-
62
- // Disables the default balloon toolbar for all widgets.
63
- if ( editor.plugins.has( 'BalloonToolbar' ) ) {
64
- const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
65
-
66
- this.listenTo( balloonToolbar, 'show', evt => {
67
- if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
68
- evt.stop();
69
- }
70
- }, { priority: 'high' } );
71
- }
72
-
73
- /**
74
- * A map of toolbar definitions.
75
- *
76
- * @protected
77
- * @member {Map.<String,module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition>} #_toolbarDefinitions
78
- */
79
- this._toolbarDefinitions = new Map();
80
-
81
- /**
82
- * @private
83
- */
84
- this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
85
-
86
- this.on( 'change:isEnabled', () => {
87
- this._updateToolbarsVisibility();
88
- } );
89
-
90
- this.listenTo( editor.ui, 'update', () => {
91
- this._updateToolbarsVisibility();
92
- } );
93
-
94
- // UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
95
- this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
96
- this._updateToolbarsVisibility();
97
- }, { priority: 'low' } );
98
- }
99
-
100
- destroy() {
101
- super.destroy();
102
-
103
- for ( const toolbarConfig of this._toolbarDefinitions.values() ) {
104
- toolbarConfig.view.destroy();
105
- }
106
- }
107
-
108
- /**
109
- * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
110
- * `getRelatedElement` function. Toolbar items are gathered from `items` array.
111
- * The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
112
- *
113
- * Note: This method should be called in the {@link module:core/plugin~PluginInterface#afterInit `Plugin#afterInit()`}
114
- * callback (or later) to make sure that the given toolbar items were already registered by other plugins.
115
- *
116
- * @param {String} toolbarId An id for the toolbar. Used to
117
- * @param {Object} options
118
- * @param {String} [options.ariaLabel] Label used by assistive technologies to describe this toolbar element.
119
- * @param {Array.<String>} options.items Array of toolbar items.
120
- * @param {Function} options.getRelatedElement Callback which returns an element the toolbar should be attached to.
121
- * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
122
- */
123
- register( toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
124
- // Trying to register a toolbar without any item.
125
- if ( !items.length ) {
126
- /**
127
- * When {@link #register registering} a new widget toolbar, you need to provide a non-empty array with
128
- * the items that will be inserted into the toolbar.
129
- *
130
- * If you see this error when integrating the editor, you likely forgot to configure one of the widget toolbars.
131
- *
132
- * See for instance:
133
- *
134
- * * {@link module:table/table~TableConfig#contentToolbar `config.table.contentToolbar`}
135
- * * {@link module:image/image~ImageConfig#toolbar `config.image.toolbar`}
136
- *
137
- * @error widget-toolbar-no-items
138
- * @param {String} toolbarId The id of the toolbar that has not been configured correctly.
139
- */
140
- logWarning( 'widget-toolbar-no-items', { toolbarId } );
141
-
142
- return;
143
- }
144
-
145
- const editor = this.editor;
146
- const t = editor.t;
147
- const toolbarView = new ToolbarView( editor.locale );
148
-
149
- toolbarView.ariaLabel = ariaLabel || t( 'Widget toolbar' );
150
-
151
- if ( this._toolbarDefinitions.has( toolbarId ) ) {
152
- /**
153
- * Toolbar with the given id was already added.
154
- *
155
- * @error widget-toolbar-duplicated
156
- * @param toolbarId Toolbar id.
157
- */
158
- throw new CKEditorError( 'widget-toolbar-duplicated', this, { toolbarId } );
159
- }
160
-
161
- toolbarView.fillFromConfig( items, editor.ui.componentFactory );
162
-
163
- const toolbarDefinition = {
164
- view: toolbarView,
165
- getRelatedElement,
166
- balloonClassName
167
- };
168
-
169
- // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
170
- editor.ui.addToolbar( toolbarView, {
171
- isContextual: true,
172
- beforeFocus: () => {
173
- const relatedElement = getRelatedElement( editor.editing.view.document.selection );
174
-
175
- if ( relatedElement ) {
176
- this._showToolbar( toolbarDefinition, relatedElement );
177
- }
178
- },
179
- afterBlur: () => {
180
- this._hideToolbar( toolbarDefinition );
181
- }
182
- } );
183
-
184
- this._toolbarDefinitions.set( toolbarId, toolbarDefinition );
185
- }
186
-
187
- /**
188
- * Iterates over stored toolbars and makes them visible or hidden.
189
- *
190
- * @private
191
- */
192
- _updateToolbarsVisibility() {
193
- let maxRelatedElementDepth = 0;
194
- let deepestRelatedElement = null;
195
- let deepestToolbarDefinition = null;
196
-
197
- for ( const definition of this._toolbarDefinitions.values() ) {
198
- const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
199
-
200
- if ( !this.isEnabled || !relatedElement ) {
201
- if ( this._isToolbarInBalloon( definition ) ) {
202
- this._hideToolbar( definition );
203
- }
204
- } else if ( !this.editor.ui.focusTracker.isFocused ) {
205
- if ( this._isToolbarVisible( definition ) ) {
206
- this._hideToolbar( definition );
207
- }
208
- } else {
209
- const relatedElementDepth = relatedElement.getAncestors().length;
210
-
211
- // Many toolbars can express willingness to be displayed but they do not know about
212
- // each other. Figure out which toolbar is deepest in the view tree to decide which
213
- // should be displayed. For instance, if a selected image is inside a table cell, display
214
- // the ImageToolbar rather than the TableToolbar (#60).
215
- if ( relatedElementDepth > maxRelatedElementDepth ) {
216
- maxRelatedElementDepth = relatedElementDepth;
217
- deepestRelatedElement = relatedElement;
218
- deepestToolbarDefinition = definition;
219
- }
220
- }
221
- }
222
-
223
- if ( deepestToolbarDefinition ) {
224
- this._showToolbar( deepestToolbarDefinition, deepestRelatedElement );
225
- }
226
- }
227
-
228
- /**
229
- * Hides the given toolbar.
230
- *
231
- * @private
232
- * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
233
- */
234
- _hideToolbar( toolbarDefinition ) {
235
- this._balloon.remove( toolbarDefinition.view );
236
- this.stopListening( this._balloon, 'change:visibleView' );
237
- }
238
-
239
- /**
240
- * Shows up the toolbar if the toolbar is not visible.
241
- * Otherwise, repositions the toolbar's balloon when toolbar's view is the most top view in balloon stack.
242
- *
243
- * It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
244
- * should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
245
- *
246
- * @private
247
- * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
248
- * @param {module:engine/view/element~Element} relatedElement
249
- */
250
- _showToolbar( toolbarDefinition, relatedElement ) {
251
- if ( this._isToolbarVisible( toolbarDefinition ) ) {
252
- repositionContextualBalloon( this.editor, relatedElement );
253
- } else if ( !this._isToolbarInBalloon( toolbarDefinition ) ) {
254
- this._balloon.add( {
255
- view: toolbarDefinition.view,
256
- position: getBalloonPositionData( this.editor, relatedElement ),
257
- balloonClassName: toolbarDefinition.balloonClassName
258
- } );
259
-
260
- // Update toolbar position each time stack with toolbar view is switched to visible.
261
- // This is in a case target element has changed when toolbar was in invisible stack
262
- // e.g. target image was wrapped by a block quote.
263
- // See https://github.com/ckeditor/ckeditor5-widget/issues/92.
264
- this.listenTo( this._balloon, 'change:visibleView', () => {
265
- for ( const definition of this._toolbarDefinitions.values() ) {
266
- if ( this._isToolbarVisible( definition ) ) {
267
- const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
268
- repositionContextualBalloon( this.editor, relatedElement );
269
- }
270
- }
271
- } );
272
- }
273
- }
274
-
275
- /**
276
- * @private
277
- * @param {Object} toolbar
278
- * @returns {Boolean}
279
- */
280
- _isToolbarVisible( toolbar ) {
281
- return this._balloon.visibleView === toolbar.view;
282
- }
283
-
284
- /**
285
- * @private
286
- * @param {Object} toolbar
287
- * @returns {Boolean}
288
- */
289
- _isToolbarInBalloon( toolbar ) {
290
- return this._balloon.hasView( toolbar.view );
291
- }
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ static get requires() {
43
+ return [ContextualBalloon];
44
+ }
45
+ /**
46
+ * @inheritDoc
47
+ */
48
+ static get pluginName() {
49
+ return 'WidgetToolbarRepository';
50
+ }
51
+ /**
52
+ * @inheritDoc
53
+ */
54
+ init() {
55
+ const editor = this.editor;
56
+ // Disables the default balloon toolbar for all widgets.
57
+ if (editor.plugins.has('BalloonToolbar')) {
58
+ const balloonToolbar = editor.plugins.get('BalloonToolbar');
59
+ this.listenTo(balloonToolbar, 'show', evt => {
60
+ if (isWidgetSelected(editor.editing.view.document.selection)) {
61
+ evt.stop();
62
+ }
63
+ }, { priority: 'high' });
64
+ }
65
+ /**
66
+ * A map of toolbar definitions.
67
+ *
68
+ * @protected
69
+ * @member {Map.<String,module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition>} #_toolbarDefinitions
70
+ */
71
+ this._toolbarDefinitions = new Map();
72
+ /**
73
+ * @private
74
+ */
75
+ this._balloon = this.editor.plugins.get('ContextualBalloon');
76
+ this.on('change:isEnabled', () => {
77
+ this._updateToolbarsVisibility();
78
+ });
79
+ this.listenTo(editor.ui, 'update', () => {
80
+ this._updateToolbarsVisibility();
81
+ });
82
+ // UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
83
+ this.listenTo(editor.ui.focusTracker, 'change:isFocused', () => {
84
+ this._updateToolbarsVisibility();
85
+ }, { priority: 'low' });
86
+ }
87
+ destroy() {
88
+ super.destroy();
89
+ for (const toolbarConfig of this._toolbarDefinitions.values()) {
90
+ toolbarConfig.view.destroy();
91
+ }
92
+ }
93
+ /**
94
+ * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
95
+ * `getRelatedElement` function. Toolbar items are gathered from `items` array.
96
+ * The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
97
+ *
98
+ * Note: This method should be called in the {@link module:core/plugin~PluginInterface#afterInit `Plugin#afterInit()`}
99
+ * callback (or later) to make sure that the given toolbar items were already registered by other plugins.
100
+ *
101
+ * @param {String} toolbarId An id for the toolbar. Used to
102
+ * @param {Object} options
103
+ * @param {String} [options.ariaLabel] Label used by assistive technologies to describe this toolbar element.
104
+ * @param {Array.<String>} options.items Array of toolbar items.
105
+ * @param {Function} options.getRelatedElement Callback which returns an element the toolbar should be attached to.
106
+ * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
107
+ */
108
+ register(toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' }) {
109
+ // Trying to register a toolbar without any item.
110
+ if (!items.length) {
111
+ /**
112
+ * When {@link #register registering} a new widget toolbar, you need to provide a non-empty array with
113
+ * the items that will be inserted into the toolbar.
114
+ *
115
+ * If you see this error when integrating the editor, you likely forgot to configure one of the widget toolbars.
116
+ *
117
+ * See for instance:
118
+ *
119
+ * * {@link module:table/table~TableConfig#contentToolbar `config.table.contentToolbar`}
120
+ * * {@link module:image/image~ImageConfig#toolbar `config.image.toolbar`}
121
+ *
122
+ * @error widget-toolbar-no-items
123
+ * @param {String} toolbarId The id of the toolbar that has not been configured correctly.
124
+ */
125
+ logWarning('widget-toolbar-no-items', { toolbarId });
126
+ return;
127
+ }
128
+ const editor = this.editor;
129
+ const t = editor.t;
130
+ const toolbarView = new ToolbarView(editor.locale);
131
+ toolbarView.ariaLabel = ariaLabel || t('Widget toolbar');
132
+ if (this._toolbarDefinitions.has(toolbarId)) {
133
+ /**
134
+ * Toolbar with the given id was already added.
135
+ *
136
+ * @error widget-toolbar-duplicated
137
+ * @param toolbarId Toolbar id.
138
+ */
139
+ throw new CKEditorError('widget-toolbar-duplicated', this, { toolbarId });
140
+ }
141
+ toolbarView.fillFromConfig(items, editor.ui.componentFactory);
142
+ const toolbarDefinition = {
143
+ view: toolbarView,
144
+ getRelatedElement,
145
+ balloonClassName
146
+ };
147
+ // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
148
+ editor.ui.addToolbar(toolbarView, {
149
+ isContextual: true,
150
+ beforeFocus: () => {
151
+ const relatedElement = getRelatedElement(editor.editing.view.document.selection);
152
+ if (relatedElement) {
153
+ this._showToolbar(toolbarDefinition, relatedElement);
154
+ }
155
+ },
156
+ afterBlur: () => {
157
+ this._hideToolbar(toolbarDefinition);
158
+ }
159
+ });
160
+ this._toolbarDefinitions.set(toolbarId, toolbarDefinition);
161
+ }
162
+ /**
163
+ * Iterates over stored toolbars and makes them visible or hidden.
164
+ *
165
+ * @private
166
+ */
167
+ _updateToolbarsVisibility() {
168
+ let maxRelatedElementDepth = 0;
169
+ let deepestRelatedElement = null;
170
+ let deepestToolbarDefinition = null;
171
+ for (const definition of this._toolbarDefinitions.values()) {
172
+ const relatedElement = definition.getRelatedElement(this.editor.editing.view.document.selection);
173
+ if (!this.isEnabled || !relatedElement) {
174
+ if (this._isToolbarInBalloon(definition)) {
175
+ this._hideToolbar(definition);
176
+ }
177
+ }
178
+ else if (!this.editor.ui.focusTracker.isFocused) {
179
+ if (this._isToolbarVisible(definition)) {
180
+ this._hideToolbar(definition);
181
+ }
182
+ }
183
+ else {
184
+ const relatedElementDepth = relatedElement.getAncestors().length;
185
+ // Many toolbars can express willingness to be displayed but they do not know about
186
+ // each other. Figure out which toolbar is deepest in the view tree to decide which
187
+ // should be displayed. For instance, if a selected image is inside a table cell, display
188
+ // the ImageToolbar rather than the TableToolbar (#60).
189
+ if (relatedElementDepth > maxRelatedElementDepth) {
190
+ maxRelatedElementDepth = relatedElementDepth;
191
+ deepestRelatedElement = relatedElement;
192
+ deepestToolbarDefinition = definition;
193
+ }
194
+ }
195
+ }
196
+ if (deepestToolbarDefinition) {
197
+ this._showToolbar(deepestToolbarDefinition, deepestRelatedElement);
198
+ }
199
+ }
200
+ /**
201
+ * Hides the given toolbar.
202
+ *
203
+ * @private
204
+ * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
205
+ */
206
+ _hideToolbar(toolbarDefinition) {
207
+ this._balloon.remove(toolbarDefinition.view);
208
+ this.stopListening(this._balloon, 'change:visibleView');
209
+ }
210
+ /**
211
+ * Shows up the toolbar if the toolbar is not visible.
212
+ * Otherwise, repositions the toolbar's balloon when toolbar's view is the most top view in balloon stack.
213
+ *
214
+ * It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
215
+ * should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
216
+ *
217
+ * @private
218
+ * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
219
+ * @param {module:engine/view/element~Element} relatedElement
220
+ */
221
+ _showToolbar(toolbarDefinition, relatedElement) {
222
+ if (this._isToolbarVisible(toolbarDefinition)) {
223
+ repositionContextualBalloon(this.editor, relatedElement);
224
+ }
225
+ else if (!this._isToolbarInBalloon(toolbarDefinition)) {
226
+ this._balloon.add({
227
+ view: toolbarDefinition.view,
228
+ position: getBalloonPositionData(this.editor, relatedElement),
229
+ balloonClassName: toolbarDefinition.balloonClassName
230
+ });
231
+ // Update toolbar position each time stack with toolbar view is switched to visible.
232
+ // This is in a case target element has changed when toolbar was in invisible stack
233
+ // e.g. target image was wrapped by a block quote.
234
+ // See https://github.com/ckeditor/ckeditor5-widget/issues/92.
235
+ this.listenTo(this._balloon, 'change:visibleView', () => {
236
+ for (const definition of this._toolbarDefinitions.values()) {
237
+ if (this._isToolbarVisible(definition)) {
238
+ const relatedElement = definition.getRelatedElement(this.editor.editing.view.document.selection);
239
+ repositionContextualBalloon(this.editor, relatedElement);
240
+ }
241
+ }
242
+ });
243
+ }
244
+ }
245
+ /**
246
+ * @private
247
+ * @param {Object} toolbar
248
+ * @returns {Boolean}
249
+ */
250
+ _isToolbarVisible(toolbar) {
251
+ return this._balloon.visibleView === toolbar.view;
252
+ }
253
+ /**
254
+ * @private
255
+ * @param {Object} toolbar
256
+ * @returns {Boolean}
257
+ */
258
+ _isToolbarInBalloon(toolbar) {
259
+ return this._balloon.hasView(toolbar.view);
260
+ }
292
261
  }
293
-
294
- function repositionContextualBalloon( editor, relatedElement ) {
295
- const balloon = editor.plugins.get( 'ContextualBalloon' );
296
- const position = getBalloonPositionData( editor, relatedElement );
297
-
298
- balloon.updatePosition( position );
262
+ function repositionContextualBalloon(editor, relatedElement) {
263
+ const balloon = editor.plugins.get('ContextualBalloon');
264
+ const position = getBalloonPositionData(editor, relatedElement);
265
+ balloon.updatePosition(position);
299
266
  }
300
-
301
- function getBalloonPositionData( editor, relatedElement ) {
302
- const editingView = editor.editing.view;
303
- const defaultPositions = BalloonPanelView.defaultPositions;
304
-
305
- return {
306
- target: editingView.domConverter.mapViewToDom( relatedElement ),
307
- positions: [
308
- defaultPositions.northArrowSouth,
309
- defaultPositions.northArrowSouthWest,
310
- defaultPositions.northArrowSouthEast,
311
- defaultPositions.southArrowNorth,
312
- defaultPositions.southArrowNorthWest,
313
- defaultPositions.southArrowNorthEast,
314
- defaultPositions.viewportStickyNorth
315
- ]
316
- };
267
+ function getBalloonPositionData(editor, relatedElement) {
268
+ const editingView = editor.editing.view;
269
+ const defaultPositions = BalloonPanelView.defaultPositions;
270
+ return {
271
+ target: editingView.domConverter.mapViewToDom(relatedElement),
272
+ positions: [
273
+ defaultPositions.northArrowSouth,
274
+ defaultPositions.northArrowSouthWest,
275
+ defaultPositions.northArrowSouthEast,
276
+ defaultPositions.southArrowNorth,
277
+ defaultPositions.southArrowNorthWest,
278
+ defaultPositions.southArrowNorthEast,
279
+ defaultPositions.viewportStickyNorth
280
+ ]
281
+ };
317
282
  }
318
-
319
- function isWidgetSelected( selection ) {
320
- const viewElement = selection.getSelectedElement();
321
-
322
- return !!( viewElement && isWidget( viewElement ) );
283
+ function isWidgetSelected(selection) {
284
+ const viewElement = selection.getSelectedElement();
285
+ return !!(viewElement && isWidget(viewElement));
323
286
  }
324
-
325
- /**
326
- * The toolbar definition object used by the toolbar repository to manage toolbars.
327
- * It contains information necessary to display the toolbar in the
328
- * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} and
329
- * update it during its life (display) cycle.
330
- *
331
- * @typedef {Object} module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition
332
- *
333
- * @property {module:ui/view~View} view The UI view of the toolbar.
334
- * @property {Function} getRelatedElement A function that returns an engine {@link module:engine/view/view~View}
335
- * element the toolbar is to be attached to. For instance, an image widget or a table widget (or `null` when
336
- * there is no such element). The function accepts an instance of {@link module:engine/view/selection~Selection}.
337
- * @property {String} balloonClassName CSS class for the widget balloon when a toolbar is displayed.
338
- */