@ckeditor/ckeditor5-typing 41.4.1 → 42.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { escapeRegExp } from 'lodash-es';
11
11
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
12
12
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
13
13
  */ /**
14
+ * @module typing/utils/changebuffer
15
+ */ /**
14
16
  * Change buffer allows to group atomic changes (like characters that have been typed) into
15
17
  * {@link module:engine/model/batch~Batch batches}.
16
18
  *
@@ -30,9 +32,58 @@ import { escapeRegExp } from 'lodash-es';
30
32
  * ```
31
33
  */ class ChangeBuffer {
32
34
  /**
33
- * The current batch to which a feature should add its operations. Once the {@link #size}
34
- * is reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
35
- */ get batch() {
35
+ * The model instance.
36
+ */ model;
37
+ /**
38
+ * The maximum number of atomic changes which can be contained in one batch.
39
+ */ limit;
40
+ /**
41
+ * Whether the buffer is locked. A locked buffer cannot be reset unless it gets unlocked.
42
+ */ _isLocked;
43
+ /**
44
+ * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
45
+ * the {@link #batch batch} is set to a new one.
46
+ */ _size;
47
+ /**
48
+ * The current batch instance.
49
+ */ _batch = null;
50
+ /**
51
+ * The callback to document the change event which later needs to be removed.
52
+ */ _changeCallback;
53
+ /**
54
+ * The callback to document selection `change:attribute` and `change:range` events which resets the buffer.
55
+ */ _selectionChangeCallback;
56
+ /**
57
+ * Creates a new instance of the change buffer.
58
+ *
59
+ * @param limit The maximum number of atomic changes which can be contained in one batch.
60
+ */ constructor(model, limit = 20){
61
+ this.model = model;
62
+ this._size = 0;
63
+ this.limit = limit;
64
+ this._isLocked = false;
65
+ // The function to be called in order to notify the buffer about batches which appeared in the document.
66
+ // The callback will check whether it is a new batch and in that case the buffer will be flushed.
67
+ //
68
+ // The reason why the buffer needs to be flushed whenever a new batch appears is that the changes added afterwards
69
+ // should be added to a new batch. For instance, when the user types, then inserts an image, and then types again,
70
+ // the characters typed after inserting the image should be added to a different batch than the characters typed before.
71
+ this._changeCallback = (evt, batch)=>{
72
+ if (batch.isLocal && batch.isUndoable && batch !== this._batch) {
73
+ this._reset(true);
74
+ }
75
+ };
76
+ this._selectionChangeCallback = ()=>{
77
+ this._reset();
78
+ };
79
+ this.model.document.on('change', this._changeCallback);
80
+ this.model.document.selection.on('change:range', this._selectionChangeCallback);
81
+ this.model.document.selection.on('change:attribute', this._selectionChangeCallback);
82
+ }
83
+ /**
84
+ * The current batch to which a feature should add its operations. Once the {@link #size}
85
+ * is reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
86
+ */ get batch() {
36
87
  if (!this._batch) {
37
88
  this._batch = this.model.createBatch({
38
89
  isTyping: true
@@ -41,106 +92,92 @@ import { escapeRegExp } from 'lodash-es';
41
92
  return this._batch;
42
93
  }
43
94
  /**
44
- * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
45
- * the {@link #batch batch} is set to a new one.
46
- */ get size() {
95
+ * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
96
+ * the {@link #batch batch} is set to a new one.
97
+ */ get size() {
47
98
  return this._size;
48
99
  }
49
100
  /**
50
- * The input number of changes into the buffer. Once the {@link #size} is
51
- * reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
52
- *
53
- * @param changeCount The number of atomic changes to input.
54
- */ input(changeCount) {
101
+ * The input number of changes into the buffer. Once the {@link #size} is
102
+ * reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
103
+ *
104
+ * @param changeCount The number of atomic changes to input.
105
+ */ input(changeCount) {
55
106
  this._size += changeCount;
56
107
  if (this._size >= this.limit) {
57
108
  this._reset(true);
58
109
  }
59
110
  }
60
111
  /**
61
- * Whether the buffer is locked. A locked buffer cannot be reset unless it gets unlocked.
62
- */ get isLocked() {
112
+ * Whether the buffer is locked. A locked buffer cannot be reset unless it gets unlocked.
113
+ */ get isLocked() {
63
114
  return this._isLocked;
64
115
  }
65
116
  /**
66
- * Locks the buffer.
67
- */ lock() {
117
+ * Locks the buffer.
118
+ */ lock() {
68
119
  this._isLocked = true;
69
120
  }
70
121
  /**
71
- * Unlocks the buffer.
72
- */ unlock() {
122
+ * Unlocks the buffer.
123
+ */ unlock() {
73
124
  this._isLocked = false;
74
125
  }
75
126
  /**
76
- * Destroys the buffer.
77
- */ destroy() {
127
+ * Destroys the buffer.
128
+ */ destroy() {
78
129
  this.model.document.off('change', this._changeCallback);
79
130
  this.model.document.selection.off('change:range', this._selectionChangeCallback);
80
131
  this.model.document.selection.off('change:attribute', this._selectionChangeCallback);
81
132
  }
82
133
  /**
83
- * Resets the change buffer.
84
- *
85
- * @param ignoreLock Whether internal lock {@link #isLocked} should be ignored.
86
- */ _reset(ignoreLock = false) {
134
+ * Resets the change buffer.
135
+ *
136
+ * @param ignoreLock Whether internal lock {@link #isLocked} should be ignored.
137
+ */ _reset(ignoreLock = false) {
87
138
  if (!this.isLocked || ignoreLock) {
88
139
  this._batch = null;
89
140
  this._size = 0;
90
141
  }
91
142
  }
92
- /**
93
- * Creates a new instance of the change buffer.
94
- *
95
- * @param limit The maximum number of atomic changes which can be contained in one batch.
96
- */ constructor(model, limit = 20){
97
- /**
98
- * The current batch instance.
99
- */ this._batch = null;
100
- this.model = model;
101
- this._size = 0;
102
- this.limit = limit;
103
- this._isLocked = false;
104
- // The function to be called in order to notify the buffer about batches which appeared in the document.
105
- // The callback will check whether it is a new batch and in that case the buffer will be flushed.
106
- //
107
- // The reason why the buffer needs to be flushed whenever a new batch appears is that the changes added afterwards
108
- // should be added to a new batch. For instance, when the user types, then inserts an image, and then types again,
109
- // the characters typed after inserting the image should be added to a different batch than the characters typed before.
110
- this._changeCallback = (evt, batch)=>{
111
- if (batch.isLocal && batch.isUndoable && batch !== this._batch) {
112
- this._reset(true);
113
- }
114
- };
115
- this._selectionChangeCallback = ()=>{
116
- this._reset();
117
- };
118
- this.model.document.on('change', this._changeCallback);
119
- this.model.document.selection.on('change:range', this._selectionChangeCallback);
120
- this.model.document.selection.on('change:attribute', this._selectionChangeCallback);
121
- }
122
143
  }
123
144
 
124
- class InsertTextCommand extends Command {
145
+ /**
146
+ * The insert text command. Used by the {@link module:typing/input~Input input feature} to handle typing.
147
+ */ class InsertTextCommand extends Command {
148
+ /**
149
+ * Typing's change buffer used to group subsequent changes into batches.
150
+ */ _buffer;
151
+ /**
152
+ * Creates an instance of the command.
153
+ *
154
+ * @param undoStepSize The maximum number of atomic changes
155
+ * which can be contained in one batch in the command buffer.
156
+ */ constructor(editor, undoStepSize){
157
+ super(editor);
158
+ this._buffer = new ChangeBuffer(editor.model, undoStepSize);
159
+ // Since this command may execute on different selectable than selection, it should be checked directly in execute block.
160
+ this._isEnabledBasedOnSelection = false;
161
+ }
125
162
  /**
126
- * The current change buffer.
127
- */ get buffer() {
163
+ * The current change buffer.
164
+ */ get buffer() {
128
165
  return this._buffer;
129
166
  }
130
167
  /**
131
- * @inheritDoc
132
- */ destroy() {
168
+ * @inheritDoc
169
+ */ destroy() {
133
170
  super.destroy();
134
171
  this._buffer.destroy();
135
172
  }
136
173
  /**
137
- * Executes the input command. It replaces the content within the given range with the given text.
138
- * Replacing is a two step process, first the content within the range is removed and then the new text is inserted
139
- * at the beginning of the range (which after the removal is a collapsed range).
140
- *
141
- * @fires execute
142
- * @param options The command options.
143
- */ execute(options = {}) {
174
+ * Executes the input command. It replaces the content within the given range with the given text.
175
+ * Replacing is a two step process, first the content within the range is removed and then the new text is inserted
176
+ * at the beginning of the range (which after the removal is a collapsed range).
177
+ *
178
+ * @fires execute
179
+ * @param options The command options.
180
+ */ execute(options = {}) {
144
181
  const model = this.editor.model;
145
182
  const doc = model.document;
146
183
  const text = options.text || '';
@@ -174,17 +211,6 @@ class InsertTextCommand extends Command {
174
211
  this._buffer.input(textInsertions);
175
212
  });
176
213
  }
177
- /**
178
- * Creates an instance of the command.
179
- *
180
- * @param undoStepSize The maximum number of atomic changes
181
- * which can be contained in one batch in the command buffer.
182
- */ constructor(editor, undoStepSize){
183
- super(editor);
184
- this._buffer = new ChangeBuffer(editor.model, undoStepSize);
185
- // Since this command may execute on different selectable than selection, it should be checked directly in execute block.
186
- this._isEnabledBasedOnSelection = false;
187
- }
188
214
  }
189
215
 
190
216
  const TYPING_INPUT_TYPES = [
@@ -199,16 +225,16 @@ const TYPING_INPUT_TYPES = [
199
225
  // This one is used by Safari when accepting spell check suggestions from the autocorrection pop-up (Mac).
200
226
  'insertReplacementText'
201
227
  ];
202
- class InsertTextObserver extends Observer {
203
- /**
204
- * @inheritDoc
205
- */ observe() {}
228
+ /**
229
+ * Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
230
+ */ class InsertTextObserver extends Observer {
206
231
  /**
207
- * @inheritDoc
208
- */ stopObserving() {}
232
+ * Instance of the focus observer. Insert text observer calls
233
+ * {@link module:engine/view/observer/focusobserver~FocusObserver#flush} to mark the latest focus change as complete.
234
+ */ focusObserver;
209
235
  /**
210
- * @inheritDoc
211
- */ constructor(view){
236
+ * @inheritDoc
237
+ */ constructor(view){
212
238
  super(view);
213
239
  this.focusObserver = view.getObserver(FocusObserver);
214
240
  // On Android composition events should immediately be applied to the model. Rendering is not disabled.
@@ -281,17 +307,25 @@ class InsertTextObserver extends Observer {
281
307
  priority: 'lowest'
282
308
  });
283
309
  }
310
+ /**
311
+ * @inheritDoc
312
+ */ observe() {}
313
+ /**
314
+ * @inheritDoc
315
+ */ stopObserving() {}
284
316
  }
285
317
 
286
- class Input extends Plugin {
318
+ /**
319
+ * Handles text input coming from the keyboard or other input methods.
320
+ */ class Input extends Plugin {
287
321
  /**
288
- * @inheritDoc
289
- */ static get pluginName() {
322
+ * @inheritDoc
323
+ */ static get pluginName() {
290
324
  return 'Input';
291
325
  }
292
326
  /**
293
- * @inheritDoc
294
- */ init() {
327
+ * @inheritDoc
328
+ */ init() {
295
329
  const editor = this.editor;
296
330
  const model = editor.model;
297
331
  const view = editor.editing.view;
@@ -407,23 +441,45 @@ function deleteSelectionContent(model, insertTextCommand) {
407
441
  buffer.unlock();
408
442
  }
409
443
 
410
- class DeleteCommand extends Command {
444
+ /**
445
+ * The delete command. Used by the {@link module:typing/delete~Delete delete feature} to handle the <kbd>Delete</kbd> and
446
+ * <kbd>Backspace</kbd> keys.
447
+ */ class DeleteCommand extends Command {
411
448
  /**
412
- * The current change buffer.
413
- */ get buffer() {
449
+ * The directionality of the delete describing in what direction it should
450
+ * consume the content when the selection is collapsed.
451
+ */ direction;
452
+ /**
453
+ * Delete's change buffer used to group subsequent changes into batches.
454
+ */ _buffer;
455
+ /**
456
+ * Creates an instance of the command.
457
+ *
458
+ * @param direction The directionality of the delete describing in what direction it
459
+ * should consume the content when the selection is collapsed.
460
+ */ constructor(editor, direction){
461
+ super(editor);
462
+ this.direction = direction;
463
+ this._buffer = new ChangeBuffer(editor.model, editor.config.get('typing.undoStep'));
464
+ // Since this command may execute on different selectable than selection, it should be checked directly in execute block.
465
+ this._isEnabledBasedOnSelection = false;
466
+ }
467
+ /**
468
+ * The current change buffer.
469
+ */ get buffer() {
414
470
  return this._buffer;
415
471
  }
416
472
  /**
417
- * Executes the delete command. Depending on whether the selection is collapsed or not, deletes its content
418
- * or a piece of content in the {@link #direction defined direction}.
419
- *
420
- * @fires execute
421
- * @param options The command options.
422
- * @param options.unit See {@link module:engine/model/utils/modifyselection~modifySelection}'s options.
423
- * @param options.sequence A number describing which subsequent delete event it is without the key being released.
424
- * See the {@link module:engine/view/document~Document#event:delete} event data.
425
- * @param options.selection Selection to remove. If not set, current model selection will be used.
426
- */ execute(options = {}) {
473
+ * Executes the delete command. Depending on whether the selection is collapsed or not, deletes its content
474
+ * or a piece of content in the {@link #direction defined direction}.
475
+ *
476
+ * @fires execute
477
+ * @param options The command options.
478
+ * @param options.unit See {@link module:engine/model/utils/modifyselection~modifySelection}'s options.
479
+ * @param options.sequence A number describing which subsequent delete event it is without the key being released.
480
+ * See the {@link module:engine/view/document~Document#event:delete} event data.
481
+ * @param options.selection Selection to remove. If not set, current model selection will be used.
482
+ */ execute(options = {}) {
427
483
  const model = this.editor.model;
428
484
  const doc = model.document;
429
485
  model.enqueueChange(this._buffer.batch, (writer)=>{
@@ -489,21 +545,21 @@ class DeleteCommand extends Command {
489
545
  });
490
546
  }
491
547
  /**
492
- * If the user keeps <kbd>Backspace</kbd> or <kbd>Delete</kbd> key pressed, the content of the current
493
- * editable will be cleared. However, this will not yet lead to resetting the remaining block to a paragraph
494
- * (which happens e.g. when the user does <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Backspace</kbd>).
495
- *
496
- * But, if the user pressed the key in an empty editable for the first time,
497
- * we want to replace the entire content with a paragraph if:
498
- *
499
- * * the current limit element is empty,
500
- * * the paragraph is allowed in the limit element,
501
- * * the limit doesn't already have a paragraph inside.
502
- *
503
- * See https://github.com/ckeditor/ckeditor5-typing/issues/61.
504
- *
505
- * @param sequence A number describing which subsequent delete event it is without the key being released.
506
- */ _shouldEntireContentBeReplacedWithParagraph(sequence) {
548
+ * If the user keeps <kbd>Backspace</kbd> or <kbd>Delete</kbd> key pressed, the content of the current
549
+ * editable will be cleared. However, this will not yet lead to resetting the remaining block to a paragraph
550
+ * (which happens e.g. when the user does <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Backspace</kbd>).
551
+ *
552
+ * But, if the user pressed the key in an empty editable for the first time,
553
+ * we want to replace the entire content with a paragraph if:
554
+ *
555
+ * * the current limit element is empty,
556
+ * * the paragraph is allowed in the limit element,
557
+ * * the limit doesn't already have a paragraph inside.
558
+ *
559
+ * See https://github.com/ckeditor/ckeditor5-typing/issues/61.
560
+ *
561
+ * @param sequence A number describing which subsequent delete event it is without the key being released.
562
+ */ _shouldEntireContentBeReplacedWithParagraph(sequence) {
507
563
  // Does nothing if user pressed and held the "Backspace" or "Delete" key.
508
564
  if (sequence > 1) {
509
565
  return false;
@@ -531,10 +587,10 @@ class DeleteCommand extends Command {
531
587
  return true;
532
588
  }
533
589
  /**
534
- * The entire content is replaced with the paragraph. Selection is moved inside the paragraph.
535
- *
536
- * @param writer The model writer.
537
- */ _replaceEntireContentWithParagraph(writer) {
590
+ * The entire content is replaced with the paragraph. Selection is moved inside the paragraph.
591
+ *
592
+ * @param writer The model writer.
593
+ */ _replaceEntireContentWithParagraph(writer) {
538
594
  const model = this.editor.model;
539
595
  const doc = model.document;
540
596
  const selection = doc.selection;
@@ -545,12 +601,12 @@ class DeleteCommand extends Command {
545
601
  writer.setSelection(paragraph, 0);
546
602
  }
547
603
  /**
548
- * Checks if the selection is inside an empty element that is the first child of the limit element
549
- * and should be replaced with a paragraph.
550
- *
551
- * @param selection The selection.
552
- * @param sequence A number describing which subsequent delete event it is without the key being released.
553
- */ _shouldReplaceFirstBlockWithParagraph(selection, sequence) {
604
+ * Checks if the selection is inside an empty element that is the first child of the limit element
605
+ * and should be replaced with a paragraph.
606
+ *
607
+ * @param selection The selection.
608
+ * @param sequence A number describing which subsequent delete event it is without the key being released.
609
+ */ _shouldReplaceFirstBlockWithParagraph(selection, sequence) {
554
610
  const model = this.editor.model;
555
611
  // Does nothing if user pressed and held the "Backspace" key or it was a "Delete" button.
556
612
  if (sequence > 1 || this.direction != 'backward') {
@@ -581,18 +637,6 @@ class DeleteCommand extends Command {
581
637
  }
582
638
  return true;
583
639
  }
584
- /**
585
- * Creates an instance of the command.
586
- *
587
- * @param direction The directionality of the delete describing in what direction it
588
- * should consume the content when the selection is collapsed.
589
- */ constructor(editor, direction){
590
- super(editor);
591
- this.direction = direction;
592
- this._buffer = new ChangeBuffer(editor.model, editor.config.get('typing.undoStep'));
593
- // Since this command may execute on different selectable than selection, it should be checked directly in execute block.
594
- this._isEnabledBasedOnSelection = false;
595
- }
596
640
  }
597
641
 
598
642
  const DELETE_CHARACTER = 'character';
@@ -680,16 +724,12 @@ const DELETE_EVENT_TYPES = {
680
724
  direction: DELETE_FORWARD
681
725
  }
682
726
  };
683
- class DeleteObserver extends Observer {
684
- /**
685
- * @inheritDoc
686
- */ observe() {}
687
- /**
688
- * @inheritDoc
689
- */ stopObserving() {}
727
+ /**
728
+ * Delete observer introduces the {@link module:engine/view/document~Document#event:delete} event.
729
+ */ class DeleteObserver extends Observer {
690
730
  /**
691
- * @inheritDoc
692
- */ constructor(view){
731
+ * @inheritDoc
732
+ */ constructor(view){
693
733
  super(view);
694
734
  const document = view.document;
695
735
  // It matters how many subsequent deletions were made, e.g. when the backspace key was pressed and held
@@ -748,6 +788,12 @@ class DeleteObserver extends Observer {
748
788
  enableChromeWorkaround(this);
749
789
  }
750
790
  }
791
+ /**
792
+ * @inheritDoc
793
+ */ observe() {}
794
+ /**
795
+ * @inheritDoc
796
+ */ stopObserving() {}
751
797
  }
752
798
  /**
753
799
  * Enables workaround for the issue https://github.com/ckeditor/ckeditor5/issues/11904.
@@ -832,15 +878,21 @@ class DeleteObserver extends Observer {
832
878
  return false;
833
879
  }
834
880
 
835
- class Delete extends Plugin {
881
+ /**
882
+ * The delete and backspace feature. Handles keys such as <kbd>Delete</kbd> and <kbd>Backspace</kbd>, other
883
+ * keystrokes and user actions that result in deleting content in the editor.
884
+ */ class Delete extends Plugin {
885
+ /**
886
+ * Whether pressing backspace should trigger undo action
887
+ */ _undoOnBackspace;
836
888
  /**
837
- * @inheritDoc
838
- */ static get pluginName() {
889
+ * @inheritDoc
890
+ */ static get pluginName() {
839
891
  return 'Delete';
840
892
  }
841
893
  /**
842
- * @inheritDoc
843
- */ init() {
894
+ * @inheritDoc
895
+ */ init() {
844
896
  const editor = this.editor;
845
897
  const view = editor.editing.view;
846
898
  const viewDocument = view.document;
@@ -893,17 +945,22 @@ class Delete extends Plugin {
893
945
  }
894
946
  }
895
947
  /**
896
- * If the next user action after calling this method is pressing backspace, it would undo the last change.
897
- *
898
- * Requires {@link module:undo/undoediting~UndoEditing} plugin. If not loaded, does nothing.
899
- */ requestUndoOnBackspace() {
948
+ * If the next user action after calling this method is pressing backspace, it would undo the last change.
949
+ *
950
+ * Requires {@link module:undo/undoediting~UndoEditing} plugin. If not loaded, does nothing.
951
+ */ requestUndoOnBackspace() {
900
952
  if (this.editor.plugins.has('UndoEditing')) {
901
953
  this._undoOnBackspace = true;
902
954
  }
903
955
  }
904
956
  }
905
957
 
906
- class Typing extends Plugin {
958
+ /**
959
+ * The typing feature. It handles typing.
960
+ *
961
+ * This is a "glue" plugin which loads the {@link module:typing/input~Input} and {@link module:typing/delete~Delete}
962
+ * plugins.
963
+ */ class Typing extends Plugin {
907
964
  static get requires() {
908
965
  return [
909
966
  Input,
@@ -911,8 +968,8 @@ class Typing extends Plugin {
911
968
  ];
912
969
  }
913
970
  /**
914
- * @inheritDoc
915
- */ static get pluginName() {
971
+ * @inheritDoc
972
+ */ static get pluginName() {
916
973
  return 'Typing';
917
974
  }
918
975
  }
@@ -921,6 +978,8 @@ class Typing extends Plugin {
921
978
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
922
979
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
923
980
  */ /**
981
+ * @module typing/utils/getlasttextline
982
+ */ /**
924
983
  * Returns the last text line from the given range.
925
984
  *
926
985
  * "The last text line" is understood as text (from one or more text nodes) which is limited either by a parent block
@@ -964,15 +1023,57 @@ class Typing extends Plugin {
964
1023
  };
965
1024
  }
966
1025
 
967
- class TextWatcher extends ObservableMixin() {
1026
+ /**
1027
+ * The text watcher feature.
1028
+ *
1029
+ * Fires the {@link module:typing/textwatcher~TextWatcher#event:matched:data `matched:data`},
1030
+ * {@link module:typing/textwatcher~TextWatcher#event:matched:selection `matched:selection`} and
1031
+ * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
1032
+ */ class TextWatcher extends /* #__PURE__ */ ObservableMixin() {
1033
+ /**
1034
+ * The editor's model.
1035
+ */ model;
1036
+ /**
1037
+ * The function used to match the text.
1038
+ *
1039
+ * The test callback can return 3 values:
1040
+ *
1041
+ * * `false` if there is no match,
1042
+ * * `true` if there is a match,
1043
+ * * an object if there is a match and we want to pass some additional information to the {@link #event:matched:data} event.
1044
+ */ testCallback;
1045
+ /**
1046
+ * Whether there is a match currently.
1047
+ */ _hasMatch;
1048
+ /**
1049
+ * Creates a text watcher instance.
1050
+ *
1051
+ * @param testCallback See {@link module:typing/textwatcher~TextWatcher#testCallback}.
1052
+ */ constructor(model, testCallback){
1053
+ super();
1054
+ this.model = model;
1055
+ this.testCallback = testCallback;
1056
+ this._hasMatch = false;
1057
+ this.set('isEnabled', true);
1058
+ // Toggle text watching on isEnabled state change.
1059
+ this.on('change:isEnabled', ()=>{
1060
+ if (this.isEnabled) {
1061
+ this._startListening();
1062
+ } else {
1063
+ this.stopListening(model.document.selection);
1064
+ this.stopListening(model.document);
1065
+ }
1066
+ });
1067
+ this._startListening();
1068
+ }
968
1069
  /**
969
- * Flag indicating whether there is a match currently.
970
- */ get hasMatch() {
1070
+ * Flag indicating whether there is a match currently.
1071
+ */ get hasMatch() {
971
1072
  return this._hasMatch;
972
1073
  }
973
1074
  /**
974
- * Starts listening to the editor for typing and selection events.
975
- */ _startListening() {
1075
+ * Starts listening to the editor for typing and selection events.
1076
+ */ _startListening() {
976
1077
  const model = this.model;
977
1078
  const document = model.document;
978
1079
  this.listenTo(document.selection, 'change:range', (evt, { directChange })=>{
@@ -1000,15 +1101,15 @@ class TextWatcher extends ObservableMixin() {
1000
1101
  });
1001
1102
  }
1002
1103
  /**
1003
- * Checks the editor content for matched text.
1004
- *
1005
- * @fires matched:data
1006
- * @fires matched:selection
1007
- * @fires unmatched
1008
- *
1009
- * @param suffix A suffix used for generating the event name.
1010
- * @param data Data object for event.
1011
- */ _evaluateTextBeforeSelection(suffix, data = {}) {
1104
+ * Checks the editor content for matched text.
1105
+ *
1106
+ * @fires matched:data
1107
+ * @fires matched:selection
1108
+ * @fires unmatched
1109
+ *
1110
+ * @param suffix A suffix used for generating the event name.
1111
+ * @param data Data object for event.
1112
+ */ _evaluateTextBeforeSelection(suffix, data = {}) {
1012
1113
  const model = this.model;
1013
1114
  const document = model.document;
1014
1115
  const selection = document.selection;
@@ -1031,38 +1132,158 @@ class TextWatcher extends ObservableMixin() {
1031
1132
  this.fire(`matched:${suffix}`, eventData);
1032
1133
  }
1033
1134
  }
1034
- /**
1035
- * Creates a text watcher instance.
1036
- *
1037
- * @param testCallback See {@link module:typing/textwatcher~TextWatcher#testCallback}.
1038
- */ constructor(model, testCallback){
1039
- super();
1040
- this.model = model;
1041
- this.testCallback = testCallback;
1042
- this._hasMatch = false;
1043
- this.set('isEnabled', true);
1044
- // Toggle text watching on isEnabled state change.
1045
- this.on('change:isEnabled', ()=>{
1046
- if (this.isEnabled) {
1047
- this._startListening();
1048
- } else {
1049
- this.stopListening(model.document.selection);
1050
- this.stopListening(model.document);
1051
- }
1052
- });
1053
- this._startListening();
1054
- }
1055
1135
  }
1056
1136
 
1057
- class TwoStepCaretMovement extends Plugin {
1137
+ /**
1138
+ * This plugin enables the two-step caret (phantom) movement behavior for
1139
+ * {@link module:typing/twostepcaretmovement~TwoStepCaretMovement#registerAttribute registered attributes}
1140
+ * on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
1141
+ *
1142
+ * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
1143
+ * beginning/end of an attribute.
1144
+ *
1145
+ * **Note:** This plugin support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
1146
+ * but for the sake of simplicity examples showcase only left–to–right use–cases.
1147
+ *
1148
+ * # Forward movement
1149
+ *
1150
+ * ## "Entering" an attribute:
1151
+ *
1152
+ * When this plugin is enabled and registered for the `a` attribute and the selection is right before it
1153
+ * (at the attribute boundary), pressing the right arrow key will not move the selection but update its
1154
+ * attributes accordingly:
1155
+ *
1156
+ * * When enabled:
1157
+ *
1158
+ * ```xml
1159
+ * foo{}<$text a="true">bar</$text>
1160
+ * ```
1161
+ *
1162
+ * <kbd>→</kbd>
1163
+ *
1164
+ * ```xml
1165
+ * foo<$text a="true">{}bar</$text>
1166
+ * ```
1167
+ *
1168
+ * * When disabled:
1169
+ *
1170
+ * ```xml
1171
+ * foo{}<$text a="true">bar</$text>
1172
+ * ```
1173
+ *
1174
+ * <kbd>→</kbd>
1175
+ *
1176
+ * ```xml
1177
+ * foo<$text a="true">b{}ar</$text>
1178
+ * ```
1179
+ *
1180
+ *
1181
+ * ## "Leaving" an attribute:
1182
+ *
1183
+ * * When enabled:
1184
+ *
1185
+ * ```xml
1186
+ * <$text a="true">bar{}</$text>baz
1187
+ * ```
1188
+ *
1189
+ * <kbd>→</kbd>
1190
+ *
1191
+ * ```xml
1192
+ * <$text a="true">bar</$text>{}baz
1193
+ * ```
1194
+ *
1195
+ * * When disabled:
1196
+ *
1197
+ * ```xml
1198
+ * <$text a="true">bar{}</$text>baz
1199
+ * ```
1200
+ *
1201
+ * <kbd>→</kbd>
1202
+ *
1203
+ * ```xml
1204
+ * <$text a="true">bar</$text>b{}az
1205
+ * ```
1206
+ *
1207
+ * # Backward movement
1208
+ *
1209
+ * * When enabled:
1210
+ *
1211
+ * ```xml
1212
+ * <$text a="true">bar</$text>{}baz
1213
+ * ```
1214
+ *
1215
+ * <kbd>←</kbd>
1216
+ *
1217
+ * ```xml
1218
+ * <$text a="true">bar{}</$text>baz
1219
+ * ```
1220
+ *
1221
+ * * When disabled:
1222
+ *
1223
+ * ```xml
1224
+ * <$text a="true">bar</$text>{}baz
1225
+ * ```
1226
+ *
1227
+ * <kbd>←</kbd>
1228
+ *
1229
+ * ```xml
1230
+ * <$text a="true">ba{}r</$text>b{}az
1231
+ * ```
1232
+ *
1233
+ * # Multiple attributes
1234
+ *
1235
+ * * When enabled and many attributes starts or ends at the same position:
1236
+ *
1237
+ * ```xml
1238
+ * <$text a="true" b="true">bar</$text>{}baz
1239
+ * ```
1240
+ *
1241
+ * <kbd>←</kbd>
1242
+ *
1243
+ * ```xml
1244
+ * <$text a="true" b="true">bar{}</$text>baz
1245
+ * ```
1246
+ *
1247
+ * * When enabled and one procedes another:
1248
+ *
1249
+ * ```xml
1250
+ * <$text a="true">bar</$text><$text b="true">{}bar</$text>
1251
+ * ```
1252
+ *
1253
+ * <kbd>←</kbd>
1254
+ *
1255
+ * ```xml
1256
+ * <$text a="true">bar{}</$text><$text b="true">bar</$text>
1257
+ * ```
1258
+ *
1259
+ */ class TwoStepCaretMovement extends Plugin {
1260
+ /**
1261
+ * A set of attributes to handle.
1262
+ */ attributes;
1058
1263
  /**
1059
- * @inheritDoc
1060
- */ static get pluginName() {
1264
+ * The current UID of the overridden gravity, as returned by
1265
+ * {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
1266
+ */ _overrideUid;
1267
+ /**
1268
+ * A flag indicating that the automatic gravity restoration should not happen upon the next
1269
+ * gravity restoration.
1270
+ * {@link module:engine/model/selection~Selection#event:change:range} event.
1271
+ */ _isNextGravityRestorationSkipped = false;
1272
+ /**
1273
+ * @inheritDoc
1274
+ */ static get pluginName() {
1061
1275
  return 'TwoStepCaretMovement';
1062
1276
  }
1063
1277
  /**
1064
- * @inheritDoc
1065
- */ init() {
1278
+ * @inheritDoc
1279
+ */ constructor(editor){
1280
+ super(editor);
1281
+ this.attributes = new Set();
1282
+ this._overrideUid = null;
1283
+ }
1284
+ /**
1285
+ * @inheritDoc
1286
+ */ init() {
1066
1287
  const editor = this.editor;
1067
1288
  const model = editor.model;
1068
1289
  const view = editor.editing.view;
@@ -1131,19 +1352,19 @@ class TwoStepCaretMovement extends Plugin {
1131
1352
  this._handleDeleteContentAfterNode();
1132
1353
  }
1133
1354
  /**
1134
- * Registers a given attribute for the two-step caret movement.
1135
- *
1136
- * @param attribute Name of the attribute to handle.
1137
- */ registerAttribute(attribute) {
1355
+ * Registers a given attribute for the two-step caret movement.
1356
+ *
1357
+ * @param attribute Name of the attribute to handle.
1358
+ */ registerAttribute(attribute) {
1138
1359
  this.attributes.add(attribute);
1139
1360
  }
1140
1361
  /**
1141
- * Updates the document selection and the view according to the two–step caret movement state
1142
- * when moving **forwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
1143
- *
1144
- * @param data Data of the key press.
1145
- * @returns `true` when the handler prevented caret movement.
1146
- */ _handleForwardMovement(data) {
1362
+ * Updates the document selection and the view according to the two–step caret movement state
1363
+ * when moving **forwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
1364
+ *
1365
+ * @param data Data of the key press.
1366
+ * @returns `true` when the handler prevented caret movement.
1367
+ */ _handleForwardMovement(data) {
1147
1368
  const attributes = this.attributes;
1148
1369
  const model = this.editor.model;
1149
1370
  const selection = model.document.selection;
@@ -1194,12 +1415,12 @@ class TwoStepCaretMovement extends Plugin {
1194
1415
  return false;
1195
1416
  }
1196
1417
  /**
1197
- * Updates the document selection and the view according to the two–step caret movement state
1198
- * when moving **backwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
1199
- *
1200
- * @param data Data of the key press.
1201
- * @returns `true` when the handler prevented caret movement
1202
- */ _handleBackwardMovement(data) {
1418
+ * Updates the document selection and the view according to the two–step caret movement state
1419
+ * when moving **backwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
1420
+ *
1421
+ * @param data Data of the key press.
1422
+ * @returns `true` when the handler prevented caret movement
1423
+ */ _handleBackwardMovement(data) {
1203
1424
  const attributes = this.attributes;
1204
1425
  const model = this.editor.model;
1205
1426
  const selection = model.document.selection;
@@ -1283,14 +1504,14 @@ class TwoStepCaretMovement extends Plugin {
1283
1504
  return false;
1284
1505
  }
1285
1506
  /**
1286
- * Starts listening to {@link module:engine/view/document~Document#event:mousedown} and
1287
- * {@link module:engine/view/document~Document#event:selectionChange} and puts the selection before/after a 2-step node
1288
- * if clicked at the beginning/ending of the 2-step node.
1289
- *
1290
- * The purpose of this action is to allow typing around the 2-step node directly after a click.
1291
- *
1292
- * See https://github.com/ckeditor/ckeditor5/issues/1016.
1293
- */ _enableClickingAfterNode() {
1507
+ * Starts listening to {@link module:engine/view/document~Document#event:mousedown} and
1508
+ * {@link module:engine/view/document~Document#event:selectionChange} and puts the selection before/after a 2-step node
1509
+ * if clicked at the beginning/ending of the 2-step node.
1510
+ *
1511
+ * The purpose of this action is to allow typing around the 2-step node directly after a click.
1512
+ *
1513
+ * See https://github.com/ckeditor/ckeditor5/issues/1016.
1514
+ */ _enableClickingAfterNode() {
1294
1515
  const editor = this.editor;
1295
1516
  const model = editor.model;
1296
1517
  const selection = model.document.selection;
@@ -1333,14 +1554,14 @@ class TwoStepCaretMovement extends Plugin {
1333
1554
  });
1334
1555
  }
1335
1556
  /**
1336
- * Starts listening to {@link module:engine/model/model~Model#event:insertContent} and corrects the model
1337
- * selection attributes if the selection is at the end of a two-step node after inserting the content.
1338
- *
1339
- * The purpose of this action is to improve the overall UX because the user is no longer "trapped" by the
1340
- * two-step attribute of the selection, and they can type a "clean" (`linkHref`–less) text right away.
1341
- *
1342
- * See https://github.com/ckeditor/ckeditor5/issues/6053.
1343
- */ _enableInsertContentSelectionAttributesFixer() {
1557
+ * Starts listening to {@link module:engine/model/model~Model#event:insertContent} and corrects the model
1558
+ * selection attributes if the selection is at the end of a two-step node after inserting the content.
1559
+ *
1560
+ * The purpose of this action is to improve the overall UX because the user is no longer "trapped" by the
1561
+ * two-step attribute of the selection, and they can type a "clean" (`linkHref`–less) text right away.
1562
+ *
1563
+ * See https://github.com/ckeditor/ckeditor5/issues/6053.
1564
+ */ _enableInsertContentSelectionAttributesFixer() {
1344
1565
  const editor = this.editor;
1345
1566
  const model = editor.model;
1346
1567
  const selection = model.document.selection;
@@ -1355,17 +1576,17 @@ class TwoStepCaretMovement extends Plugin {
1355
1576
  });
1356
1577
  }
1357
1578
  /**
1358
- * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
1359
- * removing a content right after the tow-step attribute.
1360
- *
1361
- * If so, the selection should not preserve the two-step attribute. However, if
1362
- * the {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is active and
1363
- * the selection has the two-step attribute due to overridden gravity (at the end), the two-step attribute should stay untouched.
1364
- *
1365
- * The purpose of this action is to allow removing the link text and keep the selection outside the link.
1366
- *
1367
- * See https://github.com/ckeditor/ckeditor5/issues/7521.
1368
- */ _handleDeleteContentAfterNode() {
1579
+ * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
1580
+ * removing a content right after the tow-step attribute.
1581
+ *
1582
+ * If so, the selection should not preserve the two-step attribute. However, if
1583
+ * the {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is active and
1584
+ * the selection has the two-step attribute due to overridden gravity (at the end), the two-step attribute should stay untouched.
1585
+ *
1586
+ * The purpose of this action is to allow removing the link text and keep the selection outside the link.
1587
+ *
1588
+ * See https://github.com/ckeditor/ckeditor5/issues/7521.
1589
+ */ _handleDeleteContentAfterNode() {
1369
1590
  const editor = this.editor;
1370
1591
  const model = editor.model;
1371
1592
  const selection = model.document.selection;
@@ -1415,42 +1636,30 @@ class TwoStepCaretMovement extends Plugin {
1415
1636
  });
1416
1637
  }
1417
1638
  /**
1418
- * `true` when the gravity is overridden for the plugin.
1419
- */ get _isGravityOverridden() {
1639
+ * `true` when the gravity is overridden for the plugin.
1640
+ */ get _isGravityOverridden() {
1420
1641
  return !!this._overrideUid;
1421
1642
  }
1422
1643
  /**
1423
- * Overrides the gravity using the {@link module:engine/model/writer~Writer model writer}
1424
- * and stores the information about this fact in the {@link #_overrideUid}.
1425
- *
1426
- * A shorthand for {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
1427
- */ _overrideGravity() {
1644
+ * Overrides the gravity using the {@link module:engine/model/writer~Writer model writer}
1645
+ * and stores the information about this fact in the {@link #_overrideUid}.
1646
+ *
1647
+ * A shorthand for {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
1648
+ */ _overrideGravity() {
1428
1649
  this._overrideUid = this.editor.model.change((writer)=>{
1429
1650
  return writer.overrideSelectionGravity();
1430
1651
  });
1431
1652
  }
1432
1653
  /**
1433
- * Restores the gravity using the {@link module:engine/model/writer~Writer model writer}.
1434
- *
1435
- * A shorthand for {@link module:engine/model/writer~Writer#restoreSelectionGravity}.
1436
- */ _restoreGravity() {
1654
+ * Restores the gravity using the {@link module:engine/model/writer~Writer model writer}.
1655
+ *
1656
+ * A shorthand for {@link module:engine/model/writer~Writer#restoreSelectionGravity}.
1657
+ */ _restoreGravity() {
1437
1658
  this.editor.model.change((writer)=>{
1438
1659
  writer.restoreSelectionGravity(this._overrideUid);
1439
1660
  this._overrideUid = null;
1440
1661
  });
1441
1662
  }
1442
- /**
1443
- * @inheritDoc
1444
- */ constructor(editor){
1445
- super(editor);
1446
- /**
1447
- * A flag indicating that the automatic gravity restoration should not happen upon the next
1448
- * gravity restoration.
1449
- * {@link module:engine/model/selection~Selection#event:change:range} event.
1450
- */ this._isNextGravityRestorationSkipped = false;
1451
- this.attributes = new Set();
1452
- this._overrideUid = null;
1453
- }
1454
1663
  }
1455
1664
  /**
1456
1665
  * Checks whether the selection has any of given attributes.
@@ -1715,23 +1924,35 @@ const DEFAULT_TRANSFORMATIONS = [
1715
1924
  'typography',
1716
1925
  'quotes'
1717
1926
  ];
1718
- class TextTransformation extends Plugin {
1927
+ /**
1928
+ * The text transformation plugin.
1929
+ */ class TextTransformation extends Plugin {
1719
1930
  /**
1720
- * @inheritDoc
1721
- */ static get requires() {
1931
+ * @inheritDoc
1932
+ */ static get requires() {
1722
1933
  return [
1723
1934
  'Delete',
1724
1935
  'Input'
1725
1936
  ];
1726
1937
  }
1727
1938
  /**
1728
- * @inheritDoc
1729
- */ static get pluginName() {
1939
+ * @inheritDoc
1940
+ */ static get pluginName() {
1730
1941
  return 'TextTransformation';
1731
1942
  }
1732
1943
  /**
1733
- * @inheritDoc
1734
- */ init() {
1944
+ * @inheritDoc
1945
+ */ constructor(editor){
1946
+ super(editor);
1947
+ editor.config.define('typing', {
1948
+ transformations: {
1949
+ include: DEFAULT_TRANSFORMATIONS
1950
+ }
1951
+ });
1952
+ }
1953
+ /**
1954
+ * @inheritDoc
1955
+ */ init() {
1735
1956
  const model = this.editor.model;
1736
1957
  const modelSelection = model.document.selection;
1737
1958
  modelSelection.on('change:range', ()=>{
@@ -1741,8 +1962,8 @@ class TextTransformation extends Plugin {
1741
1962
  this._enableTransformationWatchers();
1742
1963
  }
1743
1964
  /**
1744
- * Create new TextWatcher listening to the editor for typing and selection events.
1745
- */ _enableTransformationWatchers() {
1965
+ * Create new TextWatcher listening to the editor for typing and selection events.
1966
+ */ _enableTransformationWatchers() {
1746
1967
  const editor = this.editor;
1747
1968
  const model = editor.model;
1748
1969
  const deletePlugin = editor.plugins.get('Delete');
@@ -1789,16 +2010,6 @@ class TextTransformation extends Plugin {
1789
2010
  });
1790
2011
  watcher.bind('isEnabled').to(this);
1791
2012
  }
1792
- /**
1793
- * @inheritDoc
1794
- */ constructor(editor){
1795
- super(editor);
1796
- editor.config.define('typing', {
1797
- transformations: {
1798
- include: DEFAULT_TRANSFORMATIONS
1799
- }
1800
- });
1801
- }
1802
2013
  }
1803
2014
  /**
1804
2015
  * Normalizes the configuration `from` parameter value.
@@ -1876,6 +2087,8 @@ class TextTransformation extends Plugin {
1876
2087
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
1877
2088
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
1878
2089
  */ /**
2090
+ * @module typing/utils/findattributerange
2091
+ */ /**
1879
2092
  * Returns a model range that covers all consecutive nodes with the same `attributeName` and its `value`
1880
2093
  * that intersect the given `position`.
1881
2094
  *