@ckeditor/ckeditor5-widget 47.6.1 → 48.0.0-alpha.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.
Files changed (110) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/index-editor.css +484 -122
  3. package/dist/index.css +479 -187
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +26 -46
  8. package/lang/contexts.json +0 -12
  9. package/lang/translations/af.po +0 -52
  10. package/lang/translations/ar.po +0 -52
  11. package/lang/translations/ast.po +0 -52
  12. package/lang/translations/az.po +0 -52
  13. package/lang/translations/be.po +0 -52
  14. package/lang/translations/bg.po +0 -52
  15. package/lang/translations/bn.po +0 -52
  16. package/lang/translations/bs.po +0 -52
  17. package/lang/translations/ca.po +0 -52
  18. package/lang/translations/cs.po +0 -52
  19. package/lang/translations/da.po +0 -52
  20. package/lang/translations/de-ch.po +0 -52
  21. package/lang/translations/de.po +0 -52
  22. package/lang/translations/el.po +0 -52
  23. package/lang/translations/en-au.po +0 -52
  24. package/lang/translations/en-gb.po +0 -52
  25. package/lang/translations/en.po +0 -52
  26. package/lang/translations/eo.po +0 -52
  27. package/lang/translations/es-co.po +0 -52
  28. package/lang/translations/es.po +0 -52
  29. package/lang/translations/et.po +0 -52
  30. package/lang/translations/eu.po +0 -52
  31. package/lang/translations/fa.po +0 -52
  32. package/lang/translations/fi.po +0 -52
  33. package/lang/translations/fr.po +0 -52
  34. package/lang/translations/gl.po +0 -52
  35. package/lang/translations/gu.po +0 -52
  36. package/lang/translations/he.po +0 -52
  37. package/lang/translations/hi.po +0 -52
  38. package/lang/translations/hr.po +0 -52
  39. package/lang/translations/hu.po +0 -52
  40. package/lang/translations/hy.po +0 -52
  41. package/lang/translations/id.po +0 -52
  42. package/lang/translations/it.po +0 -52
  43. package/lang/translations/ja.po +0 -52
  44. package/lang/translations/jv.po +0 -52
  45. package/lang/translations/kk.po +0 -52
  46. package/lang/translations/km.po +0 -52
  47. package/lang/translations/kn.po +0 -52
  48. package/lang/translations/ko.po +0 -52
  49. package/lang/translations/ku.po +0 -52
  50. package/lang/translations/lt.po +0 -52
  51. package/lang/translations/lv.po +0 -52
  52. package/lang/translations/ms.po +0 -52
  53. package/lang/translations/nb.po +0 -52
  54. package/lang/translations/ne.po +0 -52
  55. package/lang/translations/nl.po +0 -52
  56. package/lang/translations/no.po +0 -52
  57. package/lang/translations/oc.po +0 -52
  58. package/lang/translations/pl.po +0 -52
  59. package/lang/translations/pt-br.po +0 -52
  60. package/lang/translations/pt.po +0 -52
  61. package/lang/translations/ro.po +0 -52
  62. package/lang/translations/ru.po +0 -52
  63. package/lang/translations/si.po +0 -52
  64. package/lang/translations/sk.po +0 -52
  65. package/lang/translations/sl.po +0 -52
  66. package/lang/translations/sq.po +0 -52
  67. package/lang/translations/sr-latn.po +0 -52
  68. package/lang/translations/sr.po +0 -52
  69. package/lang/translations/sv.po +0 -52
  70. package/lang/translations/th.po +0 -52
  71. package/lang/translations/ti.po +0 -52
  72. package/lang/translations/tk.po +0 -52
  73. package/lang/translations/tr.po +0 -52
  74. package/lang/translations/tt.po +0 -52
  75. package/lang/translations/ug.po +0 -52
  76. package/lang/translations/uk.po +0 -52
  77. package/lang/translations/ur.po +0 -52
  78. package/lang/translations/uz.po +0 -52
  79. package/lang/translations/vi.po +0 -52
  80. package/lang/translations/zh-cn.po +0 -52
  81. package/lang/translations/zh.po +0 -52
  82. package/src/augmentation.js +0 -5
  83. package/src/highlightstack.js +0 -126
  84. package/src/index.js +0 -19
  85. package/src/utils.js +0 -414
  86. package/src/verticalnavigation.js +0 -185
  87. package/src/widget.js +0 -727
  88. package/src/widgetresize/resizer.js +0 -390
  89. package/src/widgetresize/resizerstate.js +0 -165
  90. package/src/widgetresize/sizeview.js +0 -65
  91. package/src/widgetresize.js +0 -193
  92. package/src/widgettoolbarrepository.js +0 -274
  93. package/src/widgettypearound/utils.js +0 -60
  94. package/src/widgettypearound/widgettypearound.js +0 -778
  95. package/theme/widget.css +0 -91
  96. package/theme/widgetresize.css +0 -43
  97. package/theme/widgettypearound.css +0 -119
  98. /package/{src → dist}/augmentation.d.ts +0 -0
  99. /package/{src → dist}/highlightstack.d.ts +0 -0
  100. /package/{src → dist}/index.d.ts +0 -0
  101. /package/{src → dist}/utils.d.ts +0 -0
  102. /package/{src → dist}/verticalnavigation.d.ts +0 -0
  103. /package/{src → dist}/widget.d.ts +0 -0
  104. /package/{src → dist}/widgetresize/resizer.d.ts +0 -0
  105. /package/{src → dist}/widgetresize/resizerstate.d.ts +0 -0
  106. /package/{src → dist}/widgetresize/sizeview.d.ts +0 -0
  107. /package/{src → dist}/widgetresize.d.ts +0 -0
  108. /package/{src → dist}/widgettoolbarrepository.d.ts +0 -0
  109. /package/{src → dist}/widgettypearound/utils.d.ts +0 -0
  110. /package/{src → dist}/widgettypearound/widgettypearound.d.ts +0 -0
package/src/widget.js DELETED
@@ -1,727 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
- */
5
- /**
6
- * @module widget/widget
7
- */
8
- import { Plugin } from '@ckeditor/ckeditor5-core';
9
- import { PointerObserver, MouseObserver, ModelTreeWalker } from '@ckeditor/ckeditor5-engine';
10
- import { Delete } from '@ckeditor/ckeditor5-typing';
11
- import { env, keyCodes, getLocalizedArrowKeyCodeDirection, getRangeFromMouseEvent, compareArrays } from '@ckeditor/ckeditor5-utils';
12
- import { WidgetTypeAround } from './widgettypearound/widgettypearound.js';
13
- import { getTypeAroundFakeCaretPosition } from './widgettypearound/utils.js';
14
- import { verticalWidgetNavigationHandler } from './verticalnavigation.js';
15
- import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils.js';
16
- import '../theme/widget.css';
17
- /**
18
- * The widget plugin. It enables base support for widgets.
19
- *
20
- * See {@glink api/widget package page} for more details and documentation.
21
- *
22
- * This plugin enables multiple behaviors required by widgets:
23
- *
24
- * * The model to view selection converter for the editing pipeline (it handles widget custom selection rendering).
25
- * If a converted selection wraps around a widget element, that selection is marked as
26
- * {@link module:engine/view/selection~ViewSelection#isFake fake}. Additionally, the `ck-widget_selected` CSS class
27
- * is added to indicate that widget has been selected.
28
- * * The mouse and keyboard events handling on and around widget elements.
29
- */
30
- export class Widget extends Plugin {
31
- /**
32
- * Holds previously selected widgets.
33
- */
34
- _previouslySelected = new Set();
35
- /**
36
- * @inheritDoc
37
- */
38
- static get pluginName() {
39
- return 'Widget';
40
- }
41
- /**
42
- * @inheritDoc
43
- */
44
- static get isOfficialPlugin() {
45
- return true;
46
- }
47
- /**
48
- * @inheritDoc
49
- */
50
- static get requires() {
51
- return [WidgetTypeAround, Delete];
52
- }
53
- /**
54
- * @inheritDoc
55
- */
56
- init() {
57
- const editor = this.editor;
58
- const view = editor.editing.view;
59
- const viewDocument = view.document;
60
- const t = editor.t;
61
- // Model to view selection converter.
62
- // Converts selection placed over widget element to fake selection.
63
- //
64
- // By default, the selection is downcasted by the engine to surround the attribute element, even though its only
65
- // child is an inline widget. A similar thing also happens when a collapsed marker is rendered as a UI element
66
- // next to an inline widget: the view selection contains both the widget and the marker.
67
- //
68
- // This prevents creating a correct fake selection when this inline widget is selected. Normalize the selection
69
- // in these cases based on the model:
70
- //
71
- // [<attributeElement><inlineWidget /></attributeElement>] -> <attributeElement>[<inlineWidget />]</attributeElement>
72
- // [<uiElement></uiElement><inlineWidget />] -> <uiElement></uiElement>[<inlineWidget />]
73
- //
74
- // Thanks to this:
75
- //
76
- // * fake selection can be set correctly,
77
- // * any logic depending on (View)Selection#getSelectedElement() also works OK.
78
- //
79
- // See https://github.com/ckeditor/ckeditor5/issues/9524.
80
- this.editor.editing.downcastDispatcher.on('selection', (evt, data, conversionApi) => {
81
- const viewWriter = conversionApi.writer;
82
- const modelSelection = data.selection;
83
- // The collapsed selection can't contain any widget.
84
- if (modelSelection.isCollapsed) {
85
- return;
86
- }
87
- const selectedModelElement = modelSelection.getSelectedElement();
88
- if (!selectedModelElement) {
89
- return;
90
- }
91
- const selectedViewElement = editor.editing.mapper.toViewElement(selectedModelElement);
92
- if (!isWidget(selectedViewElement)) {
93
- return;
94
- }
95
- if (!conversionApi.consumable.consume(modelSelection, 'selection')) {
96
- return;
97
- }
98
- viewWriter.setSelection(viewWriter.createRangeOn(selectedViewElement), {
99
- fake: true,
100
- label: getLabel(selectedViewElement)
101
- });
102
- });
103
- // Mark all widgets inside the selection with the css class.
104
- // This handler is registered at the 'low' priority so it's triggered after the real selection conversion.
105
- this.editor.editing.downcastDispatcher.on('selection', (evt, data, conversionApi) => {
106
- // Remove selected class from previously selected widgets.
107
- this._clearPreviouslySelectedWidgets(conversionApi.writer);
108
- const viewWriter = conversionApi.writer;
109
- const viewSelection = viewWriter.document.selection;
110
- let lastMarked = null;
111
- for (const range of viewSelection.getRanges()) {
112
- // Note: There could be multiple selected widgets in a range but no fake selection.
113
- // All of them must be marked as selected, for instance [<widget></widget><widget></widget>]
114
- for (const value of range) {
115
- const node = value.item;
116
- // Do not mark nested widgets in selected one. See: #4594
117
- if (isWidget(node) && !isChild(node, lastMarked)) {
118
- viewWriter.addClass(WIDGET_SELECTED_CLASS_NAME, node);
119
- this._previouslySelected.add(node);
120
- lastMarked = node;
121
- }
122
- }
123
- }
124
- }, { priority: 'low' });
125
- // If mouse down is pressed on widget - create selection over whole widget.
126
- view.addObserver(MouseObserver);
127
- view.addObserver(PointerObserver);
128
- this.listenTo(viewDocument, 'mousedown', (...args) => this._onMousedown(...args));
129
- this.listenTo(viewDocument, 'pointerdown', (...args) => this._onPointerdown(...args));
130
- // There are two keydown listeners working on different priorities. This allows other
131
- // features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
132
- // and customize the behavior even further in different content/selection scenarios.
133
- //
134
- // * The first listener handles changing the selection on arrow key press
135
- // if the widget is selected or if the selection is next to a widget and the widget
136
- // should become selected upon the arrow key press.
137
- //
138
- // * The second (late) listener makes sure the default browser action on arrow key press is
139
- // prevented when a widget is selected. This prevents the selection from being moved
140
- // from a fake selection container.
141
- this.listenTo(viewDocument, 'arrowKey', (...args) => {
142
- this._handleSelectionChangeOnArrowKeyPress(...args);
143
- }, { context: [isWidget, '$text'] });
144
- this.listenTo(viewDocument, 'arrowKey', (...args) => {
145
- this._preventDefaultOnArrowKeyPress(...args);
146
- }, { context: '$root' });
147
- this.listenTo(viewDocument, 'arrowKey', verticalWidgetNavigationHandler(this.editor.editing), { context: '$text' });
148
- // Handle custom delete behaviour.
149
- this.listenTo(viewDocument, 'delete', (evt, data) => {
150
- if (this._handleDelete(data.direction == 'forward')) {
151
- data.preventDefault();
152
- evt.stop();
153
- }
154
- }, { context: '$root' });
155
- // Handle Tab/Shift+Tab key.
156
- this.listenTo(viewDocument, 'tab', (evt, data) => {
157
- if (this._selectNextEditable(data.shiftKey ? 'backward' : 'forward')) {
158
- view.scrollToTheSelection();
159
- data.preventDefault();
160
- evt.stop();
161
- }
162
- }, {
163
- context: node => isWidget(node) || node.is('editableElement'),
164
- priority: 'low'
165
- });
166
- // Handle Esc key while inside a nested editable.
167
- this.listenTo(viewDocument, 'keydown', (evt, data) => {
168
- if (data.keystroke != keyCodes.esc) {
169
- return;
170
- }
171
- if (this._selectAncestorWidget()) {
172
- data.preventDefault();
173
- evt.stop();
174
- }
175
- }, {
176
- context: node => node.is('editableElement'),
177
- priority: 'low'
178
- });
179
- // Add the information about the keystrokes to the accessibility database.
180
- editor.accessibility.addKeystrokeInfoGroup({
181
- id: 'widget',
182
- label: t('Keystrokes that can be used when a widget is selected (for example: image, table, etc.)'),
183
- keystrokes: [
184
- {
185
- label: t('Move focus from an editable area back to the parent widget'),
186
- keystroke: 'Esc'
187
- },
188
- {
189
- label: t('Insert a new paragraph directly after a widget'),
190
- keystroke: 'Enter'
191
- },
192
- {
193
- label: t('Insert a new paragraph directly before a widget'),
194
- keystroke: 'Shift+Enter'
195
- },
196
- {
197
- label: t('Move the caret to allow typing directly before a widget'),
198
- keystroke: [['arrowup'], ['arrowleft']]
199
- },
200
- {
201
- label: t('Move the caret to allow typing directly after a widget'),
202
- keystroke: [['arrowdown'], ['arrowright']]
203
- }
204
- ]
205
- });
206
- }
207
- /**
208
- * Handles {@link module:engine/view/document~ViewDocument#event:mousedown mousedown} events on widget elements.
209
- */
210
- _onMousedown(eventInfo, domEventData) {
211
- const element = domEventData.target;
212
- // Some of DOM elements have no view element representation so it may be null.
213
- if (!element) {
214
- return;
215
- }
216
- // If triple click should select entire paragraph.
217
- if (domEventData.domEvent.detail >= 3) {
218
- if (this._selectBlockContent(element)) {
219
- domEventData.preventDefault();
220
- }
221
- }
222
- }
223
- /**
224
- * Handles {@link module:engine/view/document~ViewDocument#event:pointerdown pointerdown} events on widget elements.
225
- */
226
- _onPointerdown(eventInfo, domEventData) {
227
- if (!domEventData.domEvent.isPrimary) {
228
- return;
229
- }
230
- const editor = this.editor;
231
- const view = editor.editing.view;
232
- const viewDocument = view.document;
233
- let element = domEventData.target;
234
- // Some of DOM elements have no view element representation so it may be null.
235
- if (!element) {
236
- return;
237
- }
238
- // If target is not a widget element - check if one of the ancestors is.
239
- if (!isWidget(element)) {
240
- const editableOrWidgetElement = findClosestEditableOrWidgetAncestor(element);
241
- if (!editableOrWidgetElement) {
242
- return;
243
- }
244
- if (isWidget(editableOrWidgetElement)) {
245
- element = editableOrWidgetElement;
246
- }
247
- else {
248
- // Pick view range from the point where the mouse was clicked.
249
- const clickTargetFromPoint = getElementFromMouseEvent(view, domEventData);
250
- if (clickTargetFromPoint && isWidget(clickTargetFromPoint)) {
251
- element = clickTargetFromPoint;
252
- }
253
- else {
254
- return;
255
- }
256
- }
257
- }
258
- // On Android and iOS selection would jump to the first table cell, on other devices
259
- // we can't block it (and don't need to) because of drag and drop support.
260
- // In iOS drag and drop works anyway on a long press.
261
- if (env.isAndroid || env.isiOS) {
262
- domEventData.preventDefault();
263
- }
264
- // Focus editor if is not focused already.
265
- if (!viewDocument.isFocused) {
266
- view.focus();
267
- }
268
- // Create model selection over widget.
269
- const modelElement = editor.editing.mapper.toModelElement(element);
270
- this._setSelectionOverElement(modelElement);
271
- }
272
- /**
273
- * Selects entire block content, e.g. on triple click it selects entire paragraph.
274
- */
275
- _selectBlockContent(element) {
276
- const editor = this.editor;
277
- const model = editor.model;
278
- const mapper = editor.editing.mapper;
279
- const schema = model.schema;
280
- const viewElement = mapper.findMappedViewAncestor(this.editor.editing.view.createPositionAt(element, 0));
281
- const modelElement = findTextBlockAncestor(mapper.toModelElement(viewElement), model.schema);
282
- if (!modelElement) {
283
- return false;
284
- }
285
- model.change(writer => {
286
- const nextTextBlock = !schema.isLimit(modelElement) ?
287
- findNextTextBlock(writer.createPositionAfter(modelElement), schema) :
288
- null;
289
- const start = writer.createPositionAt(modelElement, 0);
290
- const end = nextTextBlock ?
291
- writer.createPositionAt(nextTextBlock, 0) :
292
- writer.createPositionAt(modelElement, 'end');
293
- writer.setSelection(writer.createRange(start, end));
294
- });
295
- return true;
296
- }
297
- /**
298
- * Handles {@link module:engine/view/document~ViewDocument#event:keydown keydown} events and changes
299
- * the model selection when:
300
- *
301
- * * arrow key is pressed when the widget is selected,
302
- * * the selection is next to a widget and the widget should become selected upon the arrow key press.
303
- *
304
- * See {@link #_preventDefaultOnArrowKeyPress}.
305
- */
306
- _handleSelectionChangeOnArrowKeyPress(eventInfo, domEventData) {
307
- const keyCode = domEventData.keyCode;
308
- const model = this.editor.model;
309
- const schema = model.schema;
310
- const modelSelection = model.document.selection;
311
- const selectedElement = modelSelection.getSelectedElement();
312
- const direction = getLocalizedArrowKeyCodeDirection(keyCode, this.editor.locale.contentLanguageDirection);
313
- const isForward = direction == 'down' || direction == 'right';
314
- const isVerticalNavigation = direction == 'up' || direction == 'down';
315
- // Collapsing a non-collapsed selection.
316
- if (!domEventData.shiftKey && !modelSelection.isCollapsed) {
317
- // If object element is selected or object is at the edge of selection.
318
- if (hasObjectAtEdge(modelSelection, schema)) {
319
- const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
320
- const newRange = schema.getNearestSelectionRange(position, isForward ? 'forward' : 'backward');
321
- if (newRange) {
322
- model.change(writer => {
323
- writer.setSelection(newRange);
324
- });
325
- domEventData.preventDefault();
326
- eventInfo.stop();
327
- }
328
- }
329
- // Else is handled by the browser.
330
- return;
331
- }
332
- // Adjust selection for fake caret and for selection direction when single object is selected.
333
- const originalSelection = getModelSelectionAdjusted(model, isForward);
334
- // Clone current selection to use it as a probe. We must leave default selection as it is so it can return
335
- // to its current state after undo.
336
- const probe = model.createSelection(originalSelection);
337
- model.modifySelection(probe, { direction: isForward ? 'forward' : 'backward' });
338
- // The selection didn't change so there is nothing there.
339
- if (probe.isEqual(originalSelection)) {
340
- return;
341
- }
342
- // Move probe one step further to make it visually recognizable
343
- // but only when there is a block allowing text, and we are not already on the exiting edge of it.
344
- if (probe.focus.isTouching(originalSelection.focus) &&
345
- schema.checkChild(probe.focus.parent, '$text') &&
346
- (isForward ? !probe.focus.isAtEnd : !probe.focus.isAtStart)) {
347
- model.modifySelection(probe, { direction: isForward ? 'forward' : 'backward' });
348
- }
349
- const lastSelectedNode = isForward ? originalSelection.focus.nodeBefore : originalSelection.focus.nodeAfter;
350
- const nodeBeforeProbe = probe.focus.nodeBefore;
351
- const nodeAfterProbe = probe.focus.nodeAfter;
352
- const lastProbeNode = isForward ? nodeBeforeProbe : nodeAfterProbe;
353
- if (domEventData.shiftKey) {
354
- // Expand selection from a selected object or include object in selection.
355
- if (selectedElement && schema.isObject(selectedElement) ||
356
- lastProbeNode && schema.isObject(lastProbeNode) ||
357
- lastSelectedNode && schema.isObject(lastSelectedNode)) {
358
- model.change(writer => {
359
- writer.setSelection(probe);
360
- });
361
- domEventData.preventDefault();
362
- eventInfo.stop();
363
- }
364
- }
365
- else {
366
- // Select an object when moving caret over it.
367
- if (lastProbeNode && schema.isObject(lastProbeNode)) {
368
- if (schema.isInline(lastProbeNode) && isVerticalNavigation) {
369
- return;
370
- }
371
- model.change(writer => {
372
- writer.setSelection(lastProbeNode, 'on');
373
- });
374
- domEventData.preventDefault();
375
- eventInfo.stop();
376
- }
377
- }
378
- }
379
- /**
380
- * Handles {@link module:engine/view/document~ViewDocument#event:keydown keydown} events and prevents
381
- * the default browser behavior to make sure the fake selection is not being moved from a fake selection
382
- * container.
383
- *
384
- * See {@link #_handleSelectionChangeOnArrowKeyPress}.
385
- */
386
- _preventDefaultOnArrowKeyPress(eventInfo, domEventData) {
387
- const model = this.editor.model;
388
- const schema = model.schema;
389
- const objectElement = model.document.selection.getSelectedElement();
390
- // If object element is selected.
391
- if (objectElement && schema.isObject(objectElement)) {
392
- domEventData.preventDefault();
393
- eventInfo.stop();
394
- }
395
- }
396
- /**
397
- * Handles delete keys: backspace and delete.
398
- *
399
- * @param isForward Set to true if delete was performed in forward direction.
400
- * @returns Returns `true` if keys were handled correctly.
401
- */
402
- _handleDelete(isForward) {
403
- const modelDocument = this.editor.model.document;
404
- const modelSelection = modelDocument.selection;
405
- // Do nothing when the read only mode is enabled.
406
- if (!this.editor.model.canEditAt(modelSelection)) {
407
- return;
408
- }
409
- // Do nothing on non-collapsed selection.
410
- if (!modelSelection.isCollapsed) {
411
- return;
412
- }
413
- const objectElement = this._getObjectElementNextToSelection(isForward);
414
- if (objectElement) {
415
- this.editor.model.change(writer => {
416
- let previousNode = modelSelection.anchor.parent;
417
- // Remove previous element if empty.
418
- while (previousNode.isEmpty) {
419
- const nodeToRemove = previousNode;
420
- previousNode = nodeToRemove.parent;
421
- writer.remove(nodeToRemove);
422
- }
423
- this._setSelectionOverElement(objectElement);
424
- });
425
- return true;
426
- }
427
- }
428
- /**
429
- * Sets {@link module:engine/model/selection~ModelSelection document's selection} over given element.
430
- *
431
- * @internal
432
- */
433
- _setSelectionOverElement(element) {
434
- this.editor.model.change(writer => {
435
- writer.setSelection(writer.createRangeOn(element));
436
- });
437
- }
438
- /**
439
- * Checks if {@link module:engine/model/element~ModelElement element} placed next to the current
440
- * {@link module:engine/model/selection~ModelSelection model selection} exists and is marked in
441
- * {@link module:engine/model/schema~ModelSchema schema} as `object`.
442
- *
443
- * @internal
444
- * @param forward Direction of checking.
445
- */
446
- _getObjectElementNextToSelection(forward) {
447
- const model = this.editor.model;
448
- const schema = model.schema;
449
- const modelSelection = model.document.selection;
450
- // Clone current selection to use it as a probe. We must leave default selection as it is so it can return
451
- // to its current state after undo.
452
- const probe = model.createSelection(modelSelection);
453
- model.modifySelection(probe, { direction: forward ? 'forward' : 'backward' });
454
- // The selection didn't change so there is nothing there.
455
- if (probe.isEqual(modelSelection)) {
456
- return null;
457
- }
458
- const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
459
- if (objectElement && schema.isObject(objectElement)) {
460
- return objectElement;
461
- }
462
- return null;
463
- }
464
- /**
465
- * Removes CSS class from previously selected widgets.
466
- */
467
- _clearPreviouslySelectedWidgets(writer) {
468
- for (const widget of this._previouslySelected) {
469
- writer.removeClass(WIDGET_SELECTED_CLASS_NAME, widget);
470
- }
471
- this._previouslySelected.clear();
472
- }
473
- /**
474
- * Moves the document selection into the next editable or block widget.
475
- */
476
- _selectNextEditable(direction) {
477
- const editing = this.editor.editing;
478
- const view = editing.view;
479
- const model = this.editor.model;
480
- const viewSelection = view.document.selection;
481
- const modelSelection = model.document.selection;
482
- // Find start position.
483
- let startPosition;
484
- // Multiple table cells are selected - use focus cell.
485
- if (modelSelection.rangeCount > 1) {
486
- const selectionRange = modelSelection.isBackward ?
487
- modelSelection.getFirstRange() :
488
- modelSelection.getLastRange();
489
- startPosition = editing.mapper.toViewPosition(direction == 'forward' ?
490
- selectionRange.end :
491
- selectionRange.start);
492
- }
493
- else {
494
- startPosition = direction == 'forward' ?
495
- viewSelection.getFirstPosition() :
496
- viewSelection.getLastPosition();
497
- }
498
- const modelRange = this._findNextFocusRange(startPosition, direction);
499
- if (modelRange) {
500
- model.change(writer => {
501
- writer.setSelection(modelRange);
502
- });
503
- return true;
504
- }
505
- return false;
506
- }
507
- /**
508
- * Looks for next focus point in the document starting from the given view position and direction.
509
- * The focus point is either a block widget or an editable.
510
- *
511
- * @internal
512
- */
513
- _findNextFocusRange(startPosition, direction) {
514
- const editing = this.editor.editing;
515
- const view = editing.view;
516
- const model = this.editor.model;
517
- const viewSelection = view.document.selection;
518
- const editableElement = viewSelection.editableElement;
519
- const editablePath = editableElement.getPath();
520
- let selectedElement = viewSelection.getSelectedElement();
521
- if (selectedElement && !isWidget(selectedElement)) {
522
- selectedElement = null;
523
- }
524
- // Look for the next editable.
525
- const viewRange = direction == 'forward' ?
526
- view.createRange(startPosition, view.createPositionAt(startPosition.root, 'end')) :
527
- view.createRange(view.createPositionAt(startPosition.root, 0), startPosition);
528
- for (const { nextPosition } of viewRange.getWalker({ direction })) {
529
- const item = nextPosition.parent;
530
- // Some widget along the way except the currently selected one.
531
- if (isWidget(item) && item != selectedElement) {
532
- const modelElement = editing.mapper.toModelElement(item);
533
- // Do not select inline widgets.
534
- if (!model.schema.isBlock(modelElement)) {
535
- continue;
536
- }
537
- // Do not select widget itself when going out of widget or iterating over sibling elements in a widget.
538
- if (compareArrays(editablePath, item.getPath()) != 'extension') {
539
- return model.createRangeOn(modelElement);
540
- }
541
- }
542
- // Encountered an editable element.
543
- else if (item.is('editableElement')) {
544
- // Ignore the current editable for text selection,
545
- // but use it when widget was selected to be able to jump after the widget.
546
- if (item == editableElement && !selectedElement) {
547
- continue;
548
- }
549
- const modelPosition = editing.mapper.toModelPosition(nextPosition);
550
- const newRange = model.schema.getNearestSelectionRange(modelPosition, direction);
551
- // There is nothing to select so just jump to the next one.
552
- if (!newRange) {
553
- continue;
554
- }
555
- // In the same editable while widget was selected - do not select the editable content.
556
- if (item == editableElement && selectedElement) {
557
- return newRange;
558
- }
559
- // Select the content of editable element when iterating over sibling editable elements
560
- // or going deeper into nested widgets.
561
- if (compareArrays(editablePath, item.getPath()) != 'extension') {
562
- // Find a limit element closest to the new selection range.
563
- return model.createRangeIn(model.schema.getLimitElement(newRange));
564
- }
565
- return newRange;
566
- }
567
- }
568
- return null;
569
- }
570
- /**
571
- * Updates the document selection so that it selects first ancestor widget.
572
- */
573
- _selectAncestorWidget() {
574
- const editor = this.editor;
575
- const mapper = editor.editing.mapper;
576
- const selection = editor.editing.view.document.selection;
577
- const positionParent = selection.getFirstPosition().parent;
578
- const positionParentElement = positionParent.is('$text') ?
579
- positionParent.parent :
580
- positionParent;
581
- const viewElement = positionParentElement.findAncestor(isWidget);
582
- if (!viewElement) {
583
- return false;
584
- }
585
- const modelElement = mapper.toModelElement(viewElement);
586
- /* istanbul ignore next -- @preserve */
587
- if (!modelElement) {
588
- return false;
589
- }
590
- editor.model.change(writer => {
591
- writer.setSelection(modelElement, 'on');
592
- });
593
- return true;
594
- }
595
- }
596
- /**
597
- * Returns true if there is an object on an edge of the given selection.
598
- */
599
- function hasObjectAtEdge(modelSelection, schema) {
600
- const firstPosition = modelSelection.getFirstPosition();
601
- const lastPosition = modelSelection.getLastPosition();
602
- const firstSelectedNode = firstPosition.nodeAfter;
603
- const lastSelectedNode = lastPosition.nodeBefore;
604
- return (!!firstSelectedNode && schema.isObject(firstSelectedNode) ||
605
- !!lastSelectedNode && schema.isObject(lastSelectedNode));
606
- }
607
- /**
608
- * Returns new instance of the model selection adjusted for fake caret and selection direction on widgets.
609
- */
610
- function getModelSelectionAdjusted(model, isForward) {
611
- const modelSelection = model.document.selection;
612
- const selectedElement = modelSelection.getSelectedElement();
613
- // Adjust selection for fake caret.
614
- const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(modelSelection);
615
- if (selectedElement && typeAroundFakeCaretPosition == 'before') {
616
- return model.createSelection(selectedElement, 'before');
617
- }
618
- else if (selectedElement && typeAroundFakeCaretPosition == 'after') {
619
- return model.createSelection(selectedElement, 'after');
620
- }
621
- // Make a copy of selection with adjusted direction for object selected.
622
- return model.createSelection(modelSelection.getRanges(), {
623
- backward: !!selectedElement && model.schema.isObject(selectedElement) ? !isForward : modelSelection.isBackward
624
- });
625
- }
626
- /**
627
- * Finds the closest ancestor element that is either an editable element or a widget.
628
- *
629
- * @param element The element from which to start searching.
630
- * @returns The closest ancestor element that is either an editable element or a widget, or null if none is found.
631
- */
632
- function findClosestEditableOrWidgetAncestor(element) {
633
- let currentElement = element;
634
- while (currentElement) {
635
- if (currentElement.is('editableElement') || isWidget(currentElement)) {
636
- return currentElement;
637
- }
638
- currentElement = currentElement.parent;
639
- }
640
- return null;
641
- }
642
- /**
643
- * Retrieves the ViewElement associated with a mouse event in the editing view.
644
- *
645
- * @param view The editing view.
646
- * @param domEventData The DOM event data containing the mouse event.
647
- * @returns The ViewElement associated with the mouse event, or null if not found.
648
- */
649
- function getElementFromMouseEvent(view, domEventData) {
650
- const domRange = getRangeFromMouseEvent(domEventData.domEvent);
651
- let viewRange = null;
652
- if (domRange) {
653
- viewRange = view.domConverter.domRangeToView(domRange);
654
- }
655
- else {
656
- // Fallback to create range in target element. It happens frequently on Safari browser.
657
- // See more: https://github.com/ckeditor/ckeditor5/issues/16978
658
- viewRange = view.createRange(view.createPositionAt(domEventData.target, 0));
659
- }
660
- if (!viewRange) {
661
- return null;
662
- }
663
- const viewPosition = viewRange.start;
664
- if (!viewPosition.parent) {
665
- return null;
666
- }
667
- let viewNode = viewPosition.parent;
668
- if (viewPosition.parent.is('editableElement')) {
669
- if (viewPosition.isAtEnd && viewPosition.nodeBefore) {
670
- // Click after a widget tend to return position at the end of the editable element
671
- // so use the node before it if range is at the end of a parent.
672
- viewNode = viewPosition.nodeBefore;
673
- }
674
- else if (viewPosition.isAtStart && viewPosition.nodeAfter) {
675
- // Click before a widget tend to return position at the start of the editable element
676
- // so use the node after it if range is at the start of a parent.
677
- // See more: https://github.com/ckeditor/ckeditor5/issues/16992
678
- viewNode = viewPosition.nodeAfter;
679
- }
680
- }
681
- if (viewNode.is('$text')) {
682
- return viewNode.parent;
683
- }
684
- return viewNode;
685
- }
686
- /**
687
- * Checks whether the specified `element` is a child of the `parent` element.
688
- *
689
- * @param element An element to check.
690
- * @param parent A parent for the element.
691
- */
692
- function isChild(element, parent) {
693
- if (!parent) {
694
- return false;
695
- }
696
- return Array.from(element.getAncestors()).includes(parent);
697
- }
698
- /**
699
- * Returns nearest text block ancestor.
700
- */
701
- function findTextBlockAncestor(modelElement, schema) {
702
- for (const element of modelElement.getAncestors({ includeSelf: true, parentFirst: true })) {
703
- if (schema.checkChild(element, '$text')) {
704
- return element;
705
- }
706
- // Do not go beyond nested editable.
707
- if (schema.isLimit(element) && !schema.isObject(element)) {
708
- break;
709
- }
710
- }
711
- return null;
712
- }
713
- /**
714
- * Returns next text block where could put selection.
715
- */
716
- function findNextTextBlock(position, schema) {
717
- const treeWalker = new ModelTreeWalker({ startPosition: position });
718
- for (const { item } of treeWalker) {
719
- if (schema.isLimit(item) || !item.is('element')) {
720
- return null;
721
- }
722
- if (schema.checkChild(item, '$text')) {
723
- return item;
724
- }
725
- }
726
- return null;
727
- }