@ckeditor/ckeditor5-widget 39.0.1 → 40.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +3 -3
  4. package/lang/translations/ar.po +1 -0
  5. package/lang/translations/az.po +1 -0
  6. package/lang/translations/bg.po +1 -0
  7. package/lang/translations/bn.po +1 -0
  8. package/lang/translations/ca.po +1 -0
  9. package/lang/translations/cs.po +1 -0
  10. package/lang/translations/da.po +1 -0
  11. package/lang/translations/de-ch.po +1 -0
  12. package/lang/translations/de.po +1 -0
  13. package/lang/translations/el.po +1 -0
  14. package/lang/translations/en-au.po +1 -0
  15. package/lang/translations/en.po +1 -0
  16. package/lang/translations/es.po +1 -0
  17. package/lang/translations/et.po +1 -0
  18. package/lang/translations/fa.po +1 -0
  19. package/lang/translations/fi.po +1 -0
  20. package/lang/translations/fr.po +1 -0
  21. package/lang/translations/gl.po +1 -0
  22. package/lang/translations/he.po +1 -0
  23. package/lang/translations/hi.po +1 -0
  24. package/lang/translations/hr.po +1 -0
  25. package/lang/translations/hu.po +1 -0
  26. package/lang/translations/id.po +1 -0
  27. package/lang/translations/it.po +1 -0
  28. package/lang/translations/ja.po +1 -0
  29. package/lang/translations/ko.po +1 -0
  30. package/lang/translations/ku.po +1 -0
  31. package/lang/translations/lt.po +1 -0
  32. package/lang/translations/lv.po +1 -0
  33. package/lang/translations/ms.po +1 -0
  34. package/lang/translations/nl.po +1 -0
  35. package/lang/translations/no.po +1 -0
  36. package/lang/translations/pl.po +1 -0
  37. package/lang/translations/pt-br.po +1 -0
  38. package/lang/translations/pt.po +1 -0
  39. package/lang/translations/ro.po +1 -0
  40. package/lang/translations/ru.po +1 -0
  41. package/lang/translations/sk.po +1 -0
  42. package/lang/translations/sq.po +1 -0
  43. package/lang/translations/sr-latn.po +1 -0
  44. package/lang/translations/sr.po +1 -0
  45. package/lang/translations/sv.po +1 -0
  46. package/lang/translations/th.po +1 -0
  47. package/lang/translations/tk.po +1 -0
  48. package/lang/translations/tr.po +1 -0
  49. package/lang/translations/uk.po +1 -0
  50. package/lang/translations/ur.po +1 -0
  51. package/lang/translations/uz.po +1 -0
  52. package/lang/translations/vi.po +1 -0
  53. package/lang/translations/zh-cn.po +1 -0
  54. package/lang/translations/zh.po +1 -0
  55. package/package.json +7 -11
  56. package/src/augmentation.d.ts +13 -13
  57. package/src/augmentation.js +5 -5
  58. package/src/highlightstack.d.ts +74 -74
  59. package/src/highlightstack.js +129 -129
  60. package/src/index.d.ts +13 -13
  61. package/src/index.js +13 -13
  62. package/src/utils.d.ts +198 -198
  63. package/src/utils.js +348 -348
  64. package/src/verticalnavigation.d.ts +15 -15
  65. package/src/verticalnavigation.js +196 -196
  66. package/src/widget.d.ts +91 -91
  67. package/src/widget.js +380 -380
  68. package/src/widgetresize/resizer.d.ts +177 -177
  69. package/src/widgetresize/resizer.js +372 -372
  70. package/src/widgetresize/resizerstate.d.ts +125 -125
  71. package/src/widgetresize/resizerstate.js +150 -150
  72. package/src/widgetresize/sizeview.d.ts +55 -55
  73. package/src/widgetresize/sizeview.js +63 -63
  74. package/src/widgetresize.d.ts +125 -125
  75. package/src/widgetresize.js +188 -188
  76. package/src/widgettoolbarrepository.d.ts +94 -94
  77. package/src/widgettoolbarrepository.js +268 -268
  78. package/src/widgettypearound/utils.d.ts +38 -38
  79. package/src/widgettypearound/utils.js +52 -52
  80. package/src/widgettypearound/widgettypearound.d.ts +229 -229
  81. package/src/widgettypearound/widgettypearound.js +773 -773
package/src/widget.js CHANGED
@@ -1,380 +1,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 } 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 } 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
+ }