@ckeditor/ckeditor5-widget 35.2.0 → 35.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/widget.js CHANGED
@@ -2,23 +2,18 @@
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/widget
8
7
  */
9
-
10
8
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11
9
  import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
12
10
  import WidgetTypeAround from './widgettypearound/widgettypearound';
13
11
  import Delete from '@ckeditor/ckeditor5-typing/src/delete';
14
12
  import env from '@ckeditor/ckeditor5-utils/src/env';
15
13
  import { getLocalizedArrowKeyCodeDirection } from '@ckeditor/ckeditor5-utils/src/keyboard';
16
-
17
14
  import verticalNavigationHandler from './verticalnavigation';
18
15
  import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
19
-
20
16
  import '../theme/widget.css';
21
-
22
17
  /**
23
18
  * The widget plugin. It enables base support for widgets.
24
19
  *
@@ -35,445 +30,374 @@ import '../theme/widget.css';
35
30
  * @extends module:core/plugin~Plugin
36
31
  */
37
32
  export default class Widget extends Plugin {
38
- /**
39
- * @inheritDoc
40
- */
41
- static get pluginName() {
42
- return 'Widget';
43
- }
44
-
45
- /**
46
- * @inheritDoc
47
- */
48
- static get requires() {
49
- return [ WidgetTypeAround, Delete ];
50
- }
51
-
52
- /**
53
- * @inheritDoc
54
- */
55
- init() {
56
- const editor = this.editor;
57
- const view = editor.editing.view;
58
- const viewDocument = view.document;
59
-
60
- /**
61
- * Holds previously selected widgets.
62
- *
63
- * @private
64
- * @type {Set.<module:engine/view/element~Element>}
65
- */
66
- this._previouslySelected = new Set();
67
-
68
- // Model to view selection converter.
69
- // Converts selection placed over widget element to fake selection.
70
- //
71
- // By default, the selection is downcasted by the engine to surround the attribute element, even though its only
72
- // child is an inline widget. A similar thing also happens when a collapsed marker is rendered as a UI element
73
- // next to an inline widget: the view selection contains both the widget and the marker.
74
- //
75
- // This prevents creating a correct fake selection when this inline widget is selected. Normalize the selection
76
- // in these cases based on the model:
77
- //
78
- // [<attributeElement><inlineWidget /></attributeElement>] -> <attributeElement>[<inlineWidget />]</attributeElement>
79
- // [<uiElement></uiElement><inlineWidget />] -> <uiElement></uiElement>[<inlineWidget />]
80
- //
81
- // Thanks to this:
82
- //
83
- // * fake selection can be set correctly,
84
- // * any logic depending on (View)Selection#getSelectedElement() also works OK.
85
- //
86
- // See https://github.com/ckeditor/ckeditor5/issues/9524.
87
- this.editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
88
- const viewWriter = conversionApi.writer;
89
- const modelSelection = data.selection;
90
-
91
- // The collapsed selection can't contain any widget.
92
- if ( modelSelection.isCollapsed ) {
93
- return;
94
- }
95
-
96
- const selectedModelElement = modelSelection.getSelectedElement();
97
-
98
- if ( !selectedModelElement ) {
99
- return;
100
- }
101
-
102
- const selectedViewElement = editor.editing.mapper.toViewElement( selectedModelElement );
103
-
104
- if ( !isWidget( selectedViewElement ) ) {
105
- return;
106
- }
107
-
108
- if ( !conversionApi.consumable.consume( modelSelection, 'selection' ) ) {
109
- return;
110
- }
111
-
112
- viewWriter.setSelection( viewWriter.createRangeOn( selectedViewElement ), {
113
- fake: true,
114
- label: getLabel( selectedViewElement )
115
- } );
116
- } );
117
-
118
- // Mark all widgets inside the selection with the css class.
119
- // This handler is registered at the 'low' priority so it's triggered after the real selection conversion.
120
- this.editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
121
- // Remove selected class from previously selected widgets.
122
- this._clearPreviouslySelectedWidgets( conversionApi.writer );
123
-
124
- const viewWriter = conversionApi.writer;
125
- const viewSelection = viewWriter.document.selection;
126
-
127
- let lastMarked = null;
128
-
129
- for ( const range of viewSelection.getRanges() ) {
130
- // Note: There could be multiple selected widgets in a range but no fake selection.
131
- // All of them must be marked as selected, for instance [<widget></widget><widget></widget>]
132
- for ( const value of range ) {
133
- const node = value.item;
134
- // Do not mark nested widgets in selected one. See: #4594
135
- if ( isWidget( node ) && !isChild( node, lastMarked ) ) {
136
- viewWriter.addClass( WIDGET_SELECTED_CLASS_NAME, node );
137
- this._previouslySelected.add( node );
138
- lastMarked = node;
139
- }
140
- }
141
- }
142
- }, { priority: 'low' } );
143
-
144
- // If mouse down is pressed on widget - create selection over whole widget.
145
- view.addObserver( MouseObserver );
146
- this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
147
-
148
- // There are two keydown listeners working on different priorities. This allows other
149
- // features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
150
- // and customize the behavior even further in different content/selection scenarios.
151
- //
152
- // * The first listener handles changing the selection on arrow key press
153
- // if the widget is selected or if the selection is next to a widget and the widget
154
- // should become selected upon the arrow key press.
155
- //
156
- // * The second (late) listener makes sure the default browser action on arrow key press is
157
- // prevented when a widget is selected. This prevents the selection from being moved
158
- // from a fake selection container.
159
- this.listenTo( viewDocument, 'arrowKey', ( ...args ) => {
160
- this._handleSelectionChangeOnArrowKeyPress( ...args );
161
- }, { context: [ isWidget, '$text' ] } );
162
-
163
- this.listenTo( viewDocument, 'arrowKey', ( ...args ) => {
164
- this._preventDefaultOnArrowKeyPress( ...args );
165
- }, { context: '$root' } );
166
-
167
- this.listenTo( viewDocument, 'arrowKey', verticalNavigationHandler( this.editor.editing ), { context: '$text' } );
168
-
169
- // Handle custom delete behaviour.
170
- this.listenTo( viewDocument, 'delete', ( evt, data ) => {
171
- if ( this._handleDelete( data.direction == 'forward' ) ) {
172
- data.preventDefault();
173
- evt.stop();
174
- }
175
- }, { context: '$root' } );
176
- }
177
-
178
- /**
179
- * Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements.
180
- *
181
- * @private
182
- * @param {module:utils/eventinfo~EventInfo} eventInfo
183
- * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
184
- */
185
- _onMousedown( eventInfo, domEventData ) {
186
- const editor = this.editor;
187
- const view = editor.editing.view;
188
- const viewDocument = view.document;
189
- let element = domEventData.target;
190
-
191
- // Do nothing for single or double click inside nested editable.
192
- if ( isInsideNestedEditable( element ) ) {
193
- // But at least triple click inside nested editable causes broken selection in Safari.
194
- // For such event, we select the entire nested editable element.
195
- // See: https://github.com/ckeditor/ckeditor5/issues/1463.
196
- if ( ( env.isSafari || env.isGecko ) && domEventData.domEvent.detail >= 3 ) {
197
- const mapper = editor.editing.mapper;
198
- const viewElement = element.is( 'attributeElement' ) ?
199
- element.findAncestor( element => !element.is( 'attributeElement' ) ) : element;
200
- const modelElement = mapper.toModelElement( viewElement );
201
-
202
- domEventData.preventDefault();
203
-
204
- this.editor.model.change( writer => {
205
- writer.setSelection( modelElement, 'in' );
206
- } );
207
- }
208
-
209
- return;
210
- }
211
-
212
- // If target is not a widget element - check if one of the ancestors is.
213
- if ( !isWidget( element ) ) {
214
- element = element.findAncestor( isWidget );
215
-
216
- if ( !element ) {
217
- return;
218
- }
219
- }
220
-
221
- // On Android selection would jump to the first table cell, on other devices
222
- // we can't block it (and don't need to) because of drag and drop support.
223
- if ( env.isAndroid ) {
224
- domEventData.preventDefault();
225
- }
226
-
227
- // Focus editor if is not focused already.
228
- if ( !viewDocument.isFocused ) {
229
- view.focus();
230
- }
231
-
232
- // Create model selection over widget.
233
- const modelElement = editor.editing.mapper.toModelElement( element );
234
-
235
- this._setSelectionOverElement( modelElement );
236
- }
237
-
238
- /**
239
- * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
240
- * the model selection when:
241
- *
242
- * * arrow key is pressed when the widget is selected,
243
- * * the selection is next to a widget and the widget should become selected upon the arrow key press.
244
- *
245
- * See {@link #_preventDefaultOnArrowKeyPress}.
246
- *
247
- * @private
248
- * @param {module:utils/eventinfo~EventInfo} eventInfo
249
- * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
250
- */
251
- _handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) {
252
- const keyCode = domEventData.keyCode;
253
-
254
- const model = this.editor.model;
255
- const schema = model.schema;
256
- const modelSelection = model.document.selection;
257
- const objectElement = modelSelection.getSelectedElement();
258
- const direction = getLocalizedArrowKeyCodeDirection( keyCode, this.editor.locale.contentLanguageDirection );
259
- const isForward = direction == 'down' || direction == 'right';
260
- const isVerticalNavigation = direction == 'up' || direction == 'down';
261
-
262
- // If object element is selected.
263
- if ( objectElement && schema.isObject( objectElement ) ) {
264
- const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
265
- const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
266
-
267
- if ( newRange ) {
268
- model.change( writer => {
269
- writer.setSelection( newRange );
270
- } );
271
-
272
- domEventData.preventDefault();
273
- eventInfo.stop();
274
- }
275
-
276
- return;
277
- }
278
-
279
- // Handle collapsing of the selection when there is any widget on the edge of selection.
280
- // This is needed because browsers have problems with collapsing such selection.
281
- if ( !modelSelection.isCollapsed && !domEventData.shiftKey ) {
282
- const firstPosition = modelSelection.getFirstPosition();
283
- const lastPosition = modelSelection.getLastPosition();
284
-
285
- const firstSelectedNode = firstPosition.nodeAfter;
286
- const lastSelectedNode = lastPosition.nodeBefore;
287
-
288
- if ( firstSelectedNode && schema.isObject( firstSelectedNode ) || lastSelectedNode && schema.isObject( lastSelectedNode ) ) {
289
- model.change( writer => {
290
- writer.setSelection( isForward ? lastPosition : firstPosition );
291
- } );
292
-
293
- domEventData.preventDefault();
294
- eventInfo.stop();
295
- }
296
-
297
- return;
298
- }
299
-
300
- // Return if not collapsed.
301
- if ( !modelSelection.isCollapsed ) {
302
- return;
303
- }
304
-
305
- // If selection is next to object element.
306
-
307
- const objectElementNextToSelection = this._getObjectElementNextToSelection( isForward );
308
-
309
- if ( objectElementNextToSelection && schema.isObject( objectElementNextToSelection ) ) {
310
- // Do not select an inline widget while handling up/down arrow.
311
- if ( schema.isInline( objectElementNextToSelection ) && isVerticalNavigation ) {
312
- return;
313
- }
314
-
315
- this._setSelectionOverElement( objectElementNextToSelection );
316
-
317
- domEventData.preventDefault();
318
- eventInfo.stop();
319
- }
320
- }
321
-
322
- /**
323
- * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
324
- * the default browser behavior to make sure the fake selection is not being moved from a fake selection
325
- * container.
326
- *
327
- * See {@link #_handleSelectionChangeOnArrowKeyPress}.
328
- *
329
- * @private
330
- * @param {module:utils/eventinfo~EventInfo} eventInfo
331
- * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
332
- */
333
- _preventDefaultOnArrowKeyPress( eventInfo, domEventData ) {
334
- const model = this.editor.model;
335
- const schema = model.schema;
336
- const objectElement = model.document.selection.getSelectedElement();
337
-
338
- // If object element is selected.
339
- if ( objectElement && schema.isObject( objectElement ) ) {
340
- domEventData.preventDefault();
341
- eventInfo.stop();
342
- }
343
- }
344
-
345
- /**
346
- * Handles delete keys: backspace and delete.
347
- *
348
- * @private
349
- * @param {Boolean} isForward Set to true if delete was performed in forward direction.
350
- * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
351
- */
352
- _handleDelete( isForward ) {
353
- // Do nothing when the read only mode is enabled.
354
- if ( this.editor.isReadOnly ) {
355
- return;
356
- }
357
-
358
- const modelDocument = this.editor.model.document;
359
- const modelSelection = modelDocument.selection;
360
-
361
- // Do nothing on non-collapsed selection.
362
- if ( !modelSelection.isCollapsed ) {
363
- return;
364
- }
365
-
366
- const objectElement = this._getObjectElementNextToSelection( isForward );
367
-
368
- if ( objectElement ) {
369
- this.editor.model.change( writer => {
370
- let previousNode = modelSelection.anchor.parent;
371
-
372
- // Remove previous element if empty.
373
- while ( previousNode.isEmpty ) {
374
- const nodeToRemove = previousNode;
375
- previousNode = nodeToRemove.parent;
376
-
377
- writer.remove( nodeToRemove );
378
- }
379
-
380
- this._setSelectionOverElement( objectElement );
381
- } );
382
-
383
- return true;
384
- }
385
- }
386
-
387
- /**
388
- * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
389
- *
390
- * @protected
391
- * @param {module:engine/model/element~Element} element
392
- */
393
- _setSelectionOverElement( element ) {
394
- this.editor.model.change( writer => {
395
- writer.setSelection( writer.createRangeOn( element ) );
396
- } );
397
- }
398
-
399
- /**
400
- * Checks if {@link module:engine/model/element~Element element} placed next to the current
401
- * {@link module:engine/model/selection~Selection model selection} exists and is marked in
402
- * {@link module:engine/model/schema~Schema schema} as `object`.
403
- *
404
- * @protected
405
- * @param {Boolean} forward Direction of checking.
406
- * @returns {module:engine/model/element~Element|null}
407
- */
408
- _getObjectElementNextToSelection( forward ) {
409
- const model = this.editor.model;
410
- const schema = model.schema;
411
- const modelSelection = model.document.selection;
412
-
413
- // Clone current selection to use it as a probe. We must leave default selection as it is so it can return
414
- // to its current state after undo.
415
- const probe = model.createSelection( modelSelection );
416
- model.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } );
417
-
418
- // The selection didn't change so there is nothing there.
419
- if ( probe.isEqual( modelSelection ) ) {
420
- return null;
421
- }
422
-
423
- const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
424
-
425
- if ( !!objectElement && schema.isObject( objectElement ) ) {
426
- return objectElement;
427
- }
428
-
429
- return null;
430
- }
431
-
432
- /**
433
- * Removes CSS class from previously selected widgets.
434
- *
435
- * @private
436
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
437
- */
438
- _clearPreviouslySelectedWidgets( writer ) {
439
- for ( const widget of this._previouslySelected ) {
440
- writer.removeClass( WIDGET_SELECTED_CLASS_NAME, widget );
441
- }
442
-
443
- this._previouslySelected.clear();
444
- }
33
+ /**
34
+ * @inheritDoc
35
+ */
36
+ static get pluginName() {
37
+ return 'Widget';
38
+ }
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ static get requires() {
43
+ return [WidgetTypeAround, Delete];
44
+ }
45
+ /**
46
+ * @inheritDoc
47
+ */
48
+ init() {
49
+ const editor = this.editor;
50
+ const view = editor.editing.view;
51
+ const viewDocument = view.document;
52
+ /**
53
+ * Holds previously selected widgets.
54
+ *
55
+ * @private
56
+ * @type {Set.<module:engine/view/element~Element>}
57
+ */
58
+ this._previouslySelected = new Set();
59
+ // Model to view selection converter.
60
+ // Converts selection placed over widget element to fake selection.
61
+ //
62
+ // By default, the selection is downcasted by the engine to surround the attribute element, even though its only
63
+ // child is an inline widget. A similar thing also happens when a collapsed marker is rendered as a UI element
64
+ // next to an inline widget: the view selection contains both the widget and the marker.
65
+ //
66
+ // This prevents creating a correct fake selection when this inline widget is selected. Normalize the selection
67
+ // in these cases based on the model:
68
+ //
69
+ // [<attributeElement><inlineWidget /></attributeElement>] -> <attributeElement>[<inlineWidget />]</attributeElement>
70
+ // [<uiElement></uiElement><inlineWidget />] -> <uiElement></uiElement>[<inlineWidget />]
71
+ //
72
+ // Thanks to this:
73
+ //
74
+ // * fake selection can be set correctly,
75
+ // * any logic depending on (View)Selection#getSelectedElement() also works OK.
76
+ //
77
+ // See https://github.com/ckeditor/ckeditor5/issues/9524.
78
+ this.editor.editing.downcastDispatcher.on('selection', (evt, data, conversionApi) => {
79
+ const viewWriter = conversionApi.writer;
80
+ const modelSelection = data.selection;
81
+ // The collapsed selection can't contain any widget.
82
+ if (modelSelection.isCollapsed) {
83
+ return;
84
+ }
85
+ const selectedModelElement = modelSelection.getSelectedElement();
86
+ if (!selectedModelElement) {
87
+ return;
88
+ }
89
+ const selectedViewElement = editor.editing.mapper.toViewElement(selectedModelElement);
90
+ if (!isWidget(selectedViewElement)) {
91
+ return;
92
+ }
93
+ if (!conversionApi.consumable.consume(modelSelection, 'selection')) {
94
+ return;
95
+ }
96
+ viewWriter.setSelection(viewWriter.createRangeOn(selectedViewElement), {
97
+ fake: true,
98
+ label: getLabel(selectedViewElement)
99
+ });
100
+ });
101
+ // Mark all widgets inside the selection with the css class.
102
+ // This handler is registered at the 'low' priority so it's triggered after the real selection conversion.
103
+ this.editor.editing.downcastDispatcher.on('selection', (evt, data, conversionApi) => {
104
+ // Remove selected class from previously selected widgets.
105
+ this._clearPreviouslySelectedWidgets(conversionApi.writer);
106
+ const viewWriter = conversionApi.writer;
107
+ const viewSelection = viewWriter.document.selection;
108
+ let lastMarked = null;
109
+ for (const range of viewSelection.getRanges()) {
110
+ // Note: There could be multiple selected widgets in a range but no fake selection.
111
+ // All of them must be marked as selected, for instance [<widget></widget><widget></widget>]
112
+ for (const value of range) {
113
+ const node = value.item;
114
+ // Do not mark nested widgets in selected one. See: #4594
115
+ if (isWidget(node) && !isChild(node, lastMarked)) {
116
+ viewWriter.addClass(WIDGET_SELECTED_CLASS_NAME, node);
117
+ this._previouslySelected.add(node);
118
+ lastMarked = node;
119
+ }
120
+ }
121
+ }
122
+ }, { priority: 'low' });
123
+ // If mouse down is pressed on widget - create selection over whole widget.
124
+ view.addObserver(MouseObserver);
125
+ this.listenTo(viewDocument, 'mousedown', (...args) => this._onMousedown(...args));
126
+ // There are two keydown listeners working on different priorities. This allows other
127
+ // features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
128
+ // and customize the behavior even further in different content/selection scenarios.
129
+ //
130
+ // * The first listener handles changing the selection on arrow key press
131
+ // if the widget is selected or if the selection is next to a widget and the widget
132
+ // should become selected upon the arrow key press.
133
+ //
134
+ // * The second (late) listener makes sure the default browser action on arrow key press is
135
+ // prevented when a widget is selected. This prevents the selection from being moved
136
+ // from a fake selection container.
137
+ this.listenTo(viewDocument, 'arrowKey', (...args) => {
138
+ this._handleSelectionChangeOnArrowKeyPress(...args);
139
+ }, { context: [isWidget, '$text'] });
140
+ this.listenTo(viewDocument, 'arrowKey', (...args) => {
141
+ this._preventDefaultOnArrowKeyPress(...args);
142
+ }, { context: '$root' });
143
+ this.listenTo(viewDocument, 'arrowKey', verticalNavigationHandler(this.editor.editing), { context: '$text' });
144
+ // Handle custom delete behaviour.
145
+ this.listenTo(viewDocument, 'delete', (evt, data) => {
146
+ if (this._handleDelete(data.direction == 'forward')) {
147
+ data.preventDefault();
148
+ evt.stop();
149
+ }
150
+ }, { context: '$root' });
151
+ }
152
+ /**
153
+ * Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements.
154
+ *
155
+ * @private
156
+ * @param {module:utils/eventinfo~EventInfo} eventInfo
157
+ * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
158
+ */
159
+ _onMousedown(eventInfo, domEventData) {
160
+ const editor = this.editor;
161
+ const view = editor.editing.view;
162
+ const viewDocument = view.document;
163
+ let element = domEventData.target;
164
+ // Do nothing for single or double click inside nested editable.
165
+ if (isInsideNestedEditable(element)) {
166
+ // But at least triple click inside nested editable causes broken selection in Safari.
167
+ // For such event, we select the entire nested editable element.
168
+ // See: https://github.com/ckeditor/ckeditor5/issues/1463.
169
+ if ((env.isSafari || env.isGecko) && domEventData.domEvent.detail >= 3) {
170
+ const mapper = editor.editing.mapper;
171
+ const viewElement = element.is('attributeElement') ?
172
+ element.findAncestor(element => !element.is('attributeElement')) : element;
173
+ const modelElement = mapper.toModelElement(viewElement);
174
+ domEventData.preventDefault();
175
+ this.editor.model.change(writer => {
176
+ writer.setSelection(modelElement, 'in');
177
+ });
178
+ }
179
+ return;
180
+ }
181
+ // If target is not a widget element - check if one of the ancestors is.
182
+ if (!isWidget(element)) {
183
+ element = element.findAncestor(isWidget);
184
+ if (!element) {
185
+ return;
186
+ }
187
+ }
188
+ // On Android selection would jump to the first table cell, on other devices
189
+ // we can't block it (and don't need to) because of drag and drop support.
190
+ if (env.isAndroid) {
191
+ domEventData.preventDefault();
192
+ }
193
+ // Focus editor if is not focused already.
194
+ if (!viewDocument.isFocused) {
195
+ view.focus();
196
+ }
197
+ // Create model selection over widget.
198
+ const modelElement = editor.editing.mapper.toModelElement(element);
199
+ this._setSelectionOverElement(modelElement);
200
+ }
201
+ /**
202
+ * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
203
+ * the model selection when:
204
+ *
205
+ * * arrow key is pressed when the widget is selected,
206
+ * * the selection is next to a widget and the widget should become selected upon the arrow key press.
207
+ *
208
+ * See {@link #_preventDefaultOnArrowKeyPress}.
209
+ *
210
+ * @private
211
+ * @param {module:utils/eventinfo~EventInfo} eventInfo
212
+ * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
213
+ */
214
+ _handleSelectionChangeOnArrowKeyPress(eventInfo, domEventData) {
215
+ const keyCode = domEventData.keyCode;
216
+ const model = this.editor.model;
217
+ const schema = model.schema;
218
+ const modelSelection = model.document.selection;
219
+ const objectElement = modelSelection.getSelectedElement();
220
+ const direction = getLocalizedArrowKeyCodeDirection(keyCode, this.editor.locale.contentLanguageDirection);
221
+ const isForward = direction == 'down' || direction == 'right';
222
+ const isVerticalNavigation = direction == 'up' || direction == 'down';
223
+ // If object element is selected.
224
+ if (objectElement && schema.isObject(objectElement)) {
225
+ const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
226
+ const newRange = schema.getNearestSelectionRange(position, isForward ? 'forward' : 'backward');
227
+ if (newRange) {
228
+ model.change(writer => {
229
+ writer.setSelection(newRange);
230
+ });
231
+ domEventData.preventDefault();
232
+ eventInfo.stop();
233
+ }
234
+ return;
235
+ }
236
+ // Handle collapsing of the selection when there is any widget on the edge of selection.
237
+ // This is needed because browsers have problems with collapsing such selection.
238
+ if (!modelSelection.isCollapsed && !domEventData.shiftKey) {
239
+ const firstPosition = modelSelection.getFirstPosition();
240
+ const lastPosition = modelSelection.getLastPosition();
241
+ const firstSelectedNode = firstPosition.nodeAfter;
242
+ const lastSelectedNode = lastPosition.nodeBefore;
243
+ if (firstSelectedNode && schema.isObject(firstSelectedNode) || lastSelectedNode && schema.isObject(lastSelectedNode)) {
244
+ model.change(writer => {
245
+ writer.setSelection(isForward ? lastPosition : firstPosition);
246
+ });
247
+ domEventData.preventDefault();
248
+ eventInfo.stop();
249
+ }
250
+ return;
251
+ }
252
+ // Return if not collapsed.
253
+ if (!modelSelection.isCollapsed) {
254
+ return;
255
+ }
256
+ // If selection is next to object element.
257
+ const objectElementNextToSelection = this._getObjectElementNextToSelection(isForward);
258
+ if (objectElementNextToSelection && schema.isObject(objectElementNextToSelection)) {
259
+ // Do not select an inline widget while handling up/down arrow.
260
+ if (schema.isInline(objectElementNextToSelection) && isVerticalNavigation) {
261
+ return;
262
+ }
263
+ this._setSelectionOverElement(objectElementNextToSelection);
264
+ domEventData.preventDefault();
265
+ eventInfo.stop();
266
+ }
267
+ }
268
+ /**
269
+ * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
270
+ * the default browser behavior to make sure the fake selection is not being moved from a fake selection
271
+ * container.
272
+ *
273
+ * See {@link #_handleSelectionChangeOnArrowKeyPress}.
274
+ *
275
+ * @private
276
+ * @param {module:utils/eventinfo~EventInfo} eventInfo
277
+ * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
278
+ */
279
+ _preventDefaultOnArrowKeyPress(eventInfo, domEventData) {
280
+ const model = this.editor.model;
281
+ const schema = model.schema;
282
+ const objectElement = model.document.selection.getSelectedElement();
283
+ // If object element is selected.
284
+ if (objectElement && schema.isObject(objectElement)) {
285
+ domEventData.preventDefault();
286
+ eventInfo.stop();
287
+ }
288
+ }
289
+ /**
290
+ * Handles delete keys: backspace and delete.
291
+ *
292
+ * @private
293
+ * @param {Boolean} isForward Set to true if delete was performed in forward direction.
294
+ * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
295
+ */
296
+ _handleDelete(isForward) {
297
+ // Do nothing when the read only mode is enabled.
298
+ if (this.editor.isReadOnly) {
299
+ return;
300
+ }
301
+ const modelDocument = this.editor.model.document;
302
+ const modelSelection = modelDocument.selection;
303
+ // Do nothing on non-collapsed selection.
304
+ if (!modelSelection.isCollapsed) {
305
+ return;
306
+ }
307
+ const objectElement = this._getObjectElementNextToSelection(isForward);
308
+ if (objectElement) {
309
+ this.editor.model.change(writer => {
310
+ let previousNode = modelSelection.anchor.parent;
311
+ // Remove previous element if empty.
312
+ while (previousNode.isEmpty) {
313
+ const nodeToRemove = previousNode;
314
+ previousNode = nodeToRemove.parent;
315
+ writer.remove(nodeToRemove);
316
+ }
317
+ this._setSelectionOverElement(objectElement);
318
+ });
319
+ return true;
320
+ }
321
+ }
322
+ /**
323
+ * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
324
+ *
325
+ * @internal
326
+ * @protected
327
+ * @param {module:engine/model/element~Element} element
328
+ */
329
+ _setSelectionOverElement(element) {
330
+ this.editor.model.change(writer => {
331
+ writer.setSelection(writer.createRangeOn(element));
332
+ });
333
+ }
334
+ /**
335
+ * Checks if {@link module:engine/model/element~Element element} placed next to the current
336
+ * {@link module:engine/model/selection~Selection model selection} exists and is marked in
337
+ * {@link module:engine/model/schema~Schema schema} as `object`.
338
+ *
339
+ * @internal
340
+ * @protected
341
+ * @param {Boolean} forward Direction of checking.
342
+ * @returns {module:engine/model/element~Element|null}
343
+ */
344
+ _getObjectElementNextToSelection(forward) {
345
+ const model = this.editor.model;
346
+ const schema = model.schema;
347
+ const modelSelection = model.document.selection;
348
+ // Clone current selection to use it as a probe. We must leave default selection as it is so it can return
349
+ // to its current state after undo.
350
+ const probe = model.createSelection(modelSelection);
351
+ model.modifySelection(probe, { direction: forward ? 'forward' : 'backward' });
352
+ // The selection didn't change so there is nothing there.
353
+ if (probe.isEqual(modelSelection)) {
354
+ return null;
355
+ }
356
+ const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
357
+ if (!!objectElement && schema.isObject(objectElement)) {
358
+ return objectElement;
359
+ }
360
+ return null;
361
+ }
362
+ /**
363
+ * Removes CSS class from previously selected widgets.
364
+ *
365
+ * @private
366
+ * @param {module:engine/view/downcastwriter~DowncastWriter} writer
367
+ */
368
+ _clearPreviouslySelectedWidgets(writer) {
369
+ for (const widget of this._previouslySelected) {
370
+ writer.removeClass(WIDGET_SELECTED_CLASS_NAME, widget);
371
+ }
372
+ this._previouslySelected.clear();
373
+ }
445
374
  }
446
-
447
375
  // Returns `true` when element is a nested editable or is placed inside one.
448
376
  //
449
377
  // @param {module:engine/view/element~Element}
450
378
  // @returns {Boolean}
451
- function isInsideNestedEditable( element ) {
452
- while ( element ) {
453
- if ( element.is( 'editableElement' ) && !element.is( 'rootElement' ) ) {
454
- return true;
455
- }
456
-
457
- // Click on nested widget should select it.
458
- if ( isWidget( element ) ) {
459
- return false;
460
- }
461
-
462
- element = element.parent;
463
- }
464
-
465
- return false;
379
+ function isInsideNestedEditable(element) {
380
+ let currentElement = element;
381
+ while (currentElement) {
382
+ if (currentElement.is('editableElement') && !currentElement.is('rootElement')) {
383
+ return true;
384
+ }
385
+ // Click on nested widget should select it.
386
+ if (isWidget(currentElement)) {
387
+ return false;
388
+ }
389
+ currentElement = currentElement.parent;
390
+ }
391
+ return false;
466
392
  }
467
-
468
393
  // Checks whether the specified `element` is a child of the `parent` element.
469
394
  //
470
395
  // @param {module:engine/view/element~Element} element An element to check.
471
396
  // @param {module:engine/view/element~Element|null} parent A parent for the element.
472
397
  // @returns {Boolean}
473
- function isChild( element, parent ) {
474
- if ( !parent ) {
475
- return false;
476
- }
477
-
478
- return Array.from( element.getAncestors() ).includes( parent );
398
+ function isChild(element, parent) {
399
+ if (!parent) {
400
+ return false;
401
+ }
402
+ return Array.from(element.getAncestors()).includes(parent);
479
403
  }