@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/CHANGELOG.md +24 -24
- package/LICENSE.md +3 -3
- package/package.json +7 -7
- package/src/augmentation.d.ts +13 -13
- package/src/augmentation.js +5 -5
- package/src/highlightstack.d.ts +74 -74
- package/src/highlightstack.js +129 -129
- package/src/index.d.ts +13 -13
- package/src/index.js +13 -13
- package/src/utils.d.ts +198 -198
- package/src/utils.js +348 -348
- package/src/verticalnavigation.d.ts +15 -15
- package/src/verticalnavigation.js +196 -196
- package/src/widget.d.ts +95 -91
- package/src/widget.js +429 -380
- package/src/widgetresize/resizer.d.ts +177 -177
- package/src/widgetresize/resizer.js +372 -372
- package/src/widgetresize/resizerstate.d.ts +125 -125
- package/src/widgetresize/resizerstate.js +150 -150
- package/src/widgetresize/sizeview.d.ts +55 -55
- package/src/widgetresize/sizeview.js +63 -63
- package/src/widgetresize.d.ts +125 -125
- package/src/widgetresize.js +188 -188
- package/src/widgettoolbarrepository.d.ts +94 -94
- package/src/widgettoolbarrepository.js +268 -268
- package/src/widgettypearound/utils.d.ts +38 -38
- package/src/widgettypearound/utils.js +52 -52
- package/src/widgettypearound/widgettypearound.d.ts +229 -229
- package/src/widgettypearound/widgettypearound.js +773 -773
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
|
-
//
|
158
|
-
if (
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
this.
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
//
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
const
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
* @
|
278
|
-
*
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
//
|
288
|
-
if (
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
model.
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
*
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
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
|
+
}
|