@ckeditor/ckeditor5-engine 32.0.0 → 33.0.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.
@@ -31,6 +31,7 @@ import Mapper from '../conversion/mapper';
31
31
  import {
32
32
  convertCollapsedSelection,
33
33
  convertRangeSelection,
34
+ insertAttributesAndChildren,
34
35
  insertElement,
35
36
  insertText,
36
37
  insertUIElement,
@@ -233,6 +234,7 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
233
234
  mapper.bindElements( node.root, viewRoot );
234
235
 
235
236
  downcastDispatcher.on( 'insert:$text', insertText() );
237
+ downcastDispatcher.on( 'insert', insertAttributesAndChildren(), { priority: 'lowest' } );
236
238
  downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
237
239
  if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( '$textProxy' ) ) {
238
240
  const converter = wrap( ( modelAttributeValue, { writer } ) => {
@@ -260,28 +262,28 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
260
262
  return writer.createUIElement( name );
261
263
  } ) );
262
264
 
265
+ const markersMap = new Map();
266
+
267
+ if ( markers ) {
268
+ // To provide stable results, sort markers by name.
269
+ for ( const marker of Array.from( markers ).sort( ( a, b ) => a.name < b.name ? 1 : -1 ) ) {
270
+ markersMap.set( marker.name, marker.getRange() );
271
+ }
272
+ }
273
+
263
274
  // Convert model to view.
264
275
  const writer = view._writer;
265
- downcastDispatcher.convertInsert( range, writer );
276
+ downcastDispatcher.convert( range, markersMap, writer );
266
277
 
267
278
  // Convert model selection to view selection.
268
279
  if ( selection ) {
269
280
  downcastDispatcher.convertSelection( selection, markers || model.markers, writer );
270
281
  }
271
282
 
272
- if ( markers ) {
273
- // To provide stable results, sort markers by name.
274
- markers = Array.from( markers ).sort( ( a, b ) => a.name < b.name ? 1 : -1 );
275
-
276
- for ( const marker of markers ) {
277
- downcastDispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
278
- }
279
- }
280
-
281
283
  // Parse view to data string.
282
284
  let data = viewStringify( viewRoot, viewDocument.selection, { sameSelectionCharacters: true } );
283
285
 
284
- // Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
286
+ // Removing unnecessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
285
287
  data = data.substr( 5, data.length - 11 );
286
288
 
287
289
  view.destroy();
package/src/index.js CHANGED
@@ -28,6 +28,7 @@ export { default as LivePosition } from './model/liveposition';
28
28
  export { default as Model } from './model/model';
29
29
  export { default as TreeWalker } from './model/treewalker';
30
30
  export { default as Element } from './model/element';
31
+ export { default as History } from './model/history';
31
32
 
32
33
  export { default as DomConverter } from './view/domconverter';
33
34
  export { default as Renderer } from './view/renderer';
@@ -27,20 +27,20 @@ export default class Batch {
27
27
  *
28
28
  * @see module:engine/model/model~Model#enqueueChange
29
29
  * @see module:engine/model/model~Model#change
30
- * @param {Object} [type] Set of flags that specifies the type of the batch. Batch type can alter how some of the features work when
31
- * encountering given `Batch` instance (for example, when a feature listens to applied operations).
32
- * @param {Boolean} [type.isUndoable=true] Whether batch can be undone through undo feature.
33
- * @param {Boolean} [type.isLocal=true] Whether batch includes operations created locally (`true`) or operations created on
30
+ * @param {Object} [type] A set of flags that specify the type of the batch. Batch type can alter how some of the features work
31
+ * when encountering a given `Batch` instance (for example, when a feature listens to applied operations).
32
+ * @param {Boolean} [type.isUndoable=true] Whether a batch can be undone through undo feature.
33
+ * @param {Boolean} [type.isLocal=true] Whether a batch includes operations created locally (`true`) or operations created on
34
34
  * other, remote editors (`false`).
35
- * @param {Boolean} [type.isUndo=false] Whether batch was created by the undo feature and undoes other operations.
36
- * @param {Boolean} [type.isTyping=false] Whether batch includes operations connected with typing action.
35
+ * @param {Boolean} [type.isUndo=false] Whether a batch was created by the undo feature and undoes other operations.
36
+ * @param {Boolean} [type.isTyping=false] Whether a batch includes operations connected with a typing action.
37
37
  */
38
38
  constructor( type = {} ) {
39
39
  if ( typeof type === 'string' ) {
40
40
  type = type === 'transparent' ? { isUndoable: false } : {};
41
41
 
42
42
  /**
43
- * The string value for `type` property of the `Batch` constructor has been deprecated and will be removed in the near future.
43
+ * The string value for a `type` property of the `Batch` constructor has been deprecated and will be removed in the near future.
44
44
  * Please refer to the {@link module:engine/model/batch~Batch#constructor `Batch` constructor API documentation} for more
45
45
  * information.
46
46
  *
@@ -60,7 +60,7 @@ export default class Batch {
60
60
  this.operations = [];
61
61
 
62
62
  /**
63
- * Whether batch can be undone through the undo feature.
63
+ * Whether the batch can be undone through the undo feature.
64
64
  *
65
65
  * @readonly
66
66
  * @type {Boolean}
@@ -68,7 +68,7 @@ export default class Batch {
68
68
  this.isUndoable = isUndoable;
69
69
 
70
70
  /**
71
- * Whether batch includes operations created locally (`true`) or operations created on other, remote editors (`false`).
71
+ * Whether the batch includes operations created locally (`true`) or operations created on other, remote editors (`false`).
72
72
  *
73
73
  * @readonly
74
74
  * @type {Boolean}
@@ -76,7 +76,7 @@ export default class Batch {
76
76
  this.isLocal = isLocal;
77
77
 
78
78
  /**
79
- * Whether batch was created by the undo feature and undoes other operations.
79
+ * Whether the batch was created by the undo feature and undoes other operations.
80
80
  *
81
81
  * @readonly
82
82
  * @type {Boolean}
@@ -84,7 +84,7 @@ export default class Batch {
84
84
  this.isUndo = isUndo;
85
85
 
86
86
  /**
87
- * Whether batch includes operations connected with typing.
87
+ * Whether the batch includes operations connected with typing.
88
88
  *
89
89
  * @readonly
90
90
  * @type {Boolean}
@@ -95,7 +95,7 @@ export default class Batch {
95
95
  /**
96
96
  * The type of the batch.
97
97
  *
98
- * **This property has been deprecated and is always set to `'default'` value.**
98
+ * **This property has been deprecated and is always set to the `'default'` value.**
99
99
  *
100
100
  * It can be one of the following values:
101
101
  * * `'default'` &ndash; All "normal" batches. This is the most commonly used type.
@@ -58,11 +58,12 @@ export default class Differ {
58
58
  * A map that stores all changed markers.
59
59
  *
60
60
  * The keys of the map are marker names.
61
- * The values of the map are objects with the `oldRange` and `newRange` properties. They store the marker range
62
- * state before and after the change.
61
+ * The values of the map are objects with the following properties:
62
+ * - `oldMarkerData`,
63
+ * - `newMarkerData`.
63
64
  *
64
65
  * @private
65
- * @type {Map}
66
+ * @type {Map.<String, Object>}
66
67
  */
67
68
  this._changedMarkers = new Map();
68
69
 
@@ -98,6 +99,14 @@ export default class Differ {
98
99
  * @type {Array.<Object>|null}
99
100
  */
100
101
  this._cachedChangesWithGraveyard = null;
102
+
103
+ /**
104
+ * Set of model items that were marked to get refreshed in {@link #_refreshItem}.
105
+ *
106
+ * @private
107
+ * @type {Set.<module:engine/model/item~Item>}
108
+ */
109
+ this._refreshedItems = new Set();
101
110
  }
102
111
 
103
112
  /**
@@ -110,32 +119,6 @@ export default class Differ {
110
119
  return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
111
120
  }
112
121
 
113
- /**
114
- * Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
115
- * set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
116
- *
117
- * @param {module:engine/model/item~Item} item Item to refresh.
118
- */
119
- refreshItem( item ) {
120
- if ( this._isInInsertedElement( item.parent ) ) {
121
- return;
122
- }
123
-
124
- this._markRemove( item.parent, item.startOffset, item.offsetSize );
125
- this._markInsert( item.parent, item.startOffset, item.offsetSize );
126
-
127
- const range = Range._createOn( item );
128
-
129
- for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
130
- const markerRange = marker.getRange();
131
-
132
- this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
133
- }
134
-
135
- // Clear cache after each buffered operation as it is no longer valid.
136
- this._cachedChanges = null;
137
- }
138
-
139
122
  /**
140
123
  * Buffers the given operation. An operation has to be buffered before it is executed.
141
124
  *
@@ -208,9 +191,9 @@ export default class Differ {
208
191
  const range = Range._createFromPositionAndShift( operation.position, 1 );
209
192
 
210
193
  for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
211
- const markerRange = marker.getRange();
194
+ const markerData = marker.getData();
212
195
 
213
- this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
196
+ this.bufferMarkerChange( marker.name, markerData, markerData );
214
197
  }
215
198
 
216
199
  break;
@@ -267,27 +250,23 @@ export default class Differ {
267
250
  * Buffers a marker change.
268
251
  *
269
252
  * @param {String} markerName The name of the marker that changed.
270
- * @param {module:engine/model/range~Range|null} oldRange Marker range before the change or `null` if the marker has just
271
- * been created.
272
- * @param {module:engine/model/range~Range|null} newRange Marker range after the change or `null` if the marker was removed.
273
- * @param {Boolean} affectsData Flag indicating whether marker affects the editor data.
253
+ * @param {module:engine/model/markercollection~MarkerData} oldMarkerData Marker data before the change.
254
+ * @param {module:engine/model/markercollection~MarkerData} newMarkerData Marker data after the change.
274
255
  */
275
- bufferMarkerChange( markerName, oldRange, newRange, affectsData ) {
256
+ bufferMarkerChange( markerName, oldMarkerData, newMarkerData ) {
276
257
  const buffered = this._changedMarkers.get( markerName );
277
258
 
278
259
  if ( !buffered ) {
279
260
  this._changedMarkers.set( markerName, {
280
- oldRange,
281
- newRange,
282
- affectsData
261
+ newMarkerData,
262
+ oldMarkerData
283
263
  } );
284
264
  } else {
285
- buffered.newRange = newRange;
286
- buffered.affectsData = affectsData;
265
+ buffered.newMarkerData = newMarkerData;
287
266
 
288
- if ( buffered.oldRange == null && buffered.newRange == null ) {
289
- // The marker is going to be removed (`newRange == null`) but it did not exist before the first buffered change
290
- // (`buffered.oldRange == null`). In this case, do not keep the marker in buffer at all.
267
+ if ( buffered.oldMarkerData.range == null && newMarkerData.range == null ) {
268
+ // The marker is going to be removed (`newMarkerData.range == null`) but it did not exist before the first buffered change
269
+ // (`buffered.oldMarkerData.range == null`). In this case, do not keep the marker in buffer at all.
291
270
  this._changedMarkers.delete( markerName );
292
271
  }
293
272
  }
@@ -302,8 +281,8 @@ export default class Differ {
302
281
  const result = [];
303
282
 
304
283
  for ( const [ name, change ] of this._changedMarkers ) {
305
- if ( change.oldRange != null ) {
306
- result.push( { name, range: change.oldRange } );
284
+ if ( change.oldMarkerData.range != null ) {
285
+ result.push( { name, range: change.oldMarkerData.range } );
307
286
  }
308
287
  }
309
288
 
@@ -319,8 +298,8 @@ export default class Differ {
319
298
  const result = [];
320
299
 
321
300
  for ( const [ name, change ] of this._changedMarkers ) {
322
- if ( change.newRange != null ) {
323
- result.push( { name, range: change.newRange } );
301
+ if ( change.newMarkerData.range != null ) {
302
+ result.push( { name, range: change.newMarkerData.range } );
324
303
  }
325
304
  }
326
305
 
@@ -333,12 +312,12 @@ export default class Differ {
333
312
  * @returns {Array.<Object>}
334
313
  */
335
314
  getChangedMarkers() {
336
- return Array.from( this._changedMarkers ).map( item => (
315
+ return Array.from( this._changedMarkers ).map( ( [ name, change ] ) => (
337
316
  {
338
- name: item[ 0 ],
317
+ name,
339
318
  data: {
340
- oldRange: item[ 1 ].oldRange,
341
- newRange: item[ 1 ].newRange
319
+ oldRange: change.oldMarkerData.range,
320
+ newRange: change.newMarkerData.range
342
321
  }
343
322
  }
344
323
  ) );
@@ -351,13 +330,23 @@ export default class Differ {
351
330
  *
352
331
  * * model structure changes,
353
332
  * * attribute changes,
354
- * * changes of markers which were defined as `affectingData`.
333
+ * * changes of markers which were defined as `affectsData`,
334
+ * * changes of markers' `affectsData` property.
355
335
  *
356
336
  * @returns {Boolean}
357
337
  */
358
338
  hasDataChanges() {
359
- for ( const [ , change ] of this._changedMarkers ) {
360
- if ( change.affectsData ) {
339
+ for ( const { newMarkerData, oldMarkerData } of this._changedMarkers.values() ) {
340
+ if ( newMarkerData.affectsData !== oldMarkerData.affectsData ) {
341
+ return true;
342
+ }
343
+
344
+ if ( newMarkerData.affectsData ) {
345
+ // Skip markers, which ranges have not changed.
346
+ if ( newMarkerData.range && oldMarkerData.range && newMarkerData.range.isEqual( oldMarkerData.range ) ) {
347
+ return false;
348
+ }
349
+
361
350
  return true;
362
351
  }
363
352
  }
@@ -540,16 +529,25 @@ export default class Differ {
540
529
  this._changeCount = 0;
541
530
 
542
531
  // Cache changes.
543
- this._cachedChangesWithGraveyard = diffSet.slice();
532
+ this._cachedChangesWithGraveyard = diffSet;
544
533
  this._cachedChanges = diffSet.filter( _changesInGraveyardFilter );
545
534
 
546
535
  if ( options.includeChangesInGraveyard ) {
547
- return this._cachedChangesWithGraveyard;
536
+ return this._cachedChangesWithGraveyard.slice();
548
537
  } else {
549
- return this._cachedChanges;
538
+ return this._cachedChanges.slice();
550
539
  }
551
540
  }
552
541
 
542
+ /**
543
+ * Returns a set of model items that were marked to get refreshed.
544
+ *
545
+ * @return {Set.<module:engine/model/item~Item>}
546
+ */
547
+ getRefreshedItems() {
548
+ return new Set( this._refreshedItems );
549
+ }
550
+
553
551
  /**
554
552
  * Resets `Differ`. Removes all buffered changes.
555
553
  */
@@ -557,6 +555,36 @@ export default class Differ {
557
555
  this._changesInElement.clear();
558
556
  this._elementSnapshots.clear();
559
557
  this._changedMarkers.clear();
558
+ this._refreshedItems = new Set();
559
+ this._cachedChanges = null;
560
+ }
561
+
562
+ /**
563
+ * Marks the given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted
564
+ * in the differ changes set, so it will be effectively re-converted when the differ changes are handled by a dispatcher.
565
+ *
566
+ * @protected
567
+ * @param {module:engine/model/item~Item} item Item to refresh.
568
+ */
569
+ _refreshItem( item ) {
570
+ if ( this._isInInsertedElement( item.parent ) ) {
571
+ return;
572
+ }
573
+
574
+ this._markRemove( item.parent, item.startOffset, item.offsetSize );
575
+ this._markInsert( item.parent, item.startOffset, item.offsetSize );
576
+
577
+ this._refreshedItems.add( item );
578
+
579
+ const range = Range._createOn( item );
580
+
581
+ for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
582
+ const markerData = marker.getData();
583
+
584
+ this.bufferMarkerChange( marker.name, markerData, markerData );
585
+ }
586
+
587
+ // Clear cache after each buffered operation as it is no longer valid.
560
588
  this._cachedChanges = null;
561
589
  }
562
590
 
@@ -157,14 +157,23 @@ export default class Document {
157
157
  // Buffer marker changes.
158
158
  // This is not covered in buffering operations because markers may change outside of them (when they
159
159
  // are modified using `model.markers` collection, not through `MarkerOperation`).
160
- this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
160
+ this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange, oldMarkerData ) => {
161
+ // Copy the `newRange` to the new marker data as during the marker removal the range is not updated.
162
+ const newMarkerData = { ...marker.getData(), range: newRange };
163
+
161
164
  // Whenever marker is updated, buffer that change.
162
- this.differ.bufferMarkerChange( marker.name, oldRange, newRange, marker.affectsData );
165
+ this.differ.bufferMarkerChange( marker.name, oldMarkerData, newMarkerData );
163
166
 
164
167
  if ( oldRange === null ) {
165
168
  // If this is a new marker, add a listener that will buffer change whenever marker changes.
166
169
  marker.on( 'change', ( evt, oldRange ) => {
167
- this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange(), marker.affectsData );
170
+ const markerData = marker.getData();
171
+
172
+ this.differ.bufferMarkerChange(
173
+ marker.name,
174
+ { ...markerData, range: oldRange },
175
+ markerData
176
+ );
168
177
  } );
169
178
  }
170
179
  } );
@@ -106,6 +106,8 @@ export default class MarkerCollection {
106
106
  const oldMarker = this._markers.get( markerName );
107
107
 
108
108
  if ( oldMarker ) {
109
+ const oldMarkerData = oldMarker.getData();
110
+
109
111
  const oldRange = oldMarker.getRange();
110
112
  let hasChanged = false;
111
113
 
@@ -125,7 +127,7 @@ export default class MarkerCollection {
125
127
  }
126
128
 
127
129
  if ( hasChanged ) {
128
- this.fire( 'update:' + markerName, oldMarker, oldRange, range );
130
+ this.fire( 'update:' + markerName, oldMarker, oldRange, range, oldMarkerData );
129
131
  }
130
132
 
131
133
  return oldMarker;
@@ -135,7 +137,7 @@ export default class MarkerCollection {
135
137
  const marker = new Marker( markerName, liveRange, managedUsingOperations, affectsData );
136
138
 
137
139
  this._markers.set( markerName, marker );
138
- this.fire( 'update:' + markerName, marker, null, range );
140
+ this.fire( 'update:' + markerName, marker, null, range, { ...marker.getData(), range: null } );
139
141
 
140
142
  return marker;
141
143
  }
@@ -154,7 +156,7 @@ export default class MarkerCollection {
154
156
 
155
157
  if ( oldMarker ) {
156
158
  this._markers.delete( markerName );
157
- this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null );
159
+ this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null, oldMarker.getData() );
158
160
 
159
161
  this._destroyMarker( oldMarker );
160
162
 
@@ -188,7 +190,7 @@ export default class MarkerCollection {
188
190
 
189
191
  const range = marker.getRange();
190
192
 
191
- this.fire( 'update:' + markerName, marker, range, range, marker.managedUsingOperations, marker.affectsData );
193
+ this.fire( 'update:' + markerName, marker, range, range, marker.getData() );
192
194
  }
193
195
 
194
196
  /**
@@ -273,11 +275,20 @@ export default class MarkerCollection {
273
275
  * means that marker is just added.
274
276
  * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
275
277
  * means that marker is just removed.
278
+ * @param {module:engine/model/markercollection~MarkerData} oldMarkerData Data of the marker before the change.
276
279
  */
277
280
  }
278
281
 
279
282
  mix( MarkerCollection, EmitterMixin );
280
283
 
284
+ /**
285
+ * @typedef {Object} module:engine/model/markercollection~MarkerData
286
+ *
287
+ * @property {module:engine/model/range~Range|null} range Marker range. `null` if the marker was removed.
288
+ * @property {Boolean} affectsData A property defining if the marker affects data.
289
+ * @property {Boolean} managedUsingOperations A property defining if the marker is managed using operations.
290
+ */
291
+
281
292
  /**
282
293
  * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
283
294
  * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
@@ -418,6 +429,19 @@ class Marker {
418
429
  return this._affectsData;
419
430
  }
420
431
 
432
+ /**
433
+ * Returns the marker data (properties defining the marker).
434
+ *
435
+ * @returns {module:engine/model/markercollection~MarkerData}
436
+ */
437
+ getData() {
438
+ return {
439
+ range: this.getRange(),
440
+ affectsData: this.affectsData,
441
+ managedUsingOperations: this.managedUsingOperations
442
+ };
443
+ }
444
+
421
445
  /**
422
446
  * Returns current marker start position.
423
447
  *
@@ -215,7 +215,7 @@ export default class Model {
215
215
  *
216
216
  * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
217
217
  * By default, a new batch with the default {@link module:engine/model/batch~Batch#constructor batch type} is created.
218
- * In the sample above, `change` and `enqueueChange` blocks will use a different batch (and a different
218
+ * In the sample above, the `change` and `enqueueChange` blocks will use a different batch (and a different
219
219
  * {@link module:engine/model/writer~Writer} instance since each of them operates on a separate batch).
220
220
  *
221
221
  * model.enqueueChange( { isUndoable: false }, writer => {
@@ -516,6 +516,7 @@ export default class Model {
516
516
  * @param {Object} [options]
517
517
  * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
518
518
  * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
519
+ * @param {Boolean} [options.treatEmojiAsSingleUnit=false] Whether multi-characer emoji sequences should be handled as single unit.
519
520
  */
520
521
  modifySelection( selection, options ) {
521
522
  modifySelection( this, selection, options );
@@ -10,7 +10,7 @@
10
10
  import Position from '../position';
11
11
  import TreeWalker from '../treewalker';
12
12
  import Range from '../range';
13
- import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
13
+ import { isInsideSurrogatePair, isInsideCombinedSymbol, isInsideEmojiSequence } from '@ckeditor/ckeditor5-utils/src/unicode';
14
14
  import DocumentSelection from '../documentselection';
15
15
 
16
16
  const wordBoundaryCharacters = ' ,.?!:;"-()';
@@ -49,11 +49,13 @@ const wordBoundaryCharacters = ' ,.?!:;"-()';
49
49
  * @param {Object} [options]
50
50
  * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
51
51
  * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
52
+ * @param {Boolean} [options.treatEmojiAsSingleUnit=false] Whether multi-characer emoji sequences should be handled as single unit.
52
53
  */
53
54
  export default function modifySelection( model, selection, options = {} ) {
54
55
  const schema = model.schema;
55
56
  const isForward = options.direction != 'backward';
56
57
  const unit = options.unit ? options.unit : 'character';
58
+ const treatEmojiAsSingleUnit = !!options.treatEmojiAsSingleUnit;
57
59
 
58
60
  const focus = selection.focus;
59
61
 
@@ -63,7 +65,7 @@ export default function modifySelection( model, selection, options = {} ) {
63
65
  direction: isForward ? 'forward' : 'backward'
64
66
  } );
65
67
 
66
- const data = { walker, schema, isForward, unit };
68
+ const data = { walker, schema, isForward, unit, treatEmojiAsSingleUnit };
67
69
 
68
70
  let next;
69
71
 
@@ -89,10 +91,10 @@ export default function modifySelection( model, selection, options = {} ) {
89
91
  }
90
92
 
91
93
  // Checks whether the selection can be extended to the the walker's next value (next position).
92
- // @param {{ walker, unit, isForward, schema }} data
94
+ // @param {{ walker, unit, isForward, schema, treatEmojiAsSingleUnit }} data
93
95
  // @param {module:engine/view/treewalker~TreeWalkerValue} value
94
96
  function tryExtendingTo( data, value ) {
95
- const { isForward, walker, unit, schema } = data;
97
+ const { isForward, walker, unit, schema, treatEmojiAsSingleUnit } = data;
96
98
  const { type, item, nextPosition } = value;
97
99
 
98
100
  // If found text, we can certainly put the focus in it. Let's just find a correct position
@@ -102,7 +104,7 @@ function tryExtendingTo( data, value ) {
102
104
  return getCorrectWordBreakPosition( walker, isForward );
103
105
  }
104
106
 
105
- return getCorrectPosition( walker, unit, isForward );
107
+ return getCorrectPosition( walker, unit, treatEmojiAsSingleUnit );
106
108
  }
107
109
 
108
110
  // Entering an element.
@@ -139,14 +141,19 @@ function tryExtendingTo( data, value ) {
139
141
  //
140
142
  // @param {module:engine/model/treewalker~TreeWalker} walker
141
143
  // @param {String} unit The unit by which selection should be modified.
142
- function getCorrectPosition( walker, unit ) {
144
+ // @param {Boolean} treatEmojiAsSingleUnit
145
+ function getCorrectPosition( walker, unit, treatEmojiAsSingleUnit ) {
143
146
  const textNode = walker.position.textNode;
144
147
 
145
148
  if ( textNode ) {
146
149
  const data = textNode.data;
147
150
  let offset = walker.position.offset - textNode.startOffset;
148
151
 
149
- while ( isInsideSurrogatePair( data, offset ) || ( unit == 'character' && isInsideCombinedSymbol( data, offset ) ) ) {
152
+ while (
153
+ isInsideSurrogatePair( data, offset ) ||
154
+ ( unit == 'character' && isInsideCombinedSymbol( data, offset ) ) ||
155
+ ( treatEmojiAsSingleUnit && isInsideEmojiSequence( data, offset ) )
156
+ ) {
150
157
  walker.next();
151
158
 
152
159
  offset = walker.position.offset - textNode.startOffset;
@@ -27,7 +27,7 @@ import DocumentSelection from './documentselection';
27
27
 
28
28
  import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
29
29
 
30
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
30
+ import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
31
31
 
32
32
  /**
33
33
  * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
@@ -968,30 +968,8 @@ export default class Writer {
968
968
  * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
969
969
  * name is created and returned.
970
970
  *
971
- * As the second parameter you can set the new marker data or leave this parameter as empty which will just refresh
972
- * the marker by triggering downcast conversion for it. Refreshing the marker is useful when you want to change
973
- * the marker {@link module:engine/view/element~Element view element} without changing any marker data.
974
- *
975
- * let isCommentActive = false;
976
- *
977
- * model.conversion.markerToHighlight( {
978
- * model: 'comment',
979
- * view: data => {
980
- * const classes = [ 'comment-marker' ];
981
- *
982
- * if ( isCommentActive ) {
983
- * classes.push( 'comment-marker--active' );
984
- * }
985
- *
986
- * return { classes };
987
- * }
988
- * } );
989
- *
990
- * // Change the property that indicates if marker is displayed as active or not.
991
- * isCommentActive = true;
992
- *
993
- * // And refresh the marker to convert it with additional class.
994
- * model.change( writer => writer.updateMarker( 'comment' ) );
971
+ * **Note**: If you want to change the {@link module:engine/view/element~Element view element} of the marker while its data in the model
972
+ * remains the same, use the dedicated {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker} method.
995
973
  *
996
974
  * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
997
975
  * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
@@ -1037,7 +1015,7 @@ export default class Writer {
1037
1015
 
1038
1016
  if ( !currentMarker ) {
1039
1017
  /**
1040
- * Marker with provided name does not exists.
1018
+ * Marker with provided name does not exist and will not be updated.
1041
1019
  *
1042
1020
  * @error writer-updatemarker-marker-not-exists
1043
1021
  */
@@ -1045,6 +1023,18 @@ export default class Writer {
1045
1023
  }
1046
1024
 
1047
1025
  if ( !options ) {
1026
+ /**
1027
+ * The usage of `writer.updateMarker()` only to reconvert (refresh) a
1028
+ * {@link module:engine/model/markercollection~Marker model marker} was deprecated and may not work in the future.
1029
+ * Please update your code to use
1030
+ * {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker `editor.editing.reconvertMarker()`}
1031
+ * instead.
1032
+ *
1033
+ * @error writer-updatemarker-reconvert-using-editingcontroller
1034
+ * @param {String} markerName The name of the updated marker.
1035
+ */
1036
+ logWarning( 'writer-updatemarker-reconvert-using-editingcontroller', { markerName } );
1037
+
1048
1038
  this.model.markers._refresh( currentMarker );
1049
1039
 
1050
1040
  return;
@@ -141,7 +141,8 @@ export default class Document {
141
141
  *
142
142
  * * adding or removing attribute from elements,
143
143
  * * changes inside of {@link module:engine/view/uielement~UIElement UI elements},
144
- * * {@link module:engine/model/differ~Differ#refreshItem marking some of the model elements to be re-converted}.
144
+ * * {@link module:engine/controller/editingcontroller~EditingController#reconvertItem marking some of the model elements to be
145
+ * re-converted}.
145
146
  *
146
147
  * Try to avoid changes which touch view structure:
147
148
  *