@ckeditor/ckeditor5-engine 35.0.1 → 35.1.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 (124) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/package.json +30 -24
  3. package/src/controller/datacontroller.js +467 -561
  4. package/src/controller/editingcontroller.js +168 -204
  5. package/src/conversion/conversion.js +541 -565
  6. package/src/conversion/conversionhelpers.js +24 -28
  7. package/src/conversion/downcastdispatcher.js +457 -686
  8. package/src/conversion/downcasthelpers.js +1583 -1965
  9. package/src/conversion/mapper.js +518 -707
  10. package/src/conversion/modelconsumable.js +240 -283
  11. package/src/conversion/upcastdispatcher.js +372 -718
  12. package/src/conversion/upcasthelpers.js +707 -818
  13. package/src/conversion/viewconsumable.js +524 -581
  14. package/src/dataprocessor/basichtmlwriter.js +12 -16
  15. package/src/dataprocessor/dataprocessor.js +5 -0
  16. package/src/dataprocessor/htmldataprocessor.js +100 -116
  17. package/src/dataprocessor/htmlwriter.js +1 -18
  18. package/src/dataprocessor/xmldataprocessor.js +116 -137
  19. package/src/dev-utils/model.js +260 -352
  20. package/src/dev-utils/operationreplayer.js +106 -126
  21. package/src/dev-utils/utils.js +34 -51
  22. package/src/dev-utils/view.js +632 -753
  23. package/src/index.js +0 -11
  24. package/src/model/batch.js +111 -127
  25. package/src/model/differ.js +988 -1233
  26. package/src/model/document.js +340 -449
  27. package/src/model/documentfragment.js +327 -364
  28. package/src/model/documentselection.js +996 -1189
  29. package/src/model/element.js +306 -410
  30. package/src/model/history.js +224 -262
  31. package/src/model/item.js +5 -0
  32. package/src/model/liveposition.js +84 -145
  33. package/src/model/liverange.js +108 -185
  34. package/src/model/markercollection.js +379 -480
  35. package/src/model/model.js +883 -1034
  36. package/src/model/node.js +419 -463
  37. package/src/model/nodelist.js +175 -201
  38. package/src/model/operation/attributeoperation.js +153 -182
  39. package/src/model/operation/detachoperation.js +64 -83
  40. package/src/model/operation/insertoperation.js +135 -166
  41. package/src/model/operation/markeroperation.js +114 -140
  42. package/src/model/operation/mergeoperation.js +163 -191
  43. package/src/model/operation/moveoperation.js +157 -187
  44. package/src/model/operation/nooperation.js +28 -38
  45. package/src/model/operation/operation.js +106 -125
  46. package/src/model/operation/operationfactory.js +30 -34
  47. package/src/model/operation/renameoperation.js +109 -135
  48. package/src/model/operation/rootattributeoperation.js +155 -188
  49. package/src/model/operation/splitoperation.js +196 -232
  50. package/src/model/operation/transform.js +1833 -2204
  51. package/src/model/operation/utils.js +140 -204
  52. package/src/model/position.js +899 -1053
  53. package/src/model/range.js +910 -1028
  54. package/src/model/rootelement.js +77 -97
  55. package/src/model/schema.js +1189 -1835
  56. package/src/model/selection.js +745 -862
  57. package/src/model/text.js +90 -114
  58. package/src/model/textproxy.js +204 -240
  59. package/src/model/treewalker.js +316 -397
  60. package/src/model/typecheckable.js +16 -0
  61. package/src/model/utils/autoparagraphing.js +32 -44
  62. package/src/model/utils/deletecontent.js +334 -418
  63. package/src/model/utils/findoptimalinsertionrange.js +25 -36
  64. package/src/model/utils/getselectedcontent.js +96 -118
  65. package/src/model/utils/insertcontent.js +654 -773
  66. package/src/model/utils/insertobject.js +96 -119
  67. package/src/model/utils/modifyselection.js +120 -158
  68. package/src/model/utils/selection-post-fixer.js +153 -201
  69. package/src/model/writer.js +1305 -1474
  70. package/src/view/attributeelement.js +189 -225
  71. package/src/view/containerelement.js +75 -85
  72. package/src/view/document.js +172 -215
  73. package/src/view/documentfragment.js +200 -249
  74. package/src/view/documentselection.js +338 -367
  75. package/src/view/domconverter.js +1370 -1617
  76. package/src/view/downcastwriter.js +1747 -2076
  77. package/src/view/editableelement.js +81 -97
  78. package/src/view/element.js +739 -890
  79. package/src/view/elementdefinition.js +5 -0
  80. package/src/view/emptyelement.js +82 -92
  81. package/src/view/filler.js +35 -50
  82. package/src/view/item.js +5 -0
  83. package/src/view/matcher.js +260 -559
  84. package/src/view/node.js +274 -360
  85. package/src/view/observer/arrowkeysobserver.js +19 -28
  86. package/src/view/observer/bubblingemittermixin.js +120 -263
  87. package/src/view/observer/bubblingeventinfo.js +47 -55
  88. package/src/view/observer/clickobserver.js +7 -13
  89. package/src/view/observer/compositionobserver.js +14 -24
  90. package/src/view/observer/domeventdata.js +57 -67
  91. package/src/view/observer/domeventobserver.js +40 -64
  92. package/src/view/observer/fakeselectionobserver.js +81 -96
  93. package/src/view/observer/focusobserver.js +45 -61
  94. package/src/view/observer/inputobserver.js +7 -13
  95. package/src/view/observer/keyobserver.js +17 -27
  96. package/src/view/observer/mouseobserver.js +7 -14
  97. package/src/view/observer/mutationobserver.js +220 -315
  98. package/src/view/observer/observer.js +81 -102
  99. package/src/view/observer/selectionobserver.js +191 -246
  100. package/src/view/observer/tabobserver.js +23 -36
  101. package/src/view/placeholder.js +128 -173
  102. package/src/view/position.js +350 -401
  103. package/src/view/range.js +453 -513
  104. package/src/view/rawelement.js +85 -112
  105. package/src/view/renderer.js +874 -1018
  106. package/src/view/rooteditableelement.js +80 -90
  107. package/src/view/selection.js +608 -689
  108. package/src/view/styles/background.js +43 -44
  109. package/src/view/styles/border.js +220 -276
  110. package/src/view/styles/margin.js +8 -17
  111. package/src/view/styles/padding.js +8 -16
  112. package/src/view/styles/utils.js +127 -160
  113. package/src/view/stylesmap.js +728 -905
  114. package/src/view/text.js +102 -126
  115. package/src/view/textproxy.js +144 -170
  116. package/src/view/treewalker.js +383 -479
  117. package/src/view/typecheckable.js +19 -0
  118. package/src/view/uielement.js +166 -187
  119. package/src/view/upcastwriter.js +395 -449
  120. package/src/view/view.js +569 -664
  121. package/src/dataprocessor/dataprocessor.jsdoc +0 -64
  122. package/src/model/item.jsdoc +0 -14
  123. package/src/view/elementdefinition.jsdoc +0 -59
  124. package/src/view/item.jsdoc +0 -14
@@ -2,25 +2,21 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
5
+ /* eslint-disable new-cap */
6
6
  /**
7
7
  * @module engine/model/documentselection
8
8
  */
9
-
10
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
11
- import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
12
-
13
- import Selection from './selection';
9
+ import TypeCheckable from './typecheckable';
14
10
  import LiveRange from './liverange';
11
+ import Selection from './selection';
15
12
  import Text from './text';
16
13
  import TextProxy from './textproxy';
17
- import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
18
- import Collection from '@ckeditor/ckeditor5-utils/src/collection';
19
14
  import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
15
+ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
16
+ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
17
+ import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
20
18
  import uid from '@ckeditor/ckeditor5-utils/src/uid';
21
-
22
19
  const storePrefix = 'selection:';
23
-
24
20
  /**
25
21
  * `DocumentSelection` is a special selection which is used as the
26
22
  * {@link module:engine/model/document~Document#selection document's selection}.
@@ -47,527 +43,460 @@ const storePrefix = 'selection:';
47
43
  *
48
44
  * @mixes module:utils/emittermixin~EmitterMixin
49
45
  */
50
- export default class DocumentSelection {
51
- /**
52
- * Creates an empty live selection for given {@link module:engine/model/document~Document}.
53
- *
54
- * @param {module:engine/model/document~Document} doc Document which owns this selection.
55
- */
56
- constructor( doc ) {
57
- /**
58
- * Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
59
- *
60
- * @protected
61
- */
62
- this._selection = new LiveSelection( doc );
63
-
64
- this._selection.delegate( 'change:range' ).to( this );
65
- this._selection.delegate( 'change:attribute' ).to( this );
66
- this._selection.delegate( 'change:marker' ).to( this );
67
- }
68
-
69
- /**
70
- * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
71
- * collapsed.
72
- *
73
- * @readonly
74
- * @type {Boolean}
75
- */
76
- get isCollapsed() {
77
- return this._selection.isCollapsed;
78
- }
79
-
80
- /**
81
- * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
82
- * Together with {@link #focus} they define the direction of selection, which is important
83
- * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
84
- * {@link module:engine/model/range~Range#end end} position of the most recently added range.
85
- *
86
- * Is set to `null` if there are no ranges in selection.
87
- *
88
- * @see #focus
89
- * @readonly
90
- * @type {module:engine/model/position~Position|null}
91
- */
92
- get anchor() {
93
- return this._selection.anchor;
94
- }
95
-
96
- /**
97
- * Selection focus. Focus is a position where the selection ends.
98
- *
99
- * Is set to `null` if there are no ranges in selection.
100
- *
101
- * @see #anchor
102
- * @readonly
103
- * @type {module:engine/model/position~Position|null}
104
- */
105
- get focus() {
106
- return this._selection.focus;
107
- }
108
-
109
- /**
110
- * Returns number of ranges in selection.
111
- *
112
- * @readonly
113
- * @type {Number}
114
- */
115
- get rangeCount() {
116
- return this._selection.rangeCount;
117
- }
118
-
119
- /**
120
- * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
121
- * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
122
- *
123
- * @readonly
124
- * @type {Boolean}
125
- */
126
- get hasOwnRange() {
127
- return this._selection.hasOwnRange;
128
- }
129
-
130
- /**
131
- * Specifies whether the {@link #focus}
132
- * precedes {@link #anchor}.
133
- *
134
- * @readonly
135
- * @type {Boolean}
136
- */
137
- get isBackward() {
138
- return this._selection.isBackward;
139
- }
140
-
141
- /**
142
- * Describes whether the gravity is overridden (using {@link module:engine/model/writer~Writer#overrideSelectionGravity}) or not.
143
- *
144
- * Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
145
- *
146
- * @readonly
147
- * @returns {Boolean}
148
- */
149
- get isGravityOverridden() {
150
- return this._selection.isGravityOverridden;
151
- }
152
-
153
- /**
154
- * A collection of selection {@link module:engine/model/markercollection~Marker markers}.
155
- * Marker is a selection marker when selection range is inside the marker range.
156
- *
157
- * **Note**: Only markers from {@link ~DocumentSelection#observeMarkers observed markers groups} are collected.
158
- *
159
- * @readonly
160
- * @type {module:utils/collection~Collection}
161
- */
162
- get markers() {
163
- return this._selection.markers;
164
- }
165
-
166
- /**
167
- * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
168
- *
169
- * @protected
170
- */
171
- get _ranges() {
172
- return this._selection._ranges;
173
- }
174
-
175
- /**
176
- * Returns an iterable that iterates over copies of selection ranges.
177
- *
178
- * @returns {Iterable.<module:engine/model/range~Range>}
179
- */
180
- getRanges() {
181
- return this._selection.getRanges();
182
- }
183
-
184
- /**
185
- * Returns the first position in the selection.
186
- * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
187
- * any other position in the selection.
188
- *
189
- * Returns `null` if there are no ranges in selection.
190
- *
191
- * @returns {module:engine/model/position~Position|null}
192
- */
193
- getFirstPosition() {
194
- return this._selection.getFirstPosition();
195
- }
196
-
197
- /**
198
- * Returns the last position in the selection.
199
- * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
200
- * any other position in the selection.
201
- *
202
- * Returns `null` if there are no ranges in selection.
203
- *
204
- * @returns {module:engine/model/position~Position|null}
205
- */
206
- getLastPosition() {
207
- return this._selection.getLastPosition();
208
- }
209
-
210
- /**
211
- * Returns a copy of the first range in the selection.
212
- * First range is the one which {@link module:engine/model/range~Range#start start} position
213
- * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
214
- * (not to confuse with the first range added to the selection).
215
- *
216
- * Returns `null` if there are no ranges in selection.
217
- *
218
- * @returns {module:engine/model/range~Range|null}
219
- */
220
- getFirstRange() {
221
- return this._selection.getFirstRange();
222
- }
223
-
224
- /**
225
- * Returns a copy of the last range in the selection.
226
- * Last range is the one which {@link module:engine/model/range~Range#end end} position
227
- * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
228
- * recently added to the selection).
229
- *
230
- * Returns `null` if there are no ranges in selection.
231
- *
232
- * @returns {module:engine/model/range~Range|null}
233
- */
234
- getLastRange() {
235
- return this._selection.getLastRange();
236
- }
237
-
238
- /**
239
- * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
240
- *
241
- * This method's result can be used for example to apply block styling to all blocks covered by this selection.
242
- *
243
- * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
244
- * but will not return blocks nested in other blocks.
245
- *
246
- * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
247
- *
248
- * <paragraph>[a</paragraph>
249
- * <blockQuote>
250
- * <paragraph>b</paragraph>
251
- * </blockQuote>
252
- * <paragraph>c]d</paragraph>
253
- *
254
- * In this case the paragraph will also be returned, despite the collapsed selection:
255
- *
256
- * <paragraph>[]a</paragraph>
257
- *
258
- * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
259
- *
260
- * [<blockA></blockA>
261
- * <blockB>
262
- * <blockC></blockC>
263
- * <blockD></blockD>
264
- * </blockB>
265
- * <blockE></blockE>]
266
- *
267
- * If the selection is inside a block all the inner blocks (A & B) are returned:
268
- *
269
- * <block>
270
- * <blockA>[a</blockA>
271
- * <blockB>b]</blockB>
272
- * </block>
273
- *
274
- * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
275
- * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
276
- *
277
- * <paragraph>[a</paragraph>
278
- * <paragraph>b</paragraph>
279
- * <paragraph>]c</paragraph> // this block will not be returned
280
- *
281
- * @returns {Iterable.<module:engine/model/element~Element>}
282
- */
283
- getSelectedBlocks() {
284
- return this._selection.getSelectedBlocks();
285
- }
286
-
287
- /**
288
- * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
289
- * one range in the selection, and that range contains exactly one element.
290
- * Returns `null` if there is no selected element.
291
- *
292
- * @returns {module:engine/model/element~Element|null}
293
- */
294
- getSelectedElement() {
295
- return this._selection.getSelectedElement();
296
- }
297
-
298
- /**
299
- * Checks whether the selection contains the entire content of the given element. This means that selection must start
300
- * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
301
- * touching the element's end.
302
- *
303
- * By default, this method will check whether the entire content of the selection's current root is selected.
304
- * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
305
- *
306
- * @param {module:engine/model/element~Element} [element=this.anchor.root]
307
- * @returns {Boolean}
308
- */
309
- containsEntireContent( element ) {
310
- return this._selection.containsEntireContent( element );
311
- }
312
-
313
- /**
314
- * Unbinds all events previously bound by document selection.
315
- */
316
- destroy() {
317
- this._selection.destroy();
318
- }
319
-
320
- /**
321
- * Returns iterable that iterates over this selection's attribute keys.
322
- *
323
- * @returns {Iterable.<String>}
324
- */
325
- getAttributeKeys() {
326
- return this._selection.getAttributeKeys();
327
- }
328
-
329
- /**
330
- * Returns iterable that iterates over this selection's attributes.
331
- *
332
- * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
333
- * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
334
- *
335
- * @returns {Iterable.<*>}
336
- */
337
- getAttributes() {
338
- return this._selection.getAttributes();
339
- }
340
-
341
- /**
342
- * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
343
- *
344
- * @param {String} key Key of attribute to look for.
345
- * @returns {*} Attribute value or `undefined`.
346
- */
347
- getAttribute( key ) {
348
- return this._selection.getAttribute( key );
349
- }
350
-
351
- /**
352
- * Checks if the selection has an attribute for given key.
353
- *
354
- * @param {String} key Key of attribute to check.
355
- * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
356
- */
357
- hasAttribute( key ) {
358
- return this._selection.hasAttribute( key );
359
- }
360
-
361
- /**
362
- * Refreshes selection attributes and markers according to the current position in the model.
363
- */
364
- refresh() {
365
- this._selection._updateMarkers();
366
- this._selection._updateAttributes( false );
367
- }
368
-
369
- /**
370
- * Registers a marker group prefix or a marker name to be collected in the
371
- * {@link ~DocumentSelection#markers selection markers collection}.
372
- *
373
- * See also {@link module:engine/model/markercollection~MarkerCollection#getMarkersGroup `MarkerCollection#getMarkersGroup()`}.
374
- *
375
- * @param {String} prefixOrName The marker group prefix or marker name.
376
- */
377
- observeMarkers( prefixOrName ) {
378
- this._selection.observeMarkers( prefixOrName );
379
- }
380
-
381
- /**
382
- * Checks whether this object is of the given type.
383
- *
384
- * selection.is( 'selection' ); // -> true
385
- * selection.is( 'documentSelection' ); // -> true
386
- * selection.is( 'model:selection' ); // -> true
387
- * selection.is( 'model:documentSelection' ); // -> true
388
- *
389
- * selection.is( 'view:selection' ); // -> false
390
- * selection.is( 'element' ); // -> false
391
- * selection.is( 'node' ); // -> false
392
- *
393
- * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
394
- *
395
- * @param {String} type
396
- * @returns {Boolean}
397
- */
398
- is( type ) {
399
- return type === 'selection' ||
400
- type == 'model:selection' ||
401
- type == 'documentSelection' ||
402
- type == 'model:documentSelection';
403
- }
404
-
405
- /**
406
- * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
407
- * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
408
- *
409
- * The location can be specified in the same form as
410
- * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
411
- *
412
- * @see module:engine/model/writer~Writer#setSelectionFocus
413
- * @protected
414
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
415
- * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
416
- * first parameter is a {@link module:engine/model/item~Item model item}.
417
- */
418
- _setFocus( itemOrPosition, offset ) {
419
- this._selection.setFocus( itemOrPosition, offset );
420
- }
421
-
422
- /**
423
- * Sets this selection's ranges and direction to the specified location based on the given
424
- * {@link module:engine/model/selection~Selectable selectable}.
425
- * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
426
- *
427
- * @see module:engine/model/writer~Writer#setSelection
428
- * @protected
429
- * @param {module:engine/model/selection~Selectable} selectable
430
- * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
431
- * @param {Object} [options]
432
- * @param {Boolean} [options.backward] Sets this selection instance to be backward.
433
- */
434
- _setTo( selectable, placeOrOffset, options ) {
435
- this._selection.setTo( selectable, placeOrOffset, options );
436
- }
437
-
438
- /**
439
- * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
440
- * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
441
- *
442
- * @see module:engine/model/writer~Writer#setSelectionAttribute
443
- * @protected
444
- * @param {String} key Key of the attribute to set.
445
- * @param {*} value Attribute value.
446
- */
447
- _setAttribute( key, value ) {
448
- this._selection.setAttribute( key, value );
449
- }
450
-
451
- /**
452
- * Removes an attribute with given key from the selection.
453
- * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change:range}
454
- * event with removed attribute key.
455
- * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
456
- *
457
- * @see module:engine/model/writer~Writer#removeSelectionAttribute
458
- * @protected
459
- * @param {String} key Key of the attribute to remove.
460
- */
461
- _removeAttribute( key ) {
462
- this._selection.removeAttribute( key );
463
- }
464
-
465
- /**
466
- * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
467
- *
468
- * @protected
469
- * @returns {Iterable.<*>}
470
- */
471
- _getStoredAttributes() {
472
- return this._selection._getStoredAttributes();
473
- }
474
-
475
- /**
476
- * Temporarily changes the gravity of the selection from the left to the right.
477
- *
478
- * The gravity defines from which direction the selection inherits its attributes. If it's the default left
479
- * gravity, the selection (after being moved by the the user) inherits attributes from its left hand side.
480
- * This method allows to temporarily override this behavior by forcing the gravity to the right.
481
- *
482
- * It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
483
- * of the process.
484
- *
485
- * @see module:engine/model/writer~Writer#overrideSelectionGravity
486
- * @protected
487
- * @returns {String} The unique id which allows restoring the gravity.
488
- */
489
- _overrideGravity() {
490
- return this._selection.overrideGravity();
491
- }
492
-
493
- /**
494
- * Restores the {@link ~DocumentSelection#_overrideGravity overridden gravity}.
495
- *
496
- * Restoring the gravity is only possible using the unique identifier returned by
497
- * {@link ~DocumentSelection#_overrideGravity}. Note that the gravity remains overridden as long as won't be restored
498
- * the same number of times it was overridden.
499
- *
500
- * @see module:engine/model/writer~Writer#restoreSelectionGravity
501
- * @protected
502
- * @param {String} uid The unique id returned by {@link #_overrideGravity}.
503
- */
504
- _restoreGravity( uid ) {
505
- this._selection.restoreGravity( uid );
506
- }
507
-
508
- /**
509
- * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
510
- *
511
- * @protected
512
- * @param {String} key Attribute key to convert.
513
- * @returns {String} Converted attribute key, applicable for selection store.
514
- */
515
- static _getStoreAttributeKey( key ) {
516
- return storePrefix + key;
517
- }
518
-
519
- /**
520
- * Checks whether the given attribute key is an attribute stored on an element.
521
- *
522
- * @protected
523
- * @param {String} key
524
- * @returns {Boolean}
525
- */
526
- static _isStoreAttributeKey( key ) {
527
- return key.startsWith( storePrefix );
528
- }
46
+ export default class DocumentSelection extends EmitterMixin(TypeCheckable) {
47
+ /**
48
+ * Creates an empty live selection for given {@link module:engine/model/document~Document}.
49
+ *
50
+ * @param {module:engine/model/document~Document} doc Document which owns this selection.
51
+ */
52
+ constructor(doc) {
53
+ super();
54
+ /**
55
+ * Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
56
+ *
57
+ * @protected
58
+ */
59
+ this._selection = new LiveSelection(doc);
60
+ this._selection.delegate('change:range').to(this);
61
+ this._selection.delegate('change:attribute').to(this);
62
+ this._selection.delegate('change:marker').to(this);
63
+ }
64
+ /**
65
+ * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
66
+ * collapsed.
67
+ *
68
+ * @readonly
69
+ * @type {Boolean}
70
+ */
71
+ get isCollapsed() {
72
+ return this._selection.isCollapsed;
73
+ }
74
+ /**
75
+ * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
76
+ * Together with {@link #focus} they define the direction of selection, which is important
77
+ * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
78
+ * {@link module:engine/model/range~Range#end end} position of the most recently added range.
79
+ *
80
+ * Is set to `null` if there are no ranges in selection.
81
+ *
82
+ * @see #focus
83
+ * @readonly
84
+ * @type {module:engine/model/position~Position|null}
85
+ */
86
+ get anchor() {
87
+ return this._selection.anchor;
88
+ }
89
+ /**
90
+ * Selection focus. Focus is a position where the selection ends.
91
+ *
92
+ * Is set to `null` if there are no ranges in selection.
93
+ *
94
+ * @see #anchor
95
+ * @readonly
96
+ * @type {module:engine/model/position~Position|null}
97
+ */
98
+ get focus() {
99
+ return this._selection.focus;
100
+ }
101
+ /**
102
+ * Returns number of ranges in selection.
103
+ *
104
+ * @readonly
105
+ * @type {Number}
106
+ */
107
+ get rangeCount() {
108
+ return this._selection.rangeCount;
109
+ }
110
+ /**
111
+ * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
112
+ * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
113
+ *
114
+ * @readonly
115
+ * @type {Boolean}
116
+ */
117
+ get hasOwnRange() {
118
+ return this._selection.hasOwnRange;
119
+ }
120
+ /**
121
+ * Specifies whether the {@link #focus}
122
+ * precedes {@link #anchor}.
123
+ *
124
+ * @readonly
125
+ * @type {Boolean}
126
+ */
127
+ get isBackward() {
128
+ return this._selection.isBackward;
129
+ }
130
+ /**
131
+ * Describes whether the gravity is overridden (using {@link module:engine/model/writer~Writer#overrideSelectionGravity}) or not.
132
+ *
133
+ * Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
134
+ *
135
+ * @readonly
136
+ * @returns {Boolean}
137
+ */
138
+ get isGravityOverridden() {
139
+ return this._selection.isGravityOverridden;
140
+ }
141
+ /**
142
+ * A collection of selection {@link module:engine/model/markercollection~Marker markers}.
143
+ * Marker is a selection marker when selection range is inside the marker range.
144
+ *
145
+ * **Note**: Only markers from {@link ~DocumentSelection#observeMarkers observed markers groups} are collected.
146
+ *
147
+ * @readonly
148
+ * @type {module:utils/collection~Collection}
149
+ */
150
+ get markers() {
151
+ return this._selection.markers;
152
+ }
153
+ /**
154
+ * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
155
+ *
156
+ * @internal
157
+ * @protected
158
+ */
159
+ get _ranges() {
160
+ return this._selection._ranges;
161
+ }
162
+ /**
163
+ * Returns an iterable that iterates over copies of selection ranges.
164
+ *
165
+ * @returns {Iterable.<module:engine/model/range~Range>}
166
+ */
167
+ getRanges() {
168
+ return this._selection.getRanges();
169
+ }
170
+ /**
171
+ * Returns the first position in the selection.
172
+ * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
173
+ * any other position in the selection.
174
+ *
175
+ * Returns `null` if there are no ranges in selection.
176
+ *
177
+ * @returns {module:engine/model/position~Position|null}
178
+ */
179
+ getFirstPosition() {
180
+ return this._selection.getFirstPosition();
181
+ }
182
+ /**
183
+ * Returns the last position in the selection.
184
+ * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
185
+ * any other position in the selection.
186
+ *
187
+ * Returns `null` if there are no ranges in selection.
188
+ *
189
+ * @returns {module:engine/model/position~Position|null}
190
+ */
191
+ getLastPosition() {
192
+ return this._selection.getLastPosition();
193
+ }
194
+ /**
195
+ * Returns a copy of the first range in the selection.
196
+ * First range is the one which {@link module:engine/model/range~Range#start start} position
197
+ * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
198
+ * (not to confuse with the first range added to the selection).
199
+ *
200
+ * Returns `null` if there are no ranges in selection.
201
+ *
202
+ * @returns {module:engine/model/range~Range|null}
203
+ */
204
+ getFirstRange() {
205
+ return this._selection.getFirstRange();
206
+ }
207
+ /**
208
+ * Returns a copy of the last range in the selection.
209
+ * Last range is the one which {@link module:engine/model/range~Range#end end} position
210
+ * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
211
+ * recently added to the selection).
212
+ *
213
+ * Returns `null` if there are no ranges in selection.
214
+ *
215
+ * @returns {module:engine/model/range~Range|null}
216
+ */
217
+ getLastRange() {
218
+ return this._selection.getLastRange();
219
+ }
220
+ /**
221
+ * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
222
+ *
223
+ * This method's result can be used for example to apply block styling to all blocks covered by this selection.
224
+ *
225
+ * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
226
+ * but will not return blocks nested in other blocks.
227
+ *
228
+ * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
229
+ *
230
+ * <paragraph>[a</paragraph>
231
+ * <blockQuote>
232
+ * <paragraph>b</paragraph>
233
+ * </blockQuote>
234
+ * <paragraph>c]d</paragraph>
235
+ *
236
+ * In this case the paragraph will also be returned, despite the collapsed selection:
237
+ *
238
+ * <paragraph>[]a</paragraph>
239
+ *
240
+ * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
241
+ *
242
+ * [<blockA></blockA>
243
+ * <blockB>
244
+ * <blockC></blockC>
245
+ * <blockD></blockD>
246
+ * </blockB>
247
+ * <blockE></blockE>]
248
+ *
249
+ * If the selection is inside a block all the inner blocks (A & B) are returned:
250
+ *
251
+ * <block>
252
+ * <blockA>[a</blockA>
253
+ * <blockB>b]</blockB>
254
+ * </block>
255
+ *
256
+ * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
257
+ * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
258
+ *
259
+ * <paragraph>[a</paragraph>
260
+ * <paragraph>b</paragraph>
261
+ * <paragraph>]c</paragraph> // this block will not be returned
262
+ *
263
+ * @returns {Iterable.<module:engine/model/element~Element>}
264
+ */
265
+ getSelectedBlocks() {
266
+ return this._selection.getSelectedBlocks();
267
+ }
268
+ /**
269
+ * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
270
+ * one range in the selection, and that range contains exactly one element.
271
+ * Returns `null` if there is no selected element.
272
+ *
273
+ * @returns {module:engine/model/element~Element|null}
274
+ */
275
+ getSelectedElement() {
276
+ return this._selection.getSelectedElement();
277
+ }
278
+ /**
279
+ * Checks whether the selection contains the entire content of the given element. This means that selection must start
280
+ * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
281
+ * touching the element's end.
282
+ *
283
+ * By default, this method will check whether the entire content of the selection's current root is selected.
284
+ * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
285
+ *
286
+ * @param {module:engine/model/element~Element} [element=this.anchor.root]
287
+ * @returns {Boolean}
288
+ */
289
+ containsEntireContent(element) {
290
+ return this._selection.containsEntireContent(element);
291
+ }
292
+ /**
293
+ * Unbinds all events previously bound by document selection.
294
+ */
295
+ destroy() {
296
+ this._selection.destroy();
297
+ }
298
+ /**
299
+ * Returns iterable that iterates over this selection's attribute keys.
300
+ *
301
+ * @returns {Iterable.<String>}
302
+ */
303
+ getAttributeKeys() {
304
+ return this._selection.getAttributeKeys();
305
+ }
306
+ /**
307
+ * Returns iterable that iterates over this selection's attributes.
308
+ *
309
+ * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
310
+ * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
311
+ *
312
+ * @returns {Iterable.<*>}
313
+ */
314
+ getAttributes() {
315
+ return this._selection.getAttributes();
316
+ }
317
+ /**
318
+ * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
319
+ *
320
+ * @param {String} key Key of attribute to look for.
321
+ * @returns {*} Attribute value or `undefined`.
322
+ */
323
+ getAttribute(key) {
324
+ return this._selection.getAttribute(key);
325
+ }
326
+ /**
327
+ * Checks if the selection has an attribute for given key.
328
+ *
329
+ * @param {String} key Key of attribute to check.
330
+ * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
331
+ */
332
+ hasAttribute(key) {
333
+ return this._selection.hasAttribute(key);
334
+ }
335
+ /**
336
+ * Refreshes selection attributes and markers according to the current position in the model.
337
+ */
338
+ refresh() {
339
+ this._selection.updateMarkers();
340
+ this._selection._updateAttributes(false);
341
+ }
342
+ /**
343
+ * Registers a marker group prefix or a marker name to be collected in the
344
+ * {@link ~DocumentSelection#markers selection markers collection}.
345
+ *
346
+ * See also {@link module:engine/model/markercollection~MarkerCollection#getMarkersGroup `MarkerCollection#getMarkersGroup()`}.
347
+ *
348
+ * @param {String} prefixOrName The marker group prefix or marker name.
349
+ */
350
+ observeMarkers(prefixOrName) {
351
+ this._selection.observeMarkers(prefixOrName);
352
+ }
353
+ /**
354
+ * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
355
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
356
+ *
357
+ * The location can be specified in the same form as
358
+ * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
359
+ *
360
+ * @see module:engine/model/writer~Writer#setSelectionFocus
361
+ * @internal
362
+ * @protected
363
+ * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
364
+ * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
365
+ * first parameter is a {@link module:engine/model/item~Item model item}.
366
+ */
367
+ _setFocus(itemOrPosition, offset) {
368
+ this._selection.setFocus(itemOrPosition, offset);
369
+ }
370
+ /**
371
+ * Sets this selection's ranges and direction to the specified location based on the given
372
+ * {@link module:engine/model/selection~Selectable selectable}.
373
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
374
+ *
375
+ * @see module:engine/model/writer~Writer#setSelection
376
+ * @internal
377
+ * @protected
378
+ * @param {module:engine/model/selection~Selectable} selectable
379
+ * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
380
+ * @param {Object} [options]
381
+ * @param {Boolean} [options.backward] Sets this selection instance to be backward.
382
+ */
383
+ _setTo(...args) {
384
+ this._selection.setTo(...args);
385
+ }
386
+ /**
387
+ * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
388
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
389
+ *
390
+ * @see module:engine/model/writer~Writer#setSelectionAttribute
391
+ * @internal
392
+ * @protected
393
+ * @param {String} key Key of the attribute to set.
394
+ * @param {*} value Attribute value.
395
+ */
396
+ _setAttribute(key, value) {
397
+ this._selection.setAttribute(key, value);
398
+ }
399
+ /**
400
+ * Removes an attribute with given key from the selection.
401
+ * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change:range}
402
+ * event with removed attribute key.
403
+ * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
404
+ *
405
+ * @see module:engine/model/writer~Writer#removeSelectionAttribute
406
+ * @internal
407
+ * @protected
408
+ * @param {String} key Key of the attribute to remove.
409
+ */
410
+ _removeAttribute(key) {
411
+ this._selection.removeAttribute(key);
412
+ }
413
+ /**
414
+ * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
415
+ *
416
+ * @protected
417
+ * @returns {Iterable.<*>}
418
+ */
419
+ _getStoredAttributes() {
420
+ return this._selection.getStoredAttributes();
421
+ }
422
+ /**
423
+ * Temporarily changes the gravity of the selection from the left to the right.
424
+ *
425
+ * The gravity defines from which direction the selection inherits its attributes. If it's the default left
426
+ * gravity, the selection (after being moved by the the user) inherits attributes from its left hand side.
427
+ * This method allows to temporarily override this behavior by forcing the gravity to the right.
428
+ *
429
+ * It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
430
+ * of the process.
431
+ *
432
+ * @see module:engine/model/writer~Writer#overrideSelectionGravity
433
+ * @internal
434
+ * @protected
435
+ * @returns {String} The unique id which allows restoring the gravity.
436
+ */
437
+ _overrideGravity() {
438
+ return this._selection.overrideGravity();
439
+ }
440
+ /**
441
+ * Restores the {@link ~DocumentSelection#_overrideGravity overridden gravity}.
442
+ *
443
+ * Restoring the gravity is only possible using the unique identifier returned by
444
+ * {@link ~DocumentSelection#_overrideGravity}. Note that the gravity remains overridden as long as won't be restored
445
+ * the same number of times it was overridden.
446
+ *
447
+ * @see module:engine/model/writer~Writer#restoreSelectionGravity
448
+ * @internal
449
+ * @protected
450
+ * @param {String} uid The unique id returned by {@link #_overrideGravity}.
451
+ */
452
+ _restoreGravity(uid) {
453
+ this._selection.restoreGravity(uid);
454
+ }
455
+ /**
456
+ * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
457
+ *
458
+ * @internal
459
+ * @protected
460
+ * @param {String} key Attribute key to convert.
461
+ * @returns {String} Converted attribute key, applicable for selection store.
462
+ */
463
+ static _getStoreAttributeKey(key) {
464
+ return storePrefix + key;
465
+ }
466
+ /**
467
+ * Checks whether the given attribute key is an attribute stored on an element.
468
+ *
469
+ * @protected
470
+ * @param {String} key
471
+ * @returns {Boolean}
472
+ */
473
+ static _isStoreAttributeKey(key) {
474
+ return key.startsWith(storePrefix);
475
+ }
529
476
  }
530
-
531
- mix( DocumentSelection, EmitterMixin );
532
-
533
477
  /**
534
- * Fired when selection range(s) changed.
478
+ * Checks whether this object is of the given type.
535
479
  *
536
- * @event change:range
537
- * @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
538
- * to `true` which indicates that the selection change was caused by a direct use of selection's API.
539
- * The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its position
540
- * was directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
541
- * changed because the structure of the model has been changed (which means an indirect change).
542
- * The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
543
- * which mean that they are not updated once the document changes.
544
- */
545
-
546
- /**
547
- * Fired when selection attribute changed.
480
+ * selection.is( 'selection' ); // -> true
481
+ * selection.is( 'documentSelection' ); // -> true
482
+ * selection.is( 'model:selection' ); // -> true
483
+ * selection.is( 'model:documentSelection' ); // -> true
548
484
  *
549
- * @event change:attribute
550
- * @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
551
- * to `true` which indicates that the selection change was caused by a direct use of selection's API.
552
- * The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its attributes
553
- * were directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
554
- * changed in the model and its attributes were refreshed (which means an indirect change).
555
- * The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
556
- * which mean that they are not updated once the document changes.
557
- * @param {Array.<String>} attributeKeys Array containing keys of attributes that changed.
558
- */
559
-
560
- /**
561
- * Fired when selection marker(s) changed.
485
+ * selection.is( 'view:selection' ); // -> false
486
+ * selection.is( 'element' ); // -> false
487
+ * selection.is( 'node' ); // -> false
488
+ *
489
+ * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
562
490
  *
563
- * @event change:marker
564
- * @param {Boolean} directChange This is always set to `false` in case of `change:marker` event as there is no possibility
565
- * to change markers directly through {@link module:engine/model/documentselection~DocumentSelection} API.
566
- * See also {@link module:engine/model/documentselection~DocumentSelection#event:change:range} and
567
- * {@link module:engine/model/documentselection~DocumentSelection#event:change:attribute}.
568
- * @param {Array.<module:engine/model/markercollection~Marker>} oldMarkers Markers in which the selection was before the change.
491
+ * @param {String} type
492
+ * @returns {Boolean}
569
493
  */
570
-
494
+ DocumentSelection.prototype.is = function (type) {
495
+ return type === 'selection' ||
496
+ type == 'model:selection' ||
497
+ type == 'documentSelection' ||
498
+ type == 'model:documentSelection';
499
+ };
571
500
  // `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
572
501
  //
573
502
  // LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
@@ -582,680 +511,558 @@ mix( DocumentSelection, EmitterMixin );
582
511
  // @extends module:engine/model/selection~Selection
583
512
  //
584
513
  class LiveSelection extends Selection {
585
- // Creates an empty live selection for given {@link module:engine/model/document~Document}.
586
- // @param {module:engine/model/document~Document} doc Document which owns this selection.
587
- constructor( doc ) {
588
- super();
589
-
590
- // List of selection markers.
591
- // Marker is a selection marker when selection range is inside the marker range.
592
- //
593
- // @type {module:utils/collection~Collection}
594
- this.markers = new Collection( { idProperty: 'name' } );
595
-
596
- // Document which owns this selection.
597
- //
598
- // @protected
599
- // @member {module:engine/model/model~Model}
600
- this._model = doc.model;
601
-
602
- // Document which owns this selection.
603
- //
604
- // @protected
605
- // @member {module:engine/model/document~Document}
606
- this._document = doc;
607
-
608
- // Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
609
- // last time. Possible values of priority are: `'low'` and `'normal'`.
610
- //
611
- // Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
612
- // attributes API are set with `'normal'` priority.
613
- //
614
- // @private
615
- // @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
616
- this._attributePriority = new Map();
617
-
618
- // Position to which the selection should be set if the last selection range was moved to the graveyard.
619
- // @private
620
- // @member {module:engine/model/position~Position} module:engine/model/liveselection~LiveSelection#_selectionRestorePosition
621
- this._selectionRestorePosition = null;
622
-
623
- // Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
624
- // @private
625
- // @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
626
- this._hasChangedRange = false;
627
-
628
- // Each overriding gravity adds an UID to the set and each removal removes it.
629
- // Gravity is overridden when there's at least one UID in the set.
630
- // Gravity is restored when the set is empty.
631
- // This is to prevent conflicts when gravity is overridden by more than one feature at the same time.
632
- // @private
633
- // @type {Set}
634
- this._overriddenGravityRegister = new Set();
635
-
636
- // Prefixes of marker names that should affect `LiveSelection#markers` collection.
637
- // @private
638
- // @type {Set}
639
- this._observedMarkers = new Set();
640
-
641
- // Ensure selection is correct after each operation.
642
- this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
643
- const operation = args[ 0 ];
644
-
645
- if ( !operation.isDocumentOperation || operation.type == 'marker' || operation.type == 'rename' || operation.type == 'noop' ) {
646
- return;
647
- }
648
-
649
- // Fix selection if the last range was removed from it and we have a position to which we can restore the selection.
650
- if ( this._ranges.length == 0 && this._selectionRestorePosition ) {
651
- this._fixGraveyardSelection( this._selectionRestorePosition );
652
- }
653
-
654
- // "Forget" the restore position even if it was not "used".
655
- this._selectionRestorePosition = null;
656
-
657
- if ( this._hasChangedRange ) {
658
- this._hasChangedRange = false;
659
- this.fire( 'change:range', { directChange: false } );
660
- }
661
- }, { priority: 'lowest' } );
662
-
663
- // Ensure selection is correct and up to date after each range change.
664
- this.on( 'change:range', () => {
665
- for ( const range of this.getRanges() ) {
666
- if ( !this._document._validateSelectionRange( range ) ) {
667
- /**
668
- * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
669
- * starts or ends at incorrect position.
670
- *
671
- * @error document-selection-wrong-position
672
- * @param {module:engine/model/range~Range} range
673
- */
674
- throw new CKEditorError(
675
- 'document-selection-wrong-position',
676
- this,
677
- { range }
678
- );
679
- }
680
- }
681
- } );
682
-
683
- // Update markers data stored by the selection after each marker change.
684
- // This handles only marker changes done through marker operations (not model tree changes).
685
- this.listenTo( this._model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
686
- this._updateMarker( marker, newRange );
687
- } );
688
-
689
- // Ensure selection is up to date after each change block.
690
- this.listenTo( this._document, 'change', ( evt, batch ) => {
691
- clearAttributesStoredInElement( this._model, batch );
692
- } );
693
- }
694
-
695
- get isCollapsed() {
696
- const length = this._ranges.length;
697
-
698
- return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
699
- }
700
-
701
- get anchor() {
702
- return super.anchor || this._document._getDefaultRange().start;
703
- }
704
-
705
- get focus() {
706
- return super.focus || this._document._getDefaultRange().end;
707
- }
708
-
709
- get rangeCount() {
710
- return this._ranges.length ? this._ranges.length : 1;
711
- }
712
-
713
- // Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
714
- // {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
715
- //
716
- // @readonly
717
- // @type {Boolean}
718
- get hasOwnRange() {
719
- return this._ranges.length > 0;
720
- }
721
-
722
- // When set to `true` then selection attributes on node before the caret won't be taken
723
- // into consideration while updating selection attributes.
724
- //
725
- // @protected
726
- // @type {Boolean}
727
- get isGravityOverridden() {
728
- return !!this._overriddenGravityRegister.size;
729
- }
730
-
731
- // Unbinds all events previously bound by live selection.
732
- destroy() {
733
- for ( let i = 0; i < this._ranges.length; i++ ) {
734
- this._ranges[ i ].detach();
735
- }
736
-
737
- this.stopListening();
738
- }
739
-
740
- * getRanges() {
741
- if ( this._ranges.length ) {
742
- yield* super.getRanges();
743
- } else {
744
- yield this._document._getDefaultRange();
745
- }
746
- }
747
-
748
- getFirstRange() {
749
- return super.getFirstRange() || this._document._getDefaultRange();
750
- }
751
-
752
- getLastRange() {
753
- return super.getLastRange() || this._document._getDefaultRange();
754
- }
755
-
756
- setTo( selectable, optionsOrPlaceOrOffset, options ) {
757
- super.setTo( selectable, optionsOrPlaceOrOffset, options );
758
- this._updateAttributes( true );
759
- this._updateMarkers();
760
- }
761
-
762
- setFocus( itemOrPosition, offset ) {
763
- super.setFocus( itemOrPosition, offset );
764
- this._updateAttributes( true );
765
- this._updateMarkers();
766
- }
767
-
768
- setAttribute( key, value ) {
769
- if ( this._setAttribute( key, value ) ) {
770
- // Fire event with exact data.
771
- const attributeKeys = [ key ];
772
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
773
- }
774
- }
775
-
776
- removeAttribute( key ) {
777
- if ( this._removeAttribute( key ) ) {
778
- // Fire event with exact data.
779
- const attributeKeys = [ key ];
780
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
781
- }
782
- }
783
-
784
- overrideGravity() {
785
- const overrideUid = uid();
786
-
787
- // Remember that another overriding has been requested. It will need to be removed
788
- // before the gravity is to be restored.
789
- this._overriddenGravityRegister.add( overrideUid );
790
-
791
- if ( this._overriddenGravityRegister.size === 1 ) {
792
- this._updateAttributes( true );
793
- }
794
-
795
- return overrideUid;
796
- }
797
-
798
- restoreGravity( uid ) {
799
- if ( !this._overriddenGravityRegister.has( uid ) ) {
800
- /**
801
- * Restoring gravity for an unknown UID is not possible. Make sure you are using a correct
802
- * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
803
- *
804
- * @error document-selection-gravity-wrong-restore
805
- * @param {String} uid The unique identifier returned by
806
- * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
807
- */
808
- throw new CKEditorError(
809
- 'document-selection-gravity-wrong-restore',
810
- this,
811
- { uid }
812
- );
813
- }
814
-
815
- this._overriddenGravityRegister.delete( uid );
816
-
817
- // Restore gravity only when all overriding have been restored.
818
- if ( !this.isGravityOverridden ) {
819
- this._updateAttributes( true );
820
- }
821
- }
822
-
823
- observeMarkers( prefixOrName ) {
824
- this._observedMarkers.add( prefixOrName );
825
- this._updateMarkers();
826
- }
827
-
828
- _popRange() {
829
- this._ranges.pop().detach();
830
- }
831
-
832
- _pushRange( range ) {
833
- const liveRange = this._prepareRange( range );
834
-
835
- // `undefined` is returned when given `range` is in graveyard root.
836
- if ( liveRange ) {
837
- this._ranges.push( liveRange );
838
- }
839
- }
840
-
841
- // Prepares given range to be added to selection. Checks if it is correct,
842
- // converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
843
- // and sets listeners listening to the range's change event.
844
- //
845
- // @private
846
- // @param {module:engine/model/range~Range} range
847
- _prepareRange( range ) {
848
- this._checkRange( range );
849
-
850
- if ( range.root == this._document.graveyard ) {
851
- // @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
852
-
853
- return;
854
- }
855
-
856
- const liveRange = LiveRange.fromRange( range );
857
-
858
- // If selection range is moved to the graveyard remove it from the selection object.
859
- // Also, save some data that can be used to restore selection later, on `Model#applyOperation` event.
860
- liveRange.on( 'change:range', ( evt, oldRange, data ) => {
861
- this._hasChangedRange = true;
862
-
863
- if ( liveRange.root == this._document.graveyard ) {
864
- this._selectionRestorePosition = data.deletionPosition;
865
-
866
- const index = this._ranges.indexOf( liveRange );
867
- this._ranges.splice( index, 1 );
868
- liveRange.detach();
869
- }
870
- } );
871
-
872
- return liveRange;
873
- }
874
-
875
- _updateMarkers() {
876
- if ( !this._observedMarkers.size ) {
877
- return;
878
- }
879
-
880
- const markers = [];
881
- let changed = false;
882
-
883
- for ( const marker of this._model.markers ) {
884
- const markerGroup = marker.name.split( ':', 1 )[ 0 ];
885
-
886
- if ( !this._observedMarkers.has( markerGroup ) ) {
887
- continue;
888
- }
889
-
890
- const markerRange = marker.getRange();
891
-
892
- for ( const selectionRange of this.getRanges() ) {
893
- if ( markerRange.containsRange( selectionRange, !selectionRange.isCollapsed ) ) {
894
- markers.push( marker );
895
- }
896
- }
897
- }
898
-
899
- const oldMarkers = Array.from( this.markers );
900
-
901
- for ( const marker of markers ) {
902
- if ( !this.markers.has( marker ) ) {
903
- this.markers.add( marker );
904
-
905
- changed = true;
906
- }
907
- }
908
-
909
- for ( const marker of Array.from( this.markers ) ) {
910
- if ( !markers.includes( marker ) ) {
911
- this.markers.remove( marker );
912
-
913
- changed = true;
914
- }
915
- }
916
-
917
- if ( changed ) {
918
- this.fire( 'change:marker', { oldMarkers, directChange: false } );
919
- }
920
- }
921
-
922
- _updateMarker( marker, markerRange ) {
923
- const markerGroup = marker.name.split( ':', 1 )[ 0 ];
924
-
925
- if ( !this._observedMarkers.has( markerGroup ) ) {
926
- return;
927
- }
928
-
929
- let changed = false;
930
-
931
- const oldMarkers = Array.from( this.markers );
932
- const hasMarker = this.markers.has( marker );
933
-
934
- if ( !markerRange ) {
935
- if ( hasMarker ) {
936
- this.markers.remove( marker );
937
- changed = true;
938
- }
939
- } else {
940
- let contained = false;
941
-
942
- for ( const selectionRange of this.getRanges() ) {
943
- if ( markerRange.containsRange( selectionRange, !selectionRange.isCollapsed ) ) {
944
- contained = true;
945
-
946
- break;
947
- }
948
- }
949
-
950
- if ( contained && !hasMarker ) {
951
- this.markers.add( marker );
952
-
953
- changed = true;
954
- } else if ( !contained && hasMarker ) {
955
- this.markers.remove( marker );
956
-
957
- changed = true;
958
- }
959
- }
960
-
961
- if ( changed ) {
962
- this.fire( 'change:marker', { oldMarkers, directChange: false } );
963
- }
964
- }
965
-
966
- // Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
967
- //
968
- // @protected
969
- // @param {Boolean} clearAll
970
- // @fires change:attribute
971
- _updateAttributes( clearAll ) {
972
- const newAttributes = toMap( this._getSurroundingAttributes() );
973
- const oldAttributes = toMap( this.getAttributes() );
974
-
975
- if ( clearAll ) {
976
- // If `clearAll` remove all attributes and reset priorities.
977
- this._attributePriority = new Map();
978
- this._attrs = new Map();
979
- } else {
980
- // If not, remove only attributes added with `low` priority.
981
- for ( const [ key, priority ] of this._attributePriority ) {
982
- if ( priority == 'low' ) {
983
- this._attrs.delete( key );
984
- this._attributePriority.delete( key );
985
- }
986
- }
987
- }
988
-
989
- this._setAttributesTo( newAttributes );
990
-
991
- // Let's evaluate which attributes really changed.
992
- const changed = [];
993
-
994
- // First, loop through all attributes that are set on selection right now.
995
- // Check which of them are different than old attributes.
996
- for ( const [ newKey, newValue ] of this.getAttributes() ) {
997
- if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
998
- changed.push( newKey );
999
- }
1000
- }
1001
-
1002
- // Then, check which of old attributes got removed.
1003
- for ( const [ oldKey ] of oldAttributes ) {
1004
- if ( !this.hasAttribute( oldKey ) ) {
1005
- changed.push( oldKey );
1006
- }
1007
- }
1008
-
1009
- // Fire event with exact data (fire only if anything changed).
1010
- if ( changed.length > 0 ) {
1011
- this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
1012
- }
1013
- }
1014
-
1015
- // Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
1016
- // parameter).
1017
- //
1018
- // @private
1019
- // @param {String} key Attribute key.
1020
- // @param {*} value Attribute value.
1021
- // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
1022
- // is caused by `Batch` API.
1023
- // @returns {Boolean} Whether value has changed.
1024
- _setAttribute( key, value, directChange = true ) {
1025
- const priority = directChange ? 'normal' : 'low';
1026
-
1027
- if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
1028
- // Priority too low.
1029
- return false;
1030
- }
1031
-
1032
- const oldValue = super.getAttribute( key );
1033
-
1034
- // Don't do anything if value has not changed.
1035
- if ( oldValue === value ) {
1036
- return false;
1037
- }
1038
-
1039
- this._attrs.set( key, value );
1040
-
1041
- // Update priorities map.
1042
- this._attributePriority.set( key, priority );
1043
-
1044
- return true;
1045
- }
1046
-
1047
- // Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
1048
- // parameter).
1049
- //
1050
- // NOTE: Even if attribute is not present in the selection but is provided to this method, it's priority will
1051
- // be changed according to `directChange` parameter.
1052
- //
1053
- // @private
1054
- // @param {String} key Attribute key.
1055
- // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
1056
- // is caused by `Batch` API.
1057
- // @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
1058
- // existing attribute had higher priority.
1059
- _removeAttribute( key, directChange = true ) {
1060
- const priority = directChange ? 'normal' : 'low';
1061
-
1062
- if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
1063
- // Priority too low.
1064
- return false;
1065
- }
1066
-
1067
- // Update priorities map.
1068
- this._attributePriority.set( key, priority );
1069
-
1070
- // Don't do anything if value has not changed.
1071
- if ( !super.hasAttribute( key ) ) {
1072
- return false;
1073
- }
1074
-
1075
- this._attrs.delete( key );
1076
-
1077
- return true;
1078
- }
1079
-
1080
- // Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
1081
- // `directChange` parameter).
1082
- //
1083
- // @private
1084
- // @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
1085
- // @returns {Set.<String>} Changed attribute keys.
1086
- _setAttributesTo( attrs ) {
1087
- const changed = new Set();
1088
-
1089
- for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
1090
- // Do not remove attribute if attribute with same key and value is about to be set.
1091
- if ( attrs.get( oldKey ) === oldValue ) {
1092
- continue;
1093
- }
1094
-
1095
- // All rest attributes will be removed so changed attributes won't change .
1096
- this._removeAttribute( oldKey, false );
1097
- }
1098
-
1099
- for ( const [ key, value ] of attrs ) {
1100
- // Attribute may not be set because of attributes or because same key/value is already added.
1101
- const gotAdded = this._setAttribute( key, value, false );
1102
-
1103
- if ( gotAdded ) {
1104
- changed.add( key );
1105
- }
1106
- }
1107
-
1108
- return changed;
1109
- }
1110
-
1111
- // Returns an iterable that iterates through all selection attributes stored in current selection's parent.
1112
- //
1113
- // @protected
1114
- // @returns {Iterable.<*>}
1115
- * _getStoredAttributes() {
1116
- const selectionParent = this.getFirstPosition().parent;
1117
-
1118
- if ( this.isCollapsed && selectionParent.isEmpty ) {
1119
- for ( const key of selectionParent.getAttributeKeys() ) {
1120
- if ( key.startsWith( storePrefix ) ) {
1121
- const realKey = key.substr( storePrefix.length );
1122
-
1123
- yield [ realKey, selectionParent.getAttribute( key ) ];
1124
- }
1125
- }
1126
- }
1127
- }
1128
-
1129
- // Checks model text nodes that are closest to the selection's first position and returns attributes of first
1130
- // found element. If there are no text nodes in selection's first position parent, it returns selection
1131
- // attributes stored in that parent.
1132
- //
1133
- // @private
1134
- // @returns {Iterable.<*>} Collection of attributes.
1135
- _getSurroundingAttributes() {
1136
- const position = this.getFirstPosition();
1137
- const schema = this._model.schema;
1138
-
1139
- let attrs = null;
1140
-
1141
- if ( !this.isCollapsed ) {
1142
- // 1. If selection is a range...
1143
- const range = this.getFirstRange();
1144
-
1145
- // ...look for a first character node in that range and take attributes from it.
1146
- for ( const value of range ) {
1147
- // If the item is an object, we don't want to get attributes from its children.
1148
- if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
1149
- break;
1150
- }
1151
-
1152
- if ( value.type == 'text' ) {
1153
- attrs = value.item.getAttributes();
1154
- break;
1155
- }
1156
- }
1157
- } else {
1158
- // 2. If the selection is a caret or the range does not contain a character node...
1159
-
1160
- const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
1161
- const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
1162
-
1163
- // When gravity is overridden then don't take node before into consideration.
1164
- if ( !this.isGravityOverridden ) {
1165
- // ...look at the node before caret and take attributes from it if it is a character node.
1166
- attrs = getAttrsIfCharacter( nodeBefore );
1167
- }
1168
-
1169
- // 3. If not, look at the node after caret...
1170
- if ( !attrs ) {
1171
- attrs = getAttrsIfCharacter( nodeAfter );
1172
- }
1173
-
1174
- // 4. If not, try to find the first character on the left, that is in the same node.
1175
- // When gravity is overridden then don't take node before into consideration.
1176
- if ( !this.isGravityOverridden && !attrs ) {
1177
- let node = nodeBefore;
1178
-
1179
- while ( node && !schema.isInline( node ) && !attrs ) {
1180
- node = node.previousSibling;
1181
- attrs = getAttrsIfCharacter( node );
1182
- }
1183
- }
1184
-
1185
- // 5. If not found, try to find the first character on the right, that is in the same node.
1186
- if ( !attrs ) {
1187
- let node = nodeAfter;
1188
-
1189
- while ( node && !schema.isInline( node ) && !attrs ) {
1190
- node = node.nextSibling;
1191
- attrs = getAttrsIfCharacter( node );
1192
- }
1193
- }
1194
-
1195
- // 6. If not found, selection should retrieve attributes from parent.
1196
- if ( !attrs ) {
1197
- attrs = this._getStoredAttributes();
1198
- }
1199
- }
1200
-
1201
- return attrs;
1202
- }
1203
-
1204
- // Fixes the selection after all its ranges got removed.
1205
- //
1206
- // @private
1207
- // @param {module:engine/model/position~Position} deletionPosition Position where the deletion happened.
1208
- _fixGraveyardSelection( deletionPosition ) {
1209
- // Find a range that is a correct selection range and is closest to the position where the deletion happened.
1210
- const selectionRange = this._model.schema.getNearestSelectionRange( deletionPosition );
1211
-
1212
- // If nearest valid selection range has been found - add it in the place of old range.
1213
- if ( selectionRange ) {
1214
- // Check the range, convert it to live range, bind events, etc.
1215
- this._pushRange( selectionRange );
1216
- }
1217
- // If nearest valid selection range cannot be found don't add any range. Selection will be set to the default range.
1218
- }
514
+ // Creates an empty live selection for given {@link module:engine/model/document~Document}.
515
+ // @param {module:engine/model/document~Document} doc Document which owns this selection.
516
+ constructor(doc) {
517
+ super();
518
+ // List of selection markers.
519
+ // Marker is a selection marker when selection range is inside the marker range.
520
+ //
521
+ // @type {module:utils/collection~Collection}
522
+ this.markers = new Collection({ idProperty: 'name' });
523
+ // Document which owns this selection.
524
+ //
525
+ // @protected
526
+ // @member {module:engine/model/model~Model}
527
+ this._model = doc.model;
528
+ // Document which owns this selection.
529
+ //
530
+ // @protected
531
+ // @member {module:engine/model/document~Document}
532
+ this._document = doc;
533
+ // Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
534
+ // last time. Possible values of priority are: `'low'` and `'normal'`.
535
+ //
536
+ // Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
537
+ // attributes API are set with `'normal'` priority.
538
+ //
539
+ // @private
540
+ // @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
541
+ this._attributePriority = new Map();
542
+ // Position to which the selection should be set if the last selection range was moved to the graveyard.
543
+ // @private
544
+ // @member {module:engine/model/position~Position} module:engine/model/liveselection~LiveSelection#_selectionRestorePosition
545
+ this._selectionRestorePosition = null;
546
+ // Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
547
+ // @private
548
+ // @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
549
+ this._hasChangedRange = false;
550
+ // Each overriding gravity adds an UID to the set and each removal removes it.
551
+ // Gravity is overridden when there's at least one UID in the set.
552
+ // Gravity is restored when the set is empty.
553
+ // This is to prevent conflicts when gravity is overridden by more than one feature at the same time.
554
+ // @private
555
+ // @type {Set}
556
+ this._overriddenGravityRegister = new Set();
557
+ // Prefixes of marker names that should affect `LiveSelection#markers` collection.
558
+ // @private
559
+ // @type {Set}
560
+ this._observedMarkers = new Set();
561
+ // Ensure selection is correct after each operation.
562
+ this.listenTo(this._model, 'applyOperation', (evt, args) => {
563
+ const operation = args[0];
564
+ if (!operation.isDocumentOperation || operation.type == 'marker' || operation.type == 'rename' || operation.type == 'noop') {
565
+ return;
566
+ }
567
+ // Fix selection if the last range was removed from it and we have a position to which we can restore the selection.
568
+ if (this._ranges.length == 0 && this._selectionRestorePosition) {
569
+ this._fixGraveyardSelection(this._selectionRestorePosition);
570
+ }
571
+ // "Forget" the restore position even if it was not "used".
572
+ this._selectionRestorePosition = null;
573
+ if (this._hasChangedRange) {
574
+ this._hasChangedRange = false;
575
+ this.fire('change:range', { directChange: false });
576
+ }
577
+ }, { priority: 'lowest' });
578
+ // Ensure selection is correct and up to date after each range change.
579
+ this.on('change:range', () => {
580
+ this._validateSelectionRanges(this.getRanges());
581
+ });
582
+ // Update markers data stored by the selection after each marker change.
583
+ // This handles only marker changes done through marker operations (not model tree changes).
584
+ this.listenTo(this._model.markers, 'update', (evt, marker, oldRange, newRange) => {
585
+ this._updateMarker(marker, newRange);
586
+ });
587
+ // Ensure selection is up to date after each change block.
588
+ this.listenTo(this._document, 'change', (evt, batch) => {
589
+ clearAttributesStoredInElement(this._model, batch);
590
+ });
591
+ }
592
+ get isCollapsed() {
593
+ const length = this._ranges.length;
594
+ return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
595
+ }
596
+ get anchor() {
597
+ return super.anchor || this._document._getDefaultRange().start;
598
+ }
599
+ get focus() {
600
+ return super.focus || this._document._getDefaultRange().end;
601
+ }
602
+ get rangeCount() {
603
+ return this._ranges.length ? this._ranges.length : 1;
604
+ }
605
+ // Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
606
+ // {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
607
+ //
608
+ // @readonly
609
+ // @type {Boolean}
610
+ get hasOwnRange() {
611
+ return this._ranges.length > 0;
612
+ }
613
+ // When set to `true` then selection attributes on node before the caret won't be taken
614
+ // into consideration while updating selection attributes.
615
+ //
616
+ // @protected
617
+ // @type {Boolean}
618
+ get isGravityOverridden() {
619
+ return !!this._overriddenGravityRegister.size;
620
+ }
621
+ // Unbinds all events previously bound by live selection.
622
+ destroy() {
623
+ for (let i = 0; i < this._ranges.length; i++) {
624
+ this._ranges[i].detach();
625
+ }
626
+ this.stopListening();
627
+ }
628
+ *getRanges() {
629
+ if (this._ranges.length) {
630
+ yield* super.getRanges();
631
+ }
632
+ else {
633
+ yield this._document._getDefaultRange();
634
+ }
635
+ }
636
+ getFirstRange() {
637
+ return super.getFirstRange() || this._document._getDefaultRange();
638
+ }
639
+ getLastRange() {
640
+ return super.getLastRange() || this._document._getDefaultRange();
641
+ }
642
+ setTo(...args) {
643
+ super.setTo(...args);
644
+ this._updateAttributes(true);
645
+ this.updateMarkers();
646
+ }
647
+ setFocus(itemOrPosition, offset) {
648
+ super.setFocus(itemOrPosition, offset);
649
+ this._updateAttributes(true);
650
+ this.updateMarkers();
651
+ }
652
+ setAttribute(key, value) {
653
+ if (this._setAttribute(key, value)) {
654
+ // Fire event with exact data.
655
+ const attributeKeys = [key];
656
+ this.fire('change:attribute', { attributeKeys, directChange: true });
657
+ }
658
+ }
659
+ removeAttribute(key) {
660
+ if (this._removeAttribute(key)) {
661
+ // Fire event with exact data.
662
+ const attributeKeys = [key];
663
+ this.fire('change:attribute', { attributeKeys, directChange: true });
664
+ }
665
+ }
666
+ overrideGravity() {
667
+ const overrideUid = uid();
668
+ // Remember that another overriding has been requested. It will need to be removed
669
+ // before the gravity is to be restored.
670
+ this._overriddenGravityRegister.add(overrideUid);
671
+ if (this._overriddenGravityRegister.size === 1) {
672
+ this._updateAttributes(true);
673
+ }
674
+ return overrideUid;
675
+ }
676
+ restoreGravity(uid) {
677
+ if (!this._overriddenGravityRegister.has(uid)) {
678
+ /**
679
+ * Restoring gravity for an unknown UID is not possible. Make sure you are using a correct
680
+ * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
681
+ *
682
+ * @error document-selection-gravity-wrong-restore
683
+ * @param {String} uid The unique identifier returned by
684
+ * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
685
+ */
686
+ throw new CKEditorError('document-selection-gravity-wrong-restore', this, { uid });
687
+ }
688
+ this._overriddenGravityRegister.delete(uid);
689
+ // Restore gravity only when all overriding have been restored.
690
+ if (!this.isGravityOverridden) {
691
+ this._updateAttributes(true);
692
+ }
693
+ }
694
+ observeMarkers(prefixOrName) {
695
+ this._observedMarkers.add(prefixOrName);
696
+ this.updateMarkers();
697
+ }
698
+ _replaceAllRanges(ranges) {
699
+ this._validateSelectionRanges(ranges);
700
+ super._replaceAllRanges(ranges);
701
+ }
702
+ _popRange() {
703
+ this._ranges.pop().detach();
704
+ }
705
+ _pushRange(range) {
706
+ const liveRange = this._prepareRange(range);
707
+ // `undefined` is returned when given `range` is in graveyard root.
708
+ if (liveRange) {
709
+ this._ranges.push(liveRange);
710
+ }
711
+ }
712
+ _validateSelectionRanges(ranges) {
713
+ for (const range of ranges) {
714
+ if (!this._document._validateSelectionRange(range)) {
715
+ /**
716
+ * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
717
+ * starts or ends at incorrect position.
718
+ *
719
+ * @error document-selection-wrong-position
720
+ * @param {module:engine/model/range~Range} range
721
+ */
722
+ throw new CKEditorError('document-selection-wrong-position', this, { range });
723
+ }
724
+ }
725
+ }
726
+ // Prepares given range to be added to selection. Checks if it is correct,
727
+ // converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
728
+ // and sets listeners listening to the range's change event.
729
+ //
730
+ // @private
731
+ // @param {module:engine/model/range~Range} range
732
+ _prepareRange(range) {
733
+ this._checkRange(range);
734
+ if (range.root == this._document.graveyard) {
735
+ // @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
736
+ return;
737
+ }
738
+ const liveRange = LiveRange.fromRange(range);
739
+ // If selection range is moved to the graveyard remove it from the selection object.
740
+ // Also, save some data that can be used to restore selection later, on `Model#applyOperation` event.
741
+ liveRange.on('change:range', (evt, oldRange, data) => {
742
+ this._hasChangedRange = true;
743
+ if (liveRange.root == this._document.graveyard) {
744
+ this._selectionRestorePosition = data.deletionPosition;
745
+ const index = this._ranges.indexOf(liveRange);
746
+ this._ranges.splice(index, 1);
747
+ liveRange.detach();
748
+ }
749
+ });
750
+ return liveRange;
751
+ }
752
+ updateMarkers() {
753
+ if (!this._observedMarkers.size) {
754
+ return;
755
+ }
756
+ const markers = [];
757
+ let changed = false;
758
+ for (const marker of this._model.markers) {
759
+ const markerGroup = marker.name.split(':', 1)[0];
760
+ if (!this._observedMarkers.has(markerGroup)) {
761
+ continue;
762
+ }
763
+ const markerRange = marker.getRange();
764
+ for (const selectionRange of this.getRanges()) {
765
+ if (markerRange.containsRange(selectionRange, !selectionRange.isCollapsed)) {
766
+ markers.push(marker);
767
+ }
768
+ }
769
+ }
770
+ const oldMarkers = Array.from(this.markers);
771
+ for (const marker of markers) {
772
+ if (!this.markers.has(marker)) {
773
+ this.markers.add(marker);
774
+ changed = true;
775
+ }
776
+ }
777
+ for (const marker of Array.from(this.markers)) {
778
+ if (!markers.includes(marker)) {
779
+ this.markers.remove(marker);
780
+ changed = true;
781
+ }
782
+ }
783
+ if (changed) {
784
+ this.fire('change:marker', { oldMarkers, directChange: false });
785
+ }
786
+ }
787
+ _updateMarker(marker, markerRange) {
788
+ const markerGroup = marker.name.split(':', 1)[0];
789
+ if (!this._observedMarkers.has(markerGroup)) {
790
+ return;
791
+ }
792
+ let changed = false;
793
+ const oldMarkers = Array.from(this.markers);
794
+ const hasMarker = this.markers.has(marker);
795
+ if (!markerRange) {
796
+ if (hasMarker) {
797
+ this.markers.remove(marker);
798
+ changed = true;
799
+ }
800
+ }
801
+ else {
802
+ let contained = false;
803
+ for (const selectionRange of this.getRanges()) {
804
+ if (markerRange.containsRange(selectionRange, !selectionRange.isCollapsed)) {
805
+ contained = true;
806
+ break;
807
+ }
808
+ }
809
+ if (contained && !hasMarker) {
810
+ this.markers.add(marker);
811
+ changed = true;
812
+ }
813
+ else if (!contained && hasMarker) {
814
+ this.markers.remove(marker);
815
+ changed = true;
816
+ }
817
+ }
818
+ if (changed) {
819
+ this.fire('change:marker', { oldMarkers, directChange: false });
820
+ }
821
+ }
822
+ // Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
823
+ //
824
+ // @protected
825
+ // @param {Boolean} clearAll
826
+ // @fires change:attribute
827
+ _updateAttributes(clearAll) {
828
+ const newAttributes = toMap(this._getSurroundingAttributes());
829
+ const oldAttributes = toMap(this.getAttributes());
830
+ if (clearAll) {
831
+ // If `clearAll` remove all attributes and reset priorities.
832
+ this._attributePriority = new Map();
833
+ this._attrs = new Map();
834
+ }
835
+ else {
836
+ // If not, remove only attributes added with `low` priority.
837
+ for (const [key, priority] of this._attributePriority) {
838
+ if (priority == 'low') {
839
+ this._attrs.delete(key);
840
+ this._attributePriority.delete(key);
841
+ }
842
+ }
843
+ }
844
+ this._setAttributesTo(newAttributes);
845
+ // Let's evaluate which attributes really changed.
846
+ const changed = [];
847
+ // First, loop through all attributes that are set on selection right now.
848
+ // Check which of them are different than old attributes.
849
+ for (const [newKey, newValue] of this.getAttributes()) {
850
+ if (!oldAttributes.has(newKey) || oldAttributes.get(newKey) !== newValue) {
851
+ changed.push(newKey);
852
+ }
853
+ }
854
+ // Then, check which of old attributes got removed.
855
+ for (const [oldKey] of oldAttributes) {
856
+ if (!this.hasAttribute(oldKey)) {
857
+ changed.push(oldKey);
858
+ }
859
+ }
860
+ // Fire event with exact data (fire only if anything changed).
861
+ if (changed.length > 0) {
862
+ this.fire('change:attribute', { attributeKeys: changed, directChange: false });
863
+ }
864
+ }
865
+ // Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
866
+ // parameter).
867
+ //
868
+ // @private
869
+ // @param {String} key Attribute key.
870
+ // @param {*} value Attribute value.
871
+ // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
872
+ // is caused by `Batch` API.
873
+ // @returns {Boolean} Whether value has changed.
874
+ _setAttribute(key, value, directChange = true) {
875
+ const priority = directChange ? 'normal' : 'low';
876
+ if (priority == 'low' && this._attributePriority.get(key) == 'normal') {
877
+ // Priority too low.
878
+ return false;
879
+ }
880
+ const oldValue = super.getAttribute(key);
881
+ // Don't do anything if value has not changed.
882
+ if (oldValue === value) {
883
+ return false;
884
+ }
885
+ this._attrs.set(key, value);
886
+ // Update priorities map.
887
+ this._attributePriority.set(key, priority);
888
+ return true;
889
+ }
890
+ // Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
891
+ // parameter).
892
+ //
893
+ // NOTE: Even if attribute is not present in the selection but is provided to this method, it's priority will
894
+ // be changed according to `directChange` parameter.
895
+ //
896
+ // @private
897
+ // @param {String} key Attribute key.
898
+ // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
899
+ // is caused by `Batch` API.
900
+ // @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
901
+ // existing attribute had higher priority.
902
+ _removeAttribute(key, directChange = true) {
903
+ const priority = directChange ? 'normal' : 'low';
904
+ if (priority == 'low' && this._attributePriority.get(key) == 'normal') {
905
+ // Priority too low.
906
+ return false;
907
+ }
908
+ // Update priorities map.
909
+ this._attributePriority.set(key, priority);
910
+ // Don't do anything if value has not changed.
911
+ if (!super.hasAttribute(key)) {
912
+ return false;
913
+ }
914
+ this._attrs.delete(key);
915
+ return true;
916
+ }
917
+ // Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
918
+ // `directChange` parameter).
919
+ //
920
+ // @private
921
+ // @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
922
+ // @returns {Set.<String>} Changed attribute keys.
923
+ _setAttributesTo(attrs) {
924
+ const changed = new Set();
925
+ for (const [oldKey, oldValue] of this.getAttributes()) {
926
+ // Do not remove attribute if attribute with same key and value is about to be set.
927
+ if (attrs.get(oldKey) === oldValue) {
928
+ continue;
929
+ }
930
+ // All rest attributes will be removed so changed attributes won't change .
931
+ this._removeAttribute(oldKey, false);
932
+ }
933
+ for (const [key, value] of attrs) {
934
+ // Attribute may not be set because of attributes or because same key/value is already added.
935
+ const gotAdded = this._setAttribute(key, value, false);
936
+ if (gotAdded) {
937
+ changed.add(key);
938
+ }
939
+ }
940
+ return changed;
941
+ }
942
+ // Returns an iterable that iterates through all selection attributes stored in current selection's parent.
943
+ //
944
+ // @public
945
+ // @returns {Iterable.<*>}
946
+ *getStoredAttributes() {
947
+ const selectionParent = this.getFirstPosition().parent;
948
+ if (this.isCollapsed && selectionParent.isEmpty) {
949
+ for (const key of selectionParent.getAttributeKeys()) {
950
+ if (key.startsWith(storePrefix)) {
951
+ const realKey = key.substr(storePrefix.length);
952
+ yield [realKey, selectionParent.getAttribute(key)];
953
+ }
954
+ }
955
+ }
956
+ }
957
+ // Checks model text nodes that are closest to the selection's first position and returns attributes of first
958
+ // found element. If there are no text nodes in selection's first position parent, it returns selection
959
+ // attributes stored in that parent.
960
+ //
961
+ // @private
962
+ // @returns {Iterable.<*>} Collection of attributes.
963
+ _getSurroundingAttributes() {
964
+ const position = this.getFirstPosition();
965
+ const schema = this._model.schema;
966
+ let attrs = null;
967
+ if (!this.isCollapsed) {
968
+ // 1. If selection is a range...
969
+ const range = this.getFirstRange();
970
+ // ...look for a first character node in that range and take attributes from it.
971
+ for (const value of range) {
972
+ // If the item is an object, we don't want to get attributes from its children.
973
+ if (value.item.is('element') && schema.isObject(value.item)) {
974
+ break;
975
+ }
976
+ if (value.type == 'text') {
977
+ attrs = value.item.getAttributes();
978
+ break;
979
+ }
980
+ }
981
+ }
982
+ else {
983
+ // 2. If the selection is a caret or the range does not contain a character node...
984
+ const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
985
+ const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
986
+ // When gravity is overridden then don't take node before into consideration.
987
+ if (!this.isGravityOverridden) {
988
+ // ...look at the node before caret and take attributes from it if it is a character node.
989
+ attrs = getAttrsIfCharacter(nodeBefore);
990
+ }
991
+ // 3. If not, look at the node after caret...
992
+ if (!attrs) {
993
+ attrs = getAttrsIfCharacter(nodeAfter);
994
+ }
995
+ // 4. If not, try to find the first character on the left, that is in the same node.
996
+ // When gravity is overridden then don't take node before into consideration.
997
+ if (!this.isGravityOverridden && !attrs) {
998
+ let node = nodeBefore;
999
+ while (node && !schema.isInline(node) && !attrs) {
1000
+ node = node.previousSibling;
1001
+ attrs = getAttrsIfCharacter(node);
1002
+ }
1003
+ }
1004
+ // 5. If not found, try to find the first character on the right, that is in the same node.
1005
+ if (!attrs) {
1006
+ let node = nodeAfter;
1007
+ while (node && !schema.isInline(node) && !attrs) {
1008
+ node = node.nextSibling;
1009
+ attrs = getAttrsIfCharacter(node);
1010
+ }
1011
+ }
1012
+ // 6. If not found, selection should retrieve attributes from parent.
1013
+ if (!attrs) {
1014
+ attrs = this.getStoredAttributes();
1015
+ }
1016
+ }
1017
+ return attrs;
1018
+ }
1019
+ // Fixes the selection after all its ranges got removed.
1020
+ //
1021
+ // @private
1022
+ // @param {module:engine/model/position~Position} deletionPosition Position where the deletion happened.
1023
+ _fixGraveyardSelection(deletionPosition) {
1024
+ // Find a range that is a correct selection range and is closest to the position where the deletion happened.
1025
+ const selectionRange = this._model.schema.getNearestSelectionRange(deletionPosition);
1026
+ // If nearest valid selection range has been found - add it in the place of old range.
1027
+ if (selectionRange) {
1028
+ // Check the range, convert it to live range, bind events, etc.
1029
+ this._pushRange(selectionRange);
1030
+ }
1031
+ // If nearest valid selection range cannot be found don't add any range. Selection will be set to the default range.
1032
+ }
1219
1033
  }
1220
-
1221
1034
  // Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
1222
1035
  //
1223
1036
  // It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
1224
1037
  //
1225
1038
  // @param {module:engine/model/item~Item|null} node
1226
1039
  // @returns {Boolean}
1227
- function getAttrsIfCharacter( node ) {
1228
- if ( node instanceof TextProxy || node instanceof Text ) {
1229
- return node.getAttributes();
1230
- }
1231
-
1232
- return null;
1040
+ function getAttrsIfCharacter(node) {
1041
+ if (node instanceof TextProxy || node instanceof Text) {
1042
+ return node.getAttributes();
1043
+ }
1044
+ return null;
1233
1045
  }
1234
-
1235
1046
  // Removes selection attributes from element which is not empty anymore.
1236
1047
  //
1237
1048
  // @param {module:engine/model/model~Model} model
1238
1049
  // @param {module:engine/model/batch~Batch} batch
1239
- function clearAttributesStoredInElement( model, batch ) {
1240
- const differ = model.document.differ;
1241
-
1242
- for ( const entry of differ.getChanges() ) {
1243
- if ( entry.type != 'insert' ) {
1244
- continue;
1245
- }
1246
-
1247
- const changeParent = entry.position.parent;
1248
- const isNoLongerEmpty = entry.length === changeParent.maxOffset;
1249
-
1250
- if ( isNoLongerEmpty ) {
1251
- model.enqueueChange( batch, writer => {
1252
- const storedAttributes = Array.from( changeParent.getAttributeKeys() )
1253
- .filter( key => key.startsWith( storePrefix ) );
1254
-
1255
- for ( const key of storedAttributes ) {
1256
- writer.removeAttribute( key, changeParent );
1257
- }
1258
- } );
1259
- }
1260
- }
1050
+ function clearAttributesStoredInElement(model, batch) {
1051
+ const differ = model.document.differ;
1052
+ for (const entry of differ.getChanges()) {
1053
+ if (entry.type != 'insert') {
1054
+ continue;
1055
+ }
1056
+ const changeParent = entry.position.parent;
1057
+ const isNoLongerEmpty = entry.length === changeParent.maxOffset;
1058
+ if (isNoLongerEmpty) {
1059
+ model.enqueueChange(batch, writer => {
1060
+ const storedAttributes = Array.from(changeParent.getAttributeKeys())
1061
+ .filter(key => key.startsWith(storePrefix));
1062
+ for (const key of storedAttributes) {
1063
+ writer.removeAttribute(key, changeParent);
1064
+ }
1065
+ });
1066
+ }
1067
+ }
1261
1068
  }