@ckeditor/ckeditor5-typing 40.0.0 → 40.2.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/CHANGELOG.md +27 -27
- package/LICENSE.md +3 -3
- package/package.json +4 -4
- package/src/augmentation.d.ts +27 -27
- package/src/augmentation.js +5 -5
- package/src/delete.d.ts +32 -32
- package/src/delete.js +82 -82
- package/src/deletecommand.d.ts +83 -83
- package/src/deletecommand.js +201 -201
- package/src/deleteobserver.d.ts +55 -55
- package/src/deleteobserver.js +261 -261
- package/src/index.d.ts +24 -24
- package/src/index.js +18 -18
- package/src/input.d.ts +21 -21
- package/src/input.js +141 -141
- package/src/inserttextcommand.d.ts +76 -76
- package/src/inserttextcommand.js +83 -80
- package/src/inserttextobserver.d.ts +59 -59
- package/src/inserttextobserver.js +108 -108
- package/src/texttransformation.d.ts +33 -33
- package/src/texttransformation.js +228 -228
- package/src/textwatcher.d.ts +138 -138
- package/src/textwatcher.js +105 -105
- package/src/twostepcaretmovement.d.ts +232 -199
- package/src/twostepcaretmovement.js +622 -435
- package/src/typing.d.ts +23 -23
- package/src/typing.js +27 -27
- package/src/typingconfig.d.ts +204 -204
- package/src/typingconfig.js +5 -5
- package/src/utils/changebuffer.d.ts +103 -103
- package/src/utils/changebuffer.js +123 -123
- package/src/utils/findattributerange.d.ts +33 -33
- package/src/utils/findattributerange.js +41 -41
- package/src/utils/getlasttextline.d.ts +49 -49
- package/src/utils/getlasttextline.js +43 -43
- package/src/utils/inlinehighlight.d.ts +33 -33
- package/src/utils/inlinehighlight.js +74 -74
|
@@ -1,435 +1,622 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module typing/twostepcaretmovement
|
|
7
|
-
*/
|
|
8
|
-
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
-
import { keyCodes } from '@ckeditor/ckeditor5-utils';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// When
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
if (!
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// DON'T ENGAGE 2-SCM
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @module typing/twostepcaretmovement
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import { keyCodes } from '@ckeditor/ckeditor5-utils';
|
|
10
|
+
import { MouseObserver } from '@ckeditor/ckeditor5-engine';
|
|
11
|
+
/**
|
|
12
|
+
* This plugin enables the two-step caret (phantom) movement behavior for
|
|
13
|
+
* {@link module:typing/twostepcaretmovement~TwoStepCaretMovement#registerAttribute registered attributes}
|
|
14
|
+
* on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
|
|
15
|
+
*
|
|
16
|
+
* Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
|
|
17
|
+
* beginning/end of an attribute.
|
|
18
|
+
*
|
|
19
|
+
* **Note:** This plugin support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
|
|
20
|
+
* but for the sake of simplicity examples showcase only left–to–right use–cases.
|
|
21
|
+
*
|
|
22
|
+
* # Forward movement
|
|
23
|
+
*
|
|
24
|
+
* ## "Entering" an attribute:
|
|
25
|
+
*
|
|
26
|
+
* When this plugin is enabled and registered for the `a` attribute and the selection is right before it
|
|
27
|
+
* (at the attribute boundary), pressing the right arrow key will not move the selection but update its
|
|
28
|
+
* attributes accordingly:
|
|
29
|
+
*
|
|
30
|
+
* * When enabled:
|
|
31
|
+
*
|
|
32
|
+
* ```xml
|
|
33
|
+
* foo{}<$text a="true">bar</$text>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* <kbd>→</kbd>
|
|
37
|
+
*
|
|
38
|
+
* ```xml
|
|
39
|
+
* foo<$text a="true">{}bar</$text>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* * When disabled:
|
|
43
|
+
*
|
|
44
|
+
* ```xml
|
|
45
|
+
* foo{}<$text a="true">bar</$text>
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* <kbd>→</kbd>
|
|
49
|
+
*
|
|
50
|
+
* ```xml
|
|
51
|
+
* foo<$text a="true">b{}ar</$text>
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
*
|
|
55
|
+
* ## "Leaving" an attribute:
|
|
56
|
+
*
|
|
57
|
+
* * When enabled:
|
|
58
|
+
*
|
|
59
|
+
* ```xml
|
|
60
|
+
* <$text a="true">bar{}</$text>baz
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* <kbd>→</kbd>
|
|
64
|
+
*
|
|
65
|
+
* ```xml
|
|
66
|
+
* <$text a="true">bar</$text>{}baz
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* * When disabled:
|
|
70
|
+
*
|
|
71
|
+
* ```xml
|
|
72
|
+
* <$text a="true">bar{}</$text>baz
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* <kbd>→</kbd>
|
|
76
|
+
*
|
|
77
|
+
* ```xml
|
|
78
|
+
* <$text a="true">bar</$text>b{}az
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* # Backward movement
|
|
82
|
+
*
|
|
83
|
+
* * When enabled:
|
|
84
|
+
*
|
|
85
|
+
* ```xml
|
|
86
|
+
* <$text a="true">bar</$text>{}baz
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* <kbd>←</kbd>
|
|
90
|
+
*
|
|
91
|
+
* ```xml
|
|
92
|
+
* <$text a="true">bar{}</$text>baz
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* * When disabled:
|
|
96
|
+
*
|
|
97
|
+
* ```xml
|
|
98
|
+
* <$text a="true">bar</$text>{}baz
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* <kbd>←</kbd>
|
|
102
|
+
*
|
|
103
|
+
* ```xml
|
|
104
|
+
* <$text a="true">ba{}r</$text>b{}az
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* # Multiple attributes
|
|
108
|
+
*
|
|
109
|
+
* * When enabled and many attributes starts or ends at the same position:
|
|
110
|
+
*
|
|
111
|
+
* ```xml
|
|
112
|
+
* <$text a="true" b="true">bar</$text>{}baz
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* <kbd>←</kbd>
|
|
116
|
+
*
|
|
117
|
+
* ```xml
|
|
118
|
+
* <$text a="true" b="true">bar{}</$text>baz
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* * When enabled and one procedes another:
|
|
122
|
+
*
|
|
123
|
+
* ```xml
|
|
124
|
+
* <$text a="true">bar</$text><$text b="true">{}bar</$text>
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* <kbd>←</kbd>
|
|
128
|
+
*
|
|
129
|
+
* ```xml
|
|
130
|
+
* <$text a="true">bar{}</$text><$text b="true">bar</$text>
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
*/
|
|
134
|
+
export default class TwoStepCaretMovement extends Plugin {
|
|
135
|
+
/**
|
|
136
|
+
* @inheritDoc
|
|
137
|
+
*/
|
|
138
|
+
static get pluginName() {
|
|
139
|
+
return 'TwoStepCaretMovement';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @inheritDoc
|
|
143
|
+
*/
|
|
144
|
+
constructor(editor) {
|
|
145
|
+
super(editor);
|
|
146
|
+
/**
|
|
147
|
+
* A flag indicating that the automatic gravity restoration should not happen upon the next
|
|
148
|
+
* gravity restoration.
|
|
149
|
+
* {@link module:engine/model/selection~Selection#event:change:range} event.
|
|
150
|
+
*/
|
|
151
|
+
this._isNextGravityRestorationSkipped = false;
|
|
152
|
+
this.attributes = new Set();
|
|
153
|
+
this._overrideUid = null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* @inheritDoc
|
|
157
|
+
*/
|
|
158
|
+
init() {
|
|
159
|
+
const editor = this.editor;
|
|
160
|
+
const model = editor.model;
|
|
161
|
+
const view = editor.editing.view;
|
|
162
|
+
const locale = editor.locale;
|
|
163
|
+
const modelSelection = model.document.selection;
|
|
164
|
+
// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
|
|
165
|
+
this.listenTo(view.document, 'arrowKey', (evt, data) => {
|
|
166
|
+
// This implementation works only for collapsed selection.
|
|
167
|
+
if (!modelSelection.isCollapsed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// When user tries to expand the selection or jump over the whole word or to the beginning/end then
|
|
171
|
+
// two-steps movement is not necessary.
|
|
172
|
+
if (data.shiftKey || data.altKey || data.ctrlKey) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const arrowRightPressed = data.keyCode == keyCodes.arrowright;
|
|
176
|
+
const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
|
|
177
|
+
// When neither left or right arrow has been pressed then do noting.
|
|
178
|
+
if (!arrowRightPressed && !arrowLeftPressed) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const contentDirection = locale.contentLanguageDirection;
|
|
182
|
+
let isMovementHandled = false;
|
|
183
|
+
if ((contentDirection === 'ltr' && arrowRightPressed) || (contentDirection === 'rtl' && arrowLeftPressed)) {
|
|
184
|
+
isMovementHandled = this._handleForwardMovement(data);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
isMovementHandled = this._handleBackwardMovement(data);
|
|
188
|
+
}
|
|
189
|
+
// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
|
|
190
|
+
// with other features which may also take over the caret movement (e.g. Widget).
|
|
191
|
+
if (isMovementHandled === true) {
|
|
192
|
+
evt.stop();
|
|
193
|
+
}
|
|
194
|
+
}, { context: '$text', priority: 'highest' });
|
|
195
|
+
// The automatic gravity restoration logic.
|
|
196
|
+
this.listenTo(modelSelection, 'change:range', (evt, data) => {
|
|
197
|
+
// Skipping the automatic restoration is needed if the selection should change
|
|
198
|
+
// but the gravity must remain overridden afterwards. See the #handleBackwardMovement
|
|
199
|
+
// to learn more.
|
|
200
|
+
if (this._isNextGravityRestorationSkipped) {
|
|
201
|
+
this._isNextGravityRestorationSkipped = false;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Skip automatic restore when the gravity is not overridden — simply, there's nothing to restore
|
|
205
|
+
// at this moment.
|
|
206
|
+
if (!this._isGravityOverridden) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Skip automatic restore when the change is indirect AND the selection is at the attribute boundary.
|
|
210
|
+
// It means that e.g. if the change was external (collaboration) and the user had their
|
|
211
|
+
// selection around the link, its gravity should remain intact in this change:range event.
|
|
212
|
+
if (!data.directChange && isBetweenDifferentAttributes(modelSelection.getFirstPosition(), this.attributes)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this._restoreGravity();
|
|
216
|
+
});
|
|
217
|
+
// Handle a click at the beginning/end of a two-step element.
|
|
218
|
+
this._enableClickingAfterNode();
|
|
219
|
+
// Change the attributes of the selection in certain situations after the two-step node was inserted into the document.
|
|
220
|
+
this._enableInsertContentSelectionAttributesFixer();
|
|
221
|
+
// Handle removing the content after the two-step node.
|
|
222
|
+
this._handleDeleteContentAfterNode();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Registers a given attribute for the two-step caret movement.
|
|
226
|
+
*
|
|
227
|
+
* @param attribute Name of the attribute to handle.
|
|
228
|
+
*/
|
|
229
|
+
registerAttribute(attribute) {
|
|
230
|
+
this.attributes.add(attribute);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Updates the document selection and the view according to the two–step caret movement state
|
|
234
|
+
* when moving **forwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
|
|
235
|
+
*
|
|
236
|
+
* @param data Data of the key press.
|
|
237
|
+
* @returns `true` when the handler prevented caret movement.
|
|
238
|
+
*/
|
|
239
|
+
_handleForwardMovement(data) {
|
|
240
|
+
const attributes = this.attributes;
|
|
241
|
+
const model = this.editor.model;
|
|
242
|
+
const selection = model.document.selection;
|
|
243
|
+
const position = selection.getFirstPosition();
|
|
244
|
+
// DON'T ENGAGE 2-SCM if gravity is already overridden. It means that we just entered
|
|
245
|
+
//
|
|
246
|
+
// <paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
|
|
247
|
+
//
|
|
248
|
+
// or left the attribute
|
|
249
|
+
//
|
|
250
|
+
// <paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
|
|
251
|
+
//
|
|
252
|
+
// and the gravity will be restored automatically.
|
|
253
|
+
if (this._isGravityOverridden) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
// DON'T ENGAGE 2-SCM when the selection is at the beginning of the block AND already has the
|
|
257
|
+
// attribute:
|
|
258
|
+
// * when the selection was initially set there using the mouse,
|
|
259
|
+
// * when the editor has just started
|
|
260
|
+
//
|
|
261
|
+
// <paragraph><$text attribute>{}bar</$text>baz</paragraph>
|
|
262
|
+
//
|
|
263
|
+
if (position.isAtStart && hasAnyAttribute(selection, attributes)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// ENGAGE 2-SCM When at least one of the observed attributes changes its value (incl. starts, ends).
|
|
267
|
+
//
|
|
268
|
+
// <paragraph>foo<$text attribute>bar{}</$text>baz</paragraph>
|
|
269
|
+
// <paragraph>foo<$text attribute>bar{}</$text><$text otherAttribute>baz</$text></paragraph>
|
|
270
|
+
// <paragraph>foo<$text attribute=1>bar{}</$text><$text attribute=2>baz</$text></paragraph>
|
|
271
|
+
// <paragraph>foo{}<$text attribute>bar</$text>baz</paragraph>
|
|
272
|
+
//
|
|
273
|
+
if (isBetweenDifferentAttributes(position, attributes)) {
|
|
274
|
+
preventCaretMovement(data);
|
|
275
|
+
// CLEAR 2-SCM attributes if we are at the end of one 2-SCM and before
|
|
276
|
+
// the next one with a different value of the same attribute.
|
|
277
|
+
//
|
|
278
|
+
// <paragraph>foo<$text attribute=1>bar{}</$text><$text attribute=2>bar</$text>baz</paragraph>
|
|
279
|
+
//
|
|
280
|
+
if (hasAnyAttribute(selection, attributes) &&
|
|
281
|
+
isBetweenDifferentAttributes(position, attributes, true)) {
|
|
282
|
+
clearSelectionAttributes(model, attributes);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
this._overrideGravity();
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Updates the document selection and the view according to the two–step caret movement state
|
|
293
|
+
* when moving **backwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
|
|
294
|
+
*
|
|
295
|
+
* @param data Data of the key press.
|
|
296
|
+
* @returns `true` when the handler prevented caret movement
|
|
297
|
+
*/
|
|
298
|
+
_handleBackwardMovement(data) {
|
|
299
|
+
const attributes = this.attributes;
|
|
300
|
+
const model = this.editor.model;
|
|
301
|
+
const selection = model.document.selection;
|
|
302
|
+
const position = selection.getFirstPosition();
|
|
303
|
+
// When the gravity is already overridden (by this plugin), it means we are on the two-step position.
|
|
304
|
+
// Prevent the movement, restore the gravity and update selection attributes.
|
|
305
|
+
//
|
|
306
|
+
// <paragraph>foo<$text attribute=1>bar</$text><$text attribute=2>{}baz</$text></paragraph>
|
|
307
|
+
// <paragraph>foo<$text attribute>bar</$text><$text otherAttribute>{}baz</$text></paragraph>
|
|
308
|
+
// <paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
|
|
309
|
+
// <paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
|
|
310
|
+
//
|
|
311
|
+
if (this._isGravityOverridden) {
|
|
312
|
+
preventCaretMovement(data);
|
|
313
|
+
this._restoreGravity();
|
|
314
|
+
// CLEAR 2-SCM attributes if we are at the end of one 2-SCM and before
|
|
315
|
+
// the next one with a different value of the same attribute.
|
|
316
|
+
//
|
|
317
|
+
// <paragraph>foo<$text attribute=1>bar</$text><$text attribute=2>{}bar</$text>baz</paragraph>
|
|
318
|
+
//
|
|
319
|
+
if (isBetweenDifferentAttributes(position, attributes, true)) {
|
|
320
|
+
clearSelectionAttributes(model, attributes);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
setSelectionAttributesFromTheNodeBefore(model, attributes, position);
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// REMOVE SELECTION ATTRIBUTE when restoring gravity towards a non-existent content at the
|
|
329
|
+
// beginning of the block.
|
|
330
|
+
//
|
|
331
|
+
// <paragraph>{}<$text attribute>bar</$text></paragraph>
|
|
332
|
+
//
|
|
333
|
+
if (position.isAtStart) {
|
|
334
|
+
if (hasAnyAttribute(selection, attributes)) {
|
|
335
|
+
preventCaretMovement(data);
|
|
336
|
+
setSelectionAttributesFromTheNodeBefore(model, attributes, position);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
// SET 2-SCM attributes if we are between nodes with the same attribute but with different values.
|
|
342
|
+
//
|
|
343
|
+
// <paragraph>foo<$text attribute=1>bar</$text>[]<$text attribute=2>bar</$text>baz</paragraph>
|
|
344
|
+
//
|
|
345
|
+
if (!hasAnyAttribute(selection, attributes) &&
|
|
346
|
+
isBetweenDifferentAttributes(position, attributes, true)) {
|
|
347
|
+
preventCaretMovement(data);
|
|
348
|
+
setSelectionAttributesFromTheNodeBefore(model, attributes, position);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
// When we are moving from natural gravity, to the position of the 2SCM, we need to override the gravity,
|
|
352
|
+
// and make sure it won't be restored. Unless it's at the end of the block and an observed attribute.
|
|
353
|
+
// We need to check if the caret is a one position before the attribute boundary:
|
|
354
|
+
//
|
|
355
|
+
// <paragraph>foo<$text attribute=1>bar</$text><$text attribute=2>b{}az</$text></paragraph>
|
|
356
|
+
// <paragraph>foo<$text attribute>bar</$text><$text otherAttribute>b{}az</$text></paragraph>
|
|
357
|
+
// <paragraph>foo<$text attribute>b{}ar</$text>baz</paragraph>
|
|
358
|
+
// <paragraph>foo<$text attribute>bar</$text>b{}az</paragraph>
|
|
359
|
+
//
|
|
360
|
+
if (isStepAfterAnyAttributeBoundary(position, attributes)) {
|
|
361
|
+
// ENGAGE 2-SCM if the selection has no attribute. This may happen when the user
|
|
362
|
+
// left the attribute using a FORWARD 2-SCM.
|
|
363
|
+
//
|
|
364
|
+
// <paragraph><$text attribute>bar</$text>{}</paragraph>
|
|
365
|
+
//
|
|
366
|
+
if (position.isAtEnd &&
|
|
367
|
+
!hasAnyAttribute(selection, attributes) &&
|
|
368
|
+
isBetweenDifferentAttributes(position, attributes)) {
|
|
369
|
+
preventCaretMovement(data);
|
|
370
|
+
setSelectionAttributesFromTheNodeBefore(model, attributes, position);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
// Skip the automatic gravity restore upon the next selection#change:range event.
|
|
374
|
+
// If not skipped, it would automatically restore the gravity, which should remain
|
|
375
|
+
// overridden.
|
|
376
|
+
this._isNextGravityRestorationSkipped = true;
|
|
377
|
+
this._overrideGravity();
|
|
378
|
+
// Don't return "true" here because we didn't call _preventCaretMovement.
|
|
379
|
+
// Returning here will destabilize the filler logic, which also listens to
|
|
380
|
+
// keydown (and the event would be stopped).
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Starts listening to {@link module:engine/view/document~Document#event:mousedown} and
|
|
388
|
+
* {@link module:engine/view/document~Document#event:selectionChange} and puts the selection before/after a 2-step node
|
|
389
|
+
* if clicked at the beginning/ending of the 2-step node.
|
|
390
|
+
*
|
|
391
|
+
* The purpose of this action is to allow typing around the 2-step node directly after a click.
|
|
392
|
+
*
|
|
393
|
+
* See https://github.com/ckeditor/ckeditor5/issues/1016.
|
|
394
|
+
*/
|
|
395
|
+
_enableClickingAfterNode() {
|
|
396
|
+
const editor = this.editor;
|
|
397
|
+
const model = editor.model;
|
|
398
|
+
const selection = model.document.selection;
|
|
399
|
+
const document = editor.editing.view.document;
|
|
400
|
+
editor.editing.view.addObserver(MouseObserver);
|
|
401
|
+
let clicked = false;
|
|
402
|
+
// Detect the click.
|
|
403
|
+
this.listenTo(document, 'mousedown', () => {
|
|
404
|
+
clicked = true;
|
|
405
|
+
});
|
|
406
|
+
// When the selection has changed...
|
|
407
|
+
this.listenTo(document, 'selectionChange', () => {
|
|
408
|
+
const attributes = this.attributes;
|
|
409
|
+
if (!clicked) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// ...and it was caused by the click...
|
|
413
|
+
clicked = false;
|
|
414
|
+
// ...and no text is selected...
|
|
415
|
+
if (!selection.isCollapsed) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// ...and clicked text is the 2-step node...
|
|
419
|
+
if (!hasAnyAttribute(selection, attributes)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const position = selection.getFirstPosition();
|
|
423
|
+
if (!isBetweenDifferentAttributes(position, attributes)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// The selection at the start of a block would use surrounding attributes
|
|
427
|
+
// from text after the selection so just clear 2-SCM attributes.
|
|
428
|
+
//
|
|
429
|
+
// Also, clear attributes for selection between same attribute with different values.
|
|
430
|
+
if (position.isAtStart ||
|
|
431
|
+
isBetweenDifferentAttributes(position, attributes, true)) {
|
|
432
|
+
clearSelectionAttributes(model, attributes);
|
|
433
|
+
}
|
|
434
|
+
else if (!this._isGravityOverridden) {
|
|
435
|
+
this._overrideGravity();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Starts listening to {@link module:engine/model/model~Model#event:insertContent} and corrects the model
|
|
441
|
+
* selection attributes if the selection is at the end of a two-step node after inserting the content.
|
|
442
|
+
*
|
|
443
|
+
* The purpose of this action is to improve the overall UX because the user is no longer "trapped" by the
|
|
444
|
+
* two-step attribute of the selection, and they can type a "clean" (`linkHref`–less) text right away.
|
|
445
|
+
*
|
|
446
|
+
* See https://github.com/ckeditor/ckeditor5/issues/6053.
|
|
447
|
+
*/
|
|
448
|
+
_enableInsertContentSelectionAttributesFixer() {
|
|
449
|
+
const editor = this.editor;
|
|
450
|
+
const model = editor.model;
|
|
451
|
+
const selection = model.document.selection;
|
|
452
|
+
const attributes = this.attributes;
|
|
453
|
+
this.listenTo(model, 'insertContent', () => {
|
|
454
|
+
const position = selection.getFirstPosition();
|
|
455
|
+
if (hasAnyAttribute(selection, attributes) &&
|
|
456
|
+
isBetweenDifferentAttributes(position, attributes)) {
|
|
457
|
+
clearSelectionAttributes(model, attributes);
|
|
458
|
+
}
|
|
459
|
+
}, { priority: 'low' });
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
|
|
463
|
+
* removing a content right after the tow-step attribute.
|
|
464
|
+
*
|
|
465
|
+
* If so, the selection should not preserve the two-step attribute. However, if
|
|
466
|
+
* the {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is active and
|
|
467
|
+
* the selection has the two-step attribute due to overridden gravity (at the end), the two-step attribute should stay untouched.
|
|
468
|
+
*
|
|
469
|
+
* The purpose of this action is to allow removing the link text and keep the selection outside the link.
|
|
470
|
+
*
|
|
471
|
+
* See https://github.com/ckeditor/ckeditor5/issues/7521.
|
|
472
|
+
*/
|
|
473
|
+
_handleDeleteContentAfterNode() {
|
|
474
|
+
const editor = this.editor;
|
|
475
|
+
const model = editor.model;
|
|
476
|
+
const selection = model.document.selection;
|
|
477
|
+
const view = editor.editing.view;
|
|
478
|
+
let isBackspace = false;
|
|
479
|
+
let shouldPreserveAttributes = false;
|
|
480
|
+
// Detect pressing `Backspace`.
|
|
481
|
+
this.listenTo(view.document, 'delete', (evt, data) => {
|
|
482
|
+
isBackspace = data.direction === 'backward';
|
|
483
|
+
}, { priority: 'high' });
|
|
484
|
+
// Before removing the content, check whether the selection is inside a two-step attribute.
|
|
485
|
+
// If so, we want to preserve those attributes.
|
|
486
|
+
this.listenTo(model, 'deleteContent', () => {
|
|
487
|
+
if (!isBackspace) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const position = selection.getFirstPosition();
|
|
491
|
+
shouldPreserveAttributes = hasAnyAttribute(selection, this.attributes) &&
|
|
492
|
+
!isStepAfterAnyAttributeBoundary(position, this.attributes);
|
|
493
|
+
}, { priority: 'high' });
|
|
494
|
+
// After removing the content, check whether the current selection should preserve the `linkHref` attribute.
|
|
495
|
+
this.listenTo(model, 'deleteContent', () => {
|
|
496
|
+
if (!isBackspace) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
isBackspace = false;
|
|
500
|
+
// Do not escape two-step attribute if it was inside it before content deletion.
|
|
501
|
+
if (shouldPreserveAttributes) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// Use `model.enqueueChange()` in order to execute the callback at the end of the changes process.
|
|
505
|
+
editor.model.enqueueChange(() => {
|
|
506
|
+
const position = selection.getFirstPosition();
|
|
507
|
+
if (hasAnyAttribute(selection, this.attributes) &&
|
|
508
|
+
isBetweenDifferentAttributes(position, this.attributes)) {
|
|
509
|
+
if (position.isAtStart || isBetweenDifferentAttributes(position, this.attributes, true)) {
|
|
510
|
+
clearSelectionAttributes(model, this.attributes);
|
|
511
|
+
}
|
|
512
|
+
else if (!this._isGravityOverridden) {
|
|
513
|
+
this._overrideGravity();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}, { priority: 'low' });
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* `true` when the gravity is overridden for the plugin.
|
|
521
|
+
*/
|
|
522
|
+
get _isGravityOverridden() {
|
|
523
|
+
return !!this._overrideUid;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Overrides the gravity using the {@link module:engine/model/writer~Writer model writer}
|
|
527
|
+
* and stores the information about this fact in the {@link #_overrideUid}.
|
|
528
|
+
*
|
|
529
|
+
* A shorthand for {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
|
|
530
|
+
*/
|
|
531
|
+
_overrideGravity() {
|
|
532
|
+
this._overrideUid = this.editor.model.change(writer => {
|
|
533
|
+
return writer.overrideSelectionGravity();
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Restores the gravity using the {@link module:engine/model/writer~Writer model writer}.
|
|
538
|
+
*
|
|
539
|
+
* A shorthand for {@link module:engine/model/writer~Writer#restoreSelectionGravity}.
|
|
540
|
+
*/
|
|
541
|
+
_restoreGravity() {
|
|
542
|
+
this.editor.model.change(writer => {
|
|
543
|
+
writer.restoreSelectionGravity(this._overrideUid);
|
|
544
|
+
this._overrideUid = null;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Checks whether the selection has any of given attributes.
|
|
550
|
+
*/
|
|
551
|
+
function hasAnyAttribute(selection, attributes) {
|
|
552
|
+
for (const observedAttribute of attributes) {
|
|
553
|
+
if (selection.hasAttribute(observedAttribute)) {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Applies the given attributes to the current selection using using the
|
|
561
|
+
* values from the node before the current position. Uses
|
|
562
|
+
* the {@link module:engine/model/writer~Writer model writer}.
|
|
563
|
+
*/
|
|
564
|
+
function setSelectionAttributesFromTheNodeBefore(model, attributes, position) {
|
|
565
|
+
const nodeBefore = position.nodeBefore;
|
|
566
|
+
model.change(writer => {
|
|
567
|
+
if (nodeBefore) {
|
|
568
|
+
const attributes = [];
|
|
569
|
+
const isInlineObject = model.schema.isObject(nodeBefore) && model.schema.isInline(nodeBefore);
|
|
570
|
+
for (const [key, value] of nodeBefore.getAttributes()) {
|
|
571
|
+
if (model.schema.checkAttribute('$text', key) &&
|
|
572
|
+
(!isInlineObject || model.schema.getAttributeProperties(key).copyFromObject !== false)) {
|
|
573
|
+
attributes.push([key, value]);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
writer.setSelectionAttribute(attributes);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
writer.removeSelectionAttribute(attributes);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Removes 2-SCM attributes from the selection.
|
|
585
|
+
*/
|
|
586
|
+
function clearSelectionAttributes(model, attributes) {
|
|
587
|
+
model.change(writer => {
|
|
588
|
+
writer.removeSelectionAttribute(attributes);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Prevents the caret movement in the view by calling `preventDefault` on the event data.
|
|
593
|
+
*
|
|
594
|
+
* @alias data.preventDefault
|
|
595
|
+
*/
|
|
596
|
+
function preventCaretMovement(data) {
|
|
597
|
+
data.preventDefault();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Checks whether the step before `isBetweenDifferentAttributes()`.
|
|
601
|
+
*/
|
|
602
|
+
function isStepAfterAnyAttributeBoundary(position, attributes) {
|
|
603
|
+
const positionBefore = position.getShiftedBy(-1);
|
|
604
|
+
return isBetweenDifferentAttributes(positionBefore, attributes);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Checks whether the given position is between different values of given attributes.
|
|
608
|
+
*/
|
|
609
|
+
function isBetweenDifferentAttributes(position, attributes, isStrict = false) {
|
|
610
|
+
const { nodeBefore, nodeAfter } = position;
|
|
611
|
+
for (const observedAttribute of attributes) {
|
|
612
|
+
const attrBefore = nodeBefore ? nodeBefore.getAttribute(observedAttribute) : undefined;
|
|
613
|
+
const attrAfter = nodeAfter ? nodeAfter.getAttribute(observedAttribute) : undefined;
|
|
614
|
+
if (isStrict && (attrBefore === undefined || attrAfter === undefined)) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (attrAfter !== attrBefore) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
}
|