@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/utils.js CHANGED
@@ -1,348 +1,348 @@
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/utils
7
- */
8
- import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
9
- import { findOptimalInsertionRange as engineFindOptimalInsertionRange } from '@ckeditor/ckeditor5-engine';
10
- import { IconView } from '@ckeditor/ckeditor5-ui';
11
- import HighlightStack from './highlightstack';
12
- import { getTypeAroundFakeCaretPosition } from './widgettypearound/utils';
13
- import dragHandleIcon from '../theme/icons/drag-handle.svg';
14
- /**
15
- * CSS class added to each widget element.
16
- */
17
- export const WIDGET_CLASS_NAME = 'ck-widget';
18
- /**
19
- * CSS class added to currently selected widget element.
20
- */
21
- export const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
22
- /**
23
- * Returns `true` if given {@link module:engine/view/node~Node} is an {@link module:engine/view/element~Element} and a widget.
24
- */
25
- export function isWidget(node) {
26
- if (!node.is('element')) {
27
- return false;
28
- }
29
- return !!node.getCustomProperty('widget');
30
- }
31
- /**
32
- * Converts the given {@link module:engine/view/element~Element} to a widget in the following way:
33
- *
34
- * * sets the `contenteditable` attribute to `"false"`,
35
- * * adds the `ck-widget` CSS class,
36
- * * adds a custom {@link module:engine/view/element~Element#getFillerOffset `getFillerOffset()`} method returning `null`,
37
- * * adds a custom property allowing to recognize widget elements by using {@link ~isWidget `isWidget()`},
38
- * * implements the {@link ~setHighlightHandling view highlight on widgets}.
39
- *
40
- * This function needs to be used in conjunction with
41
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers downcast conversion helpers}
42
- * like {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
43
- * Moreover, typically you will want to use `toWidget()` only for `editingDowncast`, while keeping the `dataDowncast` clean.
44
- *
45
- * For example, in order to convert a `<widget>` model element to `<div class="widget">` in the view, you can define
46
- * such converters:
47
- *
48
- * ```ts
49
- * editor.conversion.for( 'editingDowncast' )
50
- * .elementToElement( {
51
- * model: 'widget',
52
- * view: ( modelItem, { writer } ) => {
53
- * const div = writer.createContainerElement( 'div', { class: 'widget' } );
54
- *
55
- * return toWidget( div, writer, { label: 'some widget' } );
56
- * }
57
- * } );
58
- *
59
- * editor.conversion.for( 'dataDowncast' )
60
- * .elementToElement( {
61
- * model: 'widget',
62
- * view: ( modelItem, { writer } ) => {
63
- * return writer.createContainerElement( 'div', { class: 'widget' } );
64
- * }
65
- * } );
66
- * ```
67
- *
68
- * See the full source code of the widget (with a nested editable) schema definition and converters in
69
- * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
70
- *
71
- * @param options Additional options.
72
- * @param options.label Element's label provided to the {@link ~setLabel} function. It can be passed as
73
- * a plain string or a function returning a string. It represents the widget for assistive technologies (like screen readers).
74
- * @param options.hasSelectionHandle If `true`, the widget will have a selection handle added.
75
- * @returns Returns the same element.
76
- */
77
- export function toWidget(element, writer, options = {}) {
78
- if (!element.is('containerElement')) {
79
- /**
80
- * The element passed to `toWidget()` must be a {@link module:engine/view/containerelement~ContainerElement}
81
- * instance.
82
- *
83
- * @error widget-to-widget-wrong-element-type
84
- * @param element The view element passed to `toWidget()`.
85
- */
86
- throw new CKEditorError('widget-to-widget-wrong-element-type', null, { element });
87
- }
88
- writer.setAttribute('contenteditable', 'false', element);
89
- writer.addClass(WIDGET_CLASS_NAME, element);
90
- writer.setCustomProperty('widget', true, element);
91
- element.getFillerOffset = getFillerOffset;
92
- writer.setCustomProperty('widgetLabel', [], element);
93
- if (options.label) {
94
- setLabel(element, options.label);
95
- }
96
- if (options.hasSelectionHandle) {
97
- addSelectionHandle(element, writer);
98
- }
99
- setHighlightHandling(element, writer);
100
- return element;
101
- }
102
- /**
103
- * Default handler for adding a highlight on a widget.
104
- * It adds CSS class and attributes basing on the given highlight descriptor.
105
- */
106
- function addHighlight(element, descriptor, writer) {
107
- if (descriptor.classes) {
108
- writer.addClass(toArray(descriptor.classes), element);
109
- }
110
- if (descriptor.attributes) {
111
- for (const key in descriptor.attributes) {
112
- writer.setAttribute(key, descriptor.attributes[key], element);
113
- }
114
- }
115
- }
116
- /**
117
- * Default handler for removing a highlight from a widget.
118
- * It removes CSS class and attributes basing on the given highlight descriptor.
119
- */
120
- function removeHighlight(element, descriptor, writer) {
121
- if (descriptor.classes) {
122
- writer.removeClass(toArray(descriptor.classes), element);
123
- }
124
- if (descriptor.attributes) {
125
- for (const key in descriptor.attributes) {
126
- writer.removeAttribute(key, element);
127
- }
128
- }
129
- }
130
- /**
131
- * Sets highlight handling methods. Uses {@link module:widget/highlightstack~HighlightStack} to
132
- * properly determine which highlight descriptor should be used at given time.
133
- */
134
- export function setHighlightHandling(element, writer, add = addHighlight, remove = removeHighlight) {
135
- const stack = new HighlightStack();
136
- stack.on('change:top', (evt, data) => {
137
- if (data.oldDescriptor) {
138
- remove(element, data.oldDescriptor, data.writer);
139
- }
140
- if (data.newDescriptor) {
141
- add(element, data.newDescriptor, data.writer);
142
- }
143
- });
144
- const addHighlightCallback = (element, descriptor, writer) => stack.add(descriptor, writer);
145
- const removeHighlightCallback = (element, id, writer) => stack.remove(id, writer);
146
- writer.setCustomProperty('addHighlight', addHighlightCallback, element);
147
- writer.setCustomProperty('removeHighlight', removeHighlightCallback, element);
148
- }
149
- /**
150
- * Sets label for given element.
151
- * It can be passed as a plain string or a function returning a string. Function will be called each time label is retrieved by
152
- * {@link ~getLabel `getLabel()`}.
153
- */
154
- export function setLabel(element, labelOrCreator) {
155
- const widgetLabel = element.getCustomProperty('widgetLabel');
156
- widgetLabel.push(labelOrCreator);
157
- }
158
- /**
159
- * Returns the label of the provided element.
160
- */
161
- export function getLabel(element) {
162
- const widgetLabel = element.getCustomProperty('widgetLabel');
163
- return widgetLabel.reduce((prev, current) => {
164
- if (typeof current === 'function') {
165
- return prev ? prev + '. ' + current() : current();
166
- }
167
- else {
168
- return prev ? prev + '. ' + current : current;
169
- }
170
- }, '');
171
- }
172
- /**
173
- * Adds functionality to the provided {@link module:engine/view/editableelement~EditableElement} to act as a widget's editable:
174
- *
175
- * * sets the `contenteditable` attribute to `true` when {@link module:engine/view/editableelement~EditableElement#isReadOnly} is `false`,
176
- * otherwise sets it to `false`,
177
- * * adds the `ck-editor__editable` and `ck-editor__nested-editable` CSS classes,
178
- * * adds the `ck-editor__nested-editable_focused` CSS class when the editable is focused and removes it when it is blurred.
179
- * * implements the {@link ~setHighlightHandling view highlight on widget's editable}.
180
- *
181
- * Similarly to {@link ~toWidget `toWidget()`} this function should be used in `editingDowncast` only and it is usually
182
- * used together with {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
183
- *
184
- * For example, in order to convert a `<nested>` model element to `<div class="nested">` in the view, you can define
185
- * such converters:
186
- *
187
- * ```ts
188
- * editor.conversion.for( 'editingDowncast' )
189
- * .elementToElement( {
190
- * model: 'nested',
191
- * view: ( modelItem, { writer } ) => {
192
- * const div = writer.createEditableElement( 'div', { class: 'nested' } );
193
- *
194
- * return toWidgetEditable( nested, writer, { label: 'label for editable' } );
195
- * }
196
- * } );
197
- *
198
- * editor.conversion.for( 'dataDowncast' )
199
- * .elementToElement( {
200
- * model: 'nested',
201
- * view: ( modelItem, { writer } ) => {
202
- * return writer.createContainerElement( 'div', { class: 'nested' } );
203
- * }
204
- * } );
205
- * ```
206
- *
207
- * See the full source code of the widget (with nested editable) schema definition and converters in
208
- * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
209
- *
210
- * @param options Additional options.
211
- * @param options.label Editable's label used by assistive technologies (e.g. screen readers).
212
- * @returns Returns the same element that was provided in the `editable` parameter
213
- */
214
- export function toWidgetEditable(editable, writer, options = {}) {
215
- writer.addClass(['ck-editor__editable', 'ck-editor__nested-editable'], editable);
216
- writer.setAttribute('role', 'textbox', editable);
217
- if (options.label) {
218
- writer.setAttribute('aria-label', options.label, editable);
219
- }
220
- // Set initial contenteditable value.
221
- writer.setAttribute('contenteditable', editable.isReadOnly ? 'false' : 'true', editable);
222
- // Bind the contenteditable property to element#isReadOnly.
223
- editable.on('change:isReadOnly', (evt, property, is) => {
224
- writer.setAttribute('contenteditable', is ? 'false' : 'true', editable);
225
- });
226
- editable.on('change:isFocused', (evt, property, is) => {
227
- if (is) {
228
- writer.addClass('ck-editor__nested-editable_focused', editable);
229
- }
230
- else {
231
- writer.removeClass('ck-editor__nested-editable_focused', editable);
232
- }
233
- });
234
- setHighlightHandling(editable, writer);
235
- return editable;
236
- }
237
- /**
238
- * Returns a model range which is optimal (in terms of UX) for inserting a widget block.
239
- *
240
- * For instance, if a selection is in the middle of a paragraph, the collapsed range before this paragraph
241
- * will be returned so that it is not split. If the selection is at the end of a paragraph,
242
- * the collapsed range after this paragraph will be returned.
243
- *
244
- * Note: If the selection is placed in an empty block, the range in that block will be returned. If that range
245
- * is then passed to {@link module:engine/model/model~Model#insertContent}, the block will be fully replaced
246
- * by the inserted widget block.
247
- *
248
- * @param selection The selection based on which the insertion position should be calculated.
249
- * @param model Model instance.
250
- * @returns The optimal range.
251
- */
252
- export function findOptimalInsertionRange(selection, model) {
253
- const selectedElement = selection.getSelectedElement();
254
- if (selectedElement) {
255
- const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(selection);
256
- // If the WidgetTypeAround "fake caret" is displayed, use its position for the insertion
257
- // to provide the most predictable UX (https://github.com/ckeditor/ckeditor5/issues/7438).
258
- if (typeAroundFakeCaretPosition) {
259
- return model.createRange(model.createPositionAt(selectedElement, typeAroundFakeCaretPosition));
260
- }
261
- }
262
- return engineFindOptimalInsertionRange(selection, model);
263
- }
264
- /**
265
- * A util to be used in order to map view positions to correct model positions when implementing a widget
266
- * which renders non-empty view element for an empty model element.
267
- *
268
- * For example:
269
- *
270
- * ```
271
- * // Model:
272
- * <placeholder type="name"></placeholder>
273
- *
274
- * // View:
275
- * <span class="placeholder">name</span>
276
- * ```
277
- *
278
- * In such case, view positions inside `<span>` cannot be correctly mapped to the model (because the model element is empty).
279
- * To handle mapping positions inside `<span class="placeholder">` to the model use this util as follows:
280
- *
281
- * ```ts
282
- * editor.editing.mapper.on(
283
- * 'viewToModelPosition',
284
- * viewToModelPositionOutsideModelElement( model, viewElement => viewElement.hasClass( 'placeholder' ) )
285
- * );
286
- * ```
287
- *
288
- * The callback will try to map the view offset of selection to an expected model position.
289
- *
290
- * 1. When the position is at the end (or in the middle) of the inline widget:
291
- *
292
- * ```
293
- * // View:
294
- * <p>foo <span class="placeholder">name|</span> bar</p>
295
- *
296
- * // Model:
297
- * <paragraph>foo <placeholder type="name"></placeholder>| bar</paragraph>
298
- * ```
299
- *
300
- * 2. When the position is at the beginning of the inline widget:
301
- *
302
- * ```
303
- * // View:
304
- * <p>foo <span class="placeholder">|name</span> bar</p>
305
- *
306
- * // Model:
307
- * <paragraph>foo |<placeholder type="name"></placeholder> bar</paragraph>
308
- * ```
309
- *
310
- * @param model Model instance on which the callback operates.
311
- * @param viewElementMatcher Function that is passed a view element and should return `true` if the custom mapping
312
- * should be applied to the given view element.
313
- */
314
- export function viewToModelPositionOutsideModelElement(model, viewElementMatcher) {
315
- return (evt, data) => {
316
- const { mapper, viewPosition } = data;
317
- const viewParent = mapper.findMappedViewAncestor(viewPosition);
318
- if (!viewElementMatcher(viewParent)) {
319
- return;
320
- }
321
- const modelParent = mapper.toModelElement(viewParent);
322
- data.modelPosition = model.createPositionAt(modelParent, viewPosition.isAtStart ? 'before' : 'after');
323
- };
324
- }
325
- /**
326
- * Default filler offset function applied to all widget elements.
327
- */
328
- function getFillerOffset() {
329
- return null;
330
- }
331
- /**
332
- * Adds a drag handle to the widget.
333
- */
334
- function addSelectionHandle(widgetElement, writer) {
335
- const selectionHandle = writer.createUIElement('div', { class: 'ck ck-widget__selection-handle' }, function (domDocument) {
336
- const domElement = this.toDomElement(domDocument);
337
- // Use the IconView from the ui library.
338
- const icon = new IconView();
339
- icon.set('content', dragHandleIcon);
340
- // Render the icon view right away to append its #element to the selectionHandle DOM element.
341
- icon.render();
342
- domElement.appendChild(icon.element);
343
- return domElement;
344
- });
345
- // Append the selection handle into the widget wrapper.
346
- writer.insert(writer.createPositionAt(widgetElement, 0), selectionHandle);
347
- writer.addClass(['ck-widget_with-selection-handle'], widgetElement);
348
- }
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/utils
7
+ */
8
+ import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
9
+ import { findOptimalInsertionRange as engineFindOptimalInsertionRange } from '@ckeditor/ckeditor5-engine';
10
+ import { IconView } from '@ckeditor/ckeditor5-ui';
11
+ import HighlightStack from './highlightstack';
12
+ import { getTypeAroundFakeCaretPosition } from './widgettypearound/utils';
13
+ import dragHandleIcon from '../theme/icons/drag-handle.svg';
14
+ /**
15
+ * CSS class added to each widget element.
16
+ */
17
+ export const WIDGET_CLASS_NAME = 'ck-widget';
18
+ /**
19
+ * CSS class added to currently selected widget element.
20
+ */
21
+ export const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
22
+ /**
23
+ * Returns `true` if given {@link module:engine/view/node~Node} is an {@link module:engine/view/element~Element} and a widget.
24
+ */
25
+ export function isWidget(node) {
26
+ if (!node.is('element')) {
27
+ return false;
28
+ }
29
+ return !!node.getCustomProperty('widget');
30
+ }
31
+ /**
32
+ * Converts the given {@link module:engine/view/element~Element} to a widget in the following way:
33
+ *
34
+ * * sets the `contenteditable` attribute to `"false"`,
35
+ * * adds the `ck-widget` CSS class,
36
+ * * adds a custom {@link module:engine/view/element~Element#getFillerOffset `getFillerOffset()`} method returning `null`,
37
+ * * adds a custom property allowing to recognize widget elements by using {@link ~isWidget `isWidget()`},
38
+ * * implements the {@link ~setHighlightHandling view highlight on widgets}.
39
+ *
40
+ * This function needs to be used in conjunction with
41
+ * {@link module:engine/conversion/downcasthelpers~DowncastHelpers downcast conversion helpers}
42
+ * like {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
43
+ * Moreover, typically you will want to use `toWidget()` only for `editingDowncast`, while keeping the `dataDowncast` clean.
44
+ *
45
+ * For example, in order to convert a `<widget>` model element to `<div class="widget">` in the view, you can define
46
+ * such converters:
47
+ *
48
+ * ```ts
49
+ * editor.conversion.for( 'editingDowncast' )
50
+ * .elementToElement( {
51
+ * model: 'widget',
52
+ * view: ( modelItem, { writer } ) => {
53
+ * const div = writer.createContainerElement( 'div', { class: 'widget' } );
54
+ *
55
+ * return toWidget( div, writer, { label: 'some widget' } );
56
+ * }
57
+ * } );
58
+ *
59
+ * editor.conversion.for( 'dataDowncast' )
60
+ * .elementToElement( {
61
+ * model: 'widget',
62
+ * view: ( modelItem, { writer } ) => {
63
+ * return writer.createContainerElement( 'div', { class: 'widget' } );
64
+ * }
65
+ * } );
66
+ * ```
67
+ *
68
+ * See the full source code of the widget (with a nested editable) schema definition and converters in
69
+ * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
70
+ *
71
+ * @param options Additional options.
72
+ * @param options.label Element's label provided to the {@link ~setLabel} function. It can be passed as
73
+ * a plain string or a function returning a string. It represents the widget for assistive technologies (like screen readers).
74
+ * @param options.hasSelectionHandle If `true`, the widget will have a selection handle added.
75
+ * @returns Returns the same element.
76
+ */
77
+ export function toWidget(element, writer, options = {}) {
78
+ if (!element.is('containerElement')) {
79
+ /**
80
+ * The element passed to `toWidget()` must be a {@link module:engine/view/containerelement~ContainerElement}
81
+ * instance.
82
+ *
83
+ * @error widget-to-widget-wrong-element-type
84
+ * @param element The view element passed to `toWidget()`.
85
+ */
86
+ throw new CKEditorError('widget-to-widget-wrong-element-type', null, { element });
87
+ }
88
+ writer.setAttribute('contenteditable', 'false', element);
89
+ writer.addClass(WIDGET_CLASS_NAME, element);
90
+ writer.setCustomProperty('widget', true, element);
91
+ element.getFillerOffset = getFillerOffset;
92
+ writer.setCustomProperty('widgetLabel', [], element);
93
+ if (options.label) {
94
+ setLabel(element, options.label);
95
+ }
96
+ if (options.hasSelectionHandle) {
97
+ addSelectionHandle(element, writer);
98
+ }
99
+ setHighlightHandling(element, writer);
100
+ return element;
101
+ }
102
+ /**
103
+ * Default handler for adding a highlight on a widget.
104
+ * It adds CSS class and attributes basing on the given highlight descriptor.
105
+ */
106
+ function addHighlight(element, descriptor, writer) {
107
+ if (descriptor.classes) {
108
+ writer.addClass(toArray(descriptor.classes), element);
109
+ }
110
+ if (descriptor.attributes) {
111
+ for (const key in descriptor.attributes) {
112
+ writer.setAttribute(key, descriptor.attributes[key], element);
113
+ }
114
+ }
115
+ }
116
+ /**
117
+ * Default handler for removing a highlight from a widget.
118
+ * It removes CSS class and attributes basing on the given highlight descriptor.
119
+ */
120
+ function removeHighlight(element, descriptor, writer) {
121
+ if (descriptor.classes) {
122
+ writer.removeClass(toArray(descriptor.classes), element);
123
+ }
124
+ if (descriptor.attributes) {
125
+ for (const key in descriptor.attributes) {
126
+ writer.removeAttribute(key, element);
127
+ }
128
+ }
129
+ }
130
+ /**
131
+ * Sets highlight handling methods. Uses {@link module:widget/highlightstack~HighlightStack} to
132
+ * properly determine which highlight descriptor should be used at given time.
133
+ */
134
+ export function setHighlightHandling(element, writer, add = addHighlight, remove = removeHighlight) {
135
+ const stack = new HighlightStack();
136
+ stack.on('change:top', (evt, data) => {
137
+ if (data.oldDescriptor) {
138
+ remove(element, data.oldDescriptor, data.writer);
139
+ }
140
+ if (data.newDescriptor) {
141
+ add(element, data.newDescriptor, data.writer);
142
+ }
143
+ });
144
+ const addHighlightCallback = (element, descriptor, writer) => stack.add(descriptor, writer);
145
+ const removeHighlightCallback = (element, id, writer) => stack.remove(id, writer);
146
+ writer.setCustomProperty('addHighlight', addHighlightCallback, element);
147
+ writer.setCustomProperty('removeHighlight', removeHighlightCallback, element);
148
+ }
149
+ /**
150
+ * Sets label for given element.
151
+ * It can be passed as a plain string or a function returning a string. Function will be called each time label is retrieved by
152
+ * {@link ~getLabel `getLabel()`}.
153
+ */
154
+ export function setLabel(element, labelOrCreator) {
155
+ const widgetLabel = element.getCustomProperty('widgetLabel');
156
+ widgetLabel.push(labelOrCreator);
157
+ }
158
+ /**
159
+ * Returns the label of the provided element.
160
+ */
161
+ export function getLabel(element) {
162
+ const widgetLabel = element.getCustomProperty('widgetLabel');
163
+ return widgetLabel.reduce((prev, current) => {
164
+ if (typeof current === 'function') {
165
+ return prev ? prev + '. ' + current() : current();
166
+ }
167
+ else {
168
+ return prev ? prev + '. ' + current : current;
169
+ }
170
+ }, '');
171
+ }
172
+ /**
173
+ * Adds functionality to the provided {@link module:engine/view/editableelement~EditableElement} to act as a widget's editable:
174
+ *
175
+ * * sets the `contenteditable` attribute to `true` when {@link module:engine/view/editableelement~EditableElement#isReadOnly} is `false`,
176
+ * otherwise sets it to `false`,
177
+ * * adds the `ck-editor__editable` and `ck-editor__nested-editable` CSS classes,
178
+ * * adds the `ck-editor__nested-editable_focused` CSS class when the editable is focused and removes it when it is blurred.
179
+ * * implements the {@link ~setHighlightHandling view highlight on widget's editable}.
180
+ *
181
+ * Similarly to {@link ~toWidget `toWidget()`} this function should be used in `editingDowncast` only and it is usually
182
+ * used together with {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
183
+ *
184
+ * For example, in order to convert a `<nested>` model element to `<div class="nested">` in the view, you can define
185
+ * such converters:
186
+ *
187
+ * ```ts
188
+ * editor.conversion.for( 'editingDowncast' )
189
+ * .elementToElement( {
190
+ * model: 'nested',
191
+ * view: ( modelItem, { writer } ) => {
192
+ * const div = writer.createEditableElement( 'div', { class: 'nested' } );
193
+ *
194
+ * return toWidgetEditable( nested, writer, { label: 'label for editable' } );
195
+ * }
196
+ * } );
197
+ *
198
+ * editor.conversion.for( 'dataDowncast' )
199
+ * .elementToElement( {
200
+ * model: 'nested',
201
+ * view: ( modelItem, { writer } ) => {
202
+ * return writer.createContainerElement( 'div', { class: 'nested' } );
203
+ * }
204
+ * } );
205
+ * ```
206
+ *
207
+ * See the full source code of the widget (with nested editable) schema definition and converters in
208
+ * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
209
+ *
210
+ * @param options Additional options.
211
+ * @param options.label Editable's label used by assistive technologies (e.g. screen readers).
212
+ * @returns Returns the same element that was provided in the `editable` parameter
213
+ */
214
+ export function toWidgetEditable(editable, writer, options = {}) {
215
+ writer.addClass(['ck-editor__editable', 'ck-editor__nested-editable'], editable);
216
+ writer.setAttribute('role', 'textbox', editable);
217
+ if (options.label) {
218
+ writer.setAttribute('aria-label', options.label, editable);
219
+ }
220
+ // Set initial contenteditable value.
221
+ writer.setAttribute('contenteditable', editable.isReadOnly ? 'false' : 'true', editable);
222
+ // Bind the contenteditable property to element#isReadOnly.
223
+ editable.on('change:isReadOnly', (evt, property, is) => {
224
+ writer.setAttribute('contenteditable', is ? 'false' : 'true', editable);
225
+ });
226
+ editable.on('change:isFocused', (evt, property, is) => {
227
+ if (is) {
228
+ writer.addClass('ck-editor__nested-editable_focused', editable);
229
+ }
230
+ else {
231
+ writer.removeClass('ck-editor__nested-editable_focused', editable);
232
+ }
233
+ });
234
+ setHighlightHandling(editable, writer);
235
+ return editable;
236
+ }
237
+ /**
238
+ * Returns a model range which is optimal (in terms of UX) for inserting a widget block.
239
+ *
240
+ * For instance, if a selection is in the middle of a paragraph, the collapsed range before this paragraph
241
+ * will be returned so that it is not split. If the selection is at the end of a paragraph,
242
+ * the collapsed range after this paragraph will be returned.
243
+ *
244
+ * Note: If the selection is placed in an empty block, the range in that block will be returned. If that range
245
+ * is then passed to {@link module:engine/model/model~Model#insertContent}, the block will be fully replaced
246
+ * by the inserted widget block.
247
+ *
248
+ * @param selection The selection based on which the insertion position should be calculated.
249
+ * @param model Model instance.
250
+ * @returns The optimal range.
251
+ */
252
+ export function findOptimalInsertionRange(selection, model) {
253
+ const selectedElement = selection.getSelectedElement();
254
+ if (selectedElement) {
255
+ const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition(selection);
256
+ // If the WidgetTypeAround "fake caret" is displayed, use its position for the insertion
257
+ // to provide the most predictable UX (https://github.com/ckeditor/ckeditor5/issues/7438).
258
+ if (typeAroundFakeCaretPosition) {
259
+ return model.createRange(model.createPositionAt(selectedElement, typeAroundFakeCaretPosition));
260
+ }
261
+ }
262
+ return engineFindOptimalInsertionRange(selection, model);
263
+ }
264
+ /**
265
+ * A util to be used in order to map view positions to correct model positions when implementing a widget
266
+ * which renders non-empty view element for an empty model element.
267
+ *
268
+ * For example:
269
+ *
270
+ * ```
271
+ * // Model:
272
+ * <placeholder type="name"></placeholder>
273
+ *
274
+ * // View:
275
+ * <span class="placeholder">name</span>
276
+ * ```
277
+ *
278
+ * In such case, view positions inside `<span>` cannot be correctly mapped to the model (because the model element is empty).
279
+ * To handle mapping positions inside `<span class="placeholder">` to the model use this util as follows:
280
+ *
281
+ * ```ts
282
+ * editor.editing.mapper.on(
283
+ * 'viewToModelPosition',
284
+ * viewToModelPositionOutsideModelElement( model, viewElement => viewElement.hasClass( 'placeholder' ) )
285
+ * );
286
+ * ```
287
+ *
288
+ * The callback will try to map the view offset of selection to an expected model position.
289
+ *
290
+ * 1. When the position is at the end (or in the middle) of the inline widget:
291
+ *
292
+ * ```
293
+ * // View:
294
+ * <p>foo <span class="placeholder">name|</span> bar</p>
295
+ *
296
+ * // Model:
297
+ * <paragraph>foo <placeholder type="name"></placeholder>| bar</paragraph>
298
+ * ```
299
+ *
300
+ * 2. When the position is at the beginning of the inline widget:
301
+ *
302
+ * ```
303
+ * // View:
304
+ * <p>foo <span class="placeholder">|name</span> bar</p>
305
+ *
306
+ * // Model:
307
+ * <paragraph>foo |<placeholder type="name"></placeholder> bar</paragraph>
308
+ * ```
309
+ *
310
+ * @param model Model instance on which the callback operates.
311
+ * @param viewElementMatcher Function that is passed a view element and should return `true` if the custom mapping
312
+ * should be applied to the given view element.
313
+ */
314
+ export function viewToModelPositionOutsideModelElement(model, viewElementMatcher) {
315
+ return (evt, data) => {
316
+ const { mapper, viewPosition } = data;
317
+ const viewParent = mapper.findMappedViewAncestor(viewPosition);
318
+ if (!viewElementMatcher(viewParent)) {
319
+ return;
320
+ }
321
+ const modelParent = mapper.toModelElement(viewParent);
322
+ data.modelPosition = model.createPositionAt(modelParent, viewPosition.isAtStart ? 'before' : 'after');
323
+ };
324
+ }
325
+ /**
326
+ * Default filler offset function applied to all widget elements.
327
+ */
328
+ function getFillerOffset() {
329
+ return null;
330
+ }
331
+ /**
332
+ * Adds a drag handle to the widget.
333
+ */
334
+ function addSelectionHandle(widgetElement, writer) {
335
+ const selectionHandle = writer.createUIElement('div', { class: 'ck ck-widget__selection-handle' }, function (domDocument) {
336
+ const domElement = this.toDomElement(domDocument);
337
+ // Use the IconView from the ui library.
338
+ const icon = new IconView();
339
+ icon.set('content', dragHandleIcon);
340
+ // Render the icon view right away to append its #element to the selectionHandle DOM element.
341
+ icon.render();
342
+ domElement.appendChild(icon.element);
343
+ return domElement;
344
+ });
345
+ // Append the selection handle into the widget wrapper.
346
+ writer.insert(writer.createPositionAt(widgetElement, 0), selectionHandle);
347
+ writer.addClass(['ck-widget_with-selection-handle'], widgetElement);
348
+ }