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