@nasser-sw/fabric 7.0.1-beta10 → 7.0.1-beta12
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 +177 -117
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +177 -117
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +177 -117
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +177 -117
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.mjs +10 -0
- package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.mjs +162 -116
- package/dist/src/shapes/IText/ITextKeyBehavior.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +4 -0
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Text/constants.d.ts +1 -1
- package/dist/src/shapes/Text/constants.d.ts.map +1 -1
- package/dist/src/shapes/Text/constants.min.mjs +1 -1
- package/dist/src/shapes/Text/constants.min.mjs.map +1 -1
- package/dist/src/shapes/Text/constants.mjs +1 -1
- package/dist/src/shapes/Text/constants.mjs.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextKeyBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/constants.d.ts +1 -1
- package/dist-extensions/src/shapes/Text/constants.d.ts.map +1 -1
- package/fabric-test-editor.html +1 -1
- package/package.json +1 -1
- package/rtl-debug.html +745 -0
- package/src/shapes/IText/ITextBehavior.ts +11 -0
- package/src/shapes/IText/ITextKeyBehavior.ts +732 -691
- package/src/shapes/Text/Text.ts +4 -0
- package/src/shapes/Text/constants.ts +1 -0
|
@@ -1,691 +1,732 @@
|
|
|
1
|
-
import { config } from '../../config';
|
|
2
|
-
import { getFabricDocument, getEnv } from '../../env';
|
|
3
|
-
import { capValue } from '../../util/misc/capValue';
|
|
4
|
-
import type { ITextEvents } from './ITextBehavior';
|
|
5
|
-
import { ITextBehavior } from './ITextBehavior';
|
|
6
|
-
import type { TKeyMapIText } from './constants';
|
|
7
|
-
import type { TOptions } from '../../typedefs';
|
|
8
|
-
import type { TextProps, SerializedTextProps } from '../Text/Text';
|
|
9
|
-
import { getDocumentFromElement } from '../../util/dom_misc';
|
|
10
|
-
import { CHANGED, LEFT, RIGHT } from '../../constants';
|
|
11
|
-
import type { IText } from './IText';
|
|
12
|
-
import type { TextStyleDeclaration } from '../Text/StyledText';
|
|
13
|
-
|
|
14
|
-
export abstract class ITextKeyBehavior<
|
|
15
|
-
Props extends TOptions<TextProps> = Partial<TextProps>,
|
|
16
|
-
SProps extends SerializedTextProps = SerializedTextProps,
|
|
17
|
-
EventSpec extends ITextEvents = ITextEvents,
|
|
18
|
-
> extends ITextBehavior<Props, SProps, EventSpec> {
|
|
19
|
-
/**
|
|
20
|
-
* For functionalities on keyDown
|
|
21
|
-
* Map a special key to a function of the instance/prototype
|
|
22
|
-
* If you need different behavior for ESC or TAB or arrows, you have to change
|
|
23
|
-
* this map setting the name of a function that you build on the IText or
|
|
24
|
-
* your prototype.
|
|
25
|
-
* the map change will affect all Instances unless you need for only some text Instances
|
|
26
|
-
* in that case you have to clone this object and assign your Instance.
|
|
27
|
-
* this.keysMap = Object.assign({}, this.keysMap);
|
|
28
|
-
* The function must be in IText.prototype.myFunction And will receive event as args[0]
|
|
29
|
-
*/
|
|
30
|
-
declare keysMap: TKeyMapIText;
|
|
31
|
-
|
|
32
|
-
declare keysMapRtl: TKeyMapIText;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* For functionalities on keyUp + ctrl || cmd
|
|
36
|
-
*/
|
|
37
|
-
declare ctrlKeysMapUp: TKeyMapIText;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* For functionalities on keyDown + ctrl || cmd
|
|
41
|
-
*/
|
|
42
|
-
declare ctrlKeysMapDown: TKeyMapIText;
|
|
43
|
-
|
|
44
|
-
declare hiddenTextarea: HTMLTextAreaElement | null;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* DOM container to append the hiddenTextarea.
|
|
48
|
-
* An alternative to attaching to the document.body.
|
|
49
|
-
* Useful to reduce laggish redraw of the full document.body tree and
|
|
50
|
-
* also with modals event capturing that won't let the textarea take focus.
|
|
51
|
-
* @type HTMLElement
|
|
52
|
-
*/
|
|
53
|
-
declare hiddenTextareaContainer?: HTMLElement | null;
|
|
54
|
-
|
|
55
|
-
declare private _clickHandlerInitialized: boolean;
|
|
56
|
-
declare private _copyDone: boolean;
|
|
57
|
-
declare private fromPaste: boolean;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Initializes hidden textarea (needed to bring up keyboard in iOS)
|
|
61
|
-
*/
|
|
62
|
-
initHiddenTextarea() {
|
|
63
|
-
const doc =
|
|
64
|
-
(this.canvas && getDocumentFromElement(this.canvas.getElement())) ||
|
|
65
|
-
getFabricDocument();
|
|
66
|
-
const textarea = doc.createElement('textarea');
|
|
67
|
-
Object.entries({
|
|
68
|
-
autocapitalize: 'off',
|
|
69
|
-
autocorrect: 'off',
|
|
70
|
-
autocomplete: 'off',
|
|
71
|
-
spellcheck: 'false',
|
|
72
|
-
'data-fabric': 'textarea',
|
|
73
|
-
wrap: 'off',
|
|
74
|
-
}).map(([attribute, value]) => textarea.setAttribute(attribute, value));
|
|
75
|
-
const { top, left, fontSize } = this._calcTextareaPosition();
|
|
76
|
-
// line-height: 1px; was removed from the style to fix this:
|
|
77
|
-
// https://bugs.chromium.org/p/chromium/issues/detail?id=870966
|
|
78
|
-
textarea.style.cssText = `position: absolute; top: ${top}; left: ${left}; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; padding-top: ${fontSize};`;
|
|
79
|
-
|
|
80
|
-
(this.hiddenTextareaContainer || doc.body).appendChild(textarea);
|
|
81
|
-
|
|
82
|
-
Object.entries({
|
|
83
|
-
blur: 'blur',
|
|
84
|
-
keydown: 'onKeyDown',
|
|
85
|
-
keyup: 'onKeyUp',
|
|
86
|
-
input: 'onInput',
|
|
87
|
-
copy: 'copy',
|
|
88
|
-
cut: 'copy',
|
|
89
|
-
paste: 'paste',
|
|
90
|
-
compositionstart: 'onCompositionStart',
|
|
91
|
-
compositionupdate: 'onCompositionUpdate',
|
|
92
|
-
compositionend: 'onCompositionEnd',
|
|
93
|
-
} as Record<string, keyof this>).map(([eventName, handler]) =>
|
|
94
|
-
textarea.addEventListener(
|
|
95
|
-
eventName,
|
|
96
|
-
(this[handler] as EventListener).bind(this),
|
|
97
|
-
),
|
|
98
|
-
);
|
|
99
|
-
this.hiddenTextarea = textarea;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Override this method to customize cursor behavior on textbox blur
|
|
104
|
-
*/
|
|
105
|
-
blur() {
|
|
106
|
-
this.abortCursorAnimation();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Handles keydown event
|
|
111
|
-
* only used for arrows and combination of modifier keys.
|
|
112
|
-
* @param {KeyboardEvent} e Event object
|
|
113
|
-
*/
|
|
114
|
-
onKeyDown(e: KeyboardEvent) {
|
|
115
|
-
if (!this.isEditing) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap;
|
|
119
|
-
if (e.keyCode in keyMap) {
|
|
120
|
-
(this[keyMap[e.keyCode] as keyof this] as (arg: KeyboardEvent) => void)(
|
|
121
|
-
e,
|
|
122
|
-
);
|
|
123
|
-
} else if (e.keyCode in this.ctrlKeysMapDown && (e.ctrlKey || e.metaKey)) {
|
|
124
|
-
(
|
|
125
|
-
this[this.ctrlKeysMapDown[e.keyCode] as keyof this] as (
|
|
126
|
-
arg: KeyboardEvent,
|
|
127
|
-
) => void
|
|
128
|
-
)(e);
|
|
129
|
-
} else {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
e.stopImmediatePropagation();
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
if (e.keyCode >= 33 && e.keyCode <= 40) {
|
|
135
|
-
// if i press an arrow key just update selection
|
|
136
|
-
this.inCompositionMode = false;
|
|
137
|
-
this.clearContextTop();
|
|
138
|
-
this.renderCursorOrSelection();
|
|
139
|
-
} else {
|
|
140
|
-
this.canvas && this.canvas.requestRenderAll();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Handles keyup event
|
|
146
|
-
* We handle KeyUp because ie11 and edge have difficulties copy/pasting
|
|
147
|
-
* if a copy/cut event fired, keyup is dismissed
|
|
148
|
-
* @param {KeyboardEvent} e Event object
|
|
149
|
-
*/
|
|
150
|
-
onKeyUp(e: KeyboardEvent) {
|
|
151
|
-
if (!this.isEditing || this._copyDone || this.inCompositionMode) {
|
|
152
|
-
this._copyDone = false;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (e.keyCode in this.ctrlKeysMapUp && (e.ctrlKey || e.metaKey)) {
|
|
156
|
-
(
|
|
157
|
-
this[this.ctrlKeysMapUp[e.keyCode] as keyof this] as (
|
|
158
|
-
arg: KeyboardEvent,
|
|
159
|
-
) => void
|
|
160
|
-
)(e);
|
|
161
|
-
} else {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
e.stopImmediatePropagation();
|
|
165
|
-
e.preventDefault();
|
|
166
|
-
this.canvas && this.canvas.requestRenderAll();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Handles onInput event
|
|
171
|
-
* @param {Event} e Event object
|
|
172
|
-
*/
|
|
173
|
-
onInput(this: this & { hiddenTextarea: HTMLTextAreaElement }, e: Event) {
|
|
174
|
-
const fromPaste = this.fromPaste;
|
|
175
|
-
const { value, selectionStart, selectionEnd } = this.hiddenTextarea;
|
|
176
|
-
this.fromPaste = false;
|
|
177
|
-
e && e.stopPropagation();
|
|
178
|
-
if (!this.isEditing) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
this.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
this.
|
|
500
|
-
this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
this.
|
|
530
|
-
} else {
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
this
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
*
|
|
591
|
-
* @
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
this.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1
|
+
import { config } from '../../config';
|
|
2
|
+
import { getFabricDocument, getEnv } from '../../env';
|
|
3
|
+
import { capValue } from '../../util/misc/capValue';
|
|
4
|
+
import type { ITextEvents } from './ITextBehavior';
|
|
5
|
+
import { ITextBehavior } from './ITextBehavior';
|
|
6
|
+
import type { TKeyMapIText } from './constants';
|
|
7
|
+
import type { TOptions } from '../../typedefs';
|
|
8
|
+
import type { TextProps, SerializedTextProps } from '../Text/Text';
|
|
9
|
+
import { getDocumentFromElement } from '../../util/dom_misc';
|
|
10
|
+
import { CHANGED, LEFT, RIGHT } from '../../constants';
|
|
11
|
+
import type { IText } from './IText';
|
|
12
|
+
import type { TextStyleDeclaration } from '../Text/StyledText';
|
|
13
|
+
|
|
14
|
+
export abstract class ITextKeyBehavior<
|
|
15
|
+
Props extends TOptions<TextProps> = Partial<TextProps>,
|
|
16
|
+
SProps extends SerializedTextProps = SerializedTextProps,
|
|
17
|
+
EventSpec extends ITextEvents = ITextEvents,
|
|
18
|
+
> extends ITextBehavior<Props, SProps, EventSpec> {
|
|
19
|
+
/**
|
|
20
|
+
* For functionalities on keyDown
|
|
21
|
+
* Map a special key to a function of the instance/prototype
|
|
22
|
+
* If you need different behavior for ESC or TAB or arrows, you have to change
|
|
23
|
+
* this map setting the name of a function that you build on the IText or
|
|
24
|
+
* your prototype.
|
|
25
|
+
* the map change will affect all Instances unless you need for only some text Instances
|
|
26
|
+
* in that case you have to clone this object and assign your Instance.
|
|
27
|
+
* this.keysMap = Object.assign({}, this.keysMap);
|
|
28
|
+
* The function must be in IText.prototype.myFunction And will receive event as args[0]
|
|
29
|
+
*/
|
|
30
|
+
declare keysMap: TKeyMapIText;
|
|
31
|
+
|
|
32
|
+
declare keysMapRtl: TKeyMapIText;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* For functionalities on keyUp + ctrl || cmd
|
|
36
|
+
*/
|
|
37
|
+
declare ctrlKeysMapUp: TKeyMapIText;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* For functionalities on keyDown + ctrl || cmd
|
|
41
|
+
*/
|
|
42
|
+
declare ctrlKeysMapDown: TKeyMapIText;
|
|
43
|
+
|
|
44
|
+
declare hiddenTextarea: HTMLTextAreaElement | null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* DOM container to append the hiddenTextarea.
|
|
48
|
+
* An alternative to attaching to the document.body.
|
|
49
|
+
* Useful to reduce laggish redraw of the full document.body tree and
|
|
50
|
+
* also with modals event capturing that won't let the textarea take focus.
|
|
51
|
+
* @type HTMLElement
|
|
52
|
+
*/
|
|
53
|
+
declare hiddenTextareaContainer?: HTMLElement | null;
|
|
54
|
+
|
|
55
|
+
declare private _clickHandlerInitialized: boolean;
|
|
56
|
+
declare private _copyDone: boolean;
|
|
57
|
+
declare private fromPaste: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initializes hidden textarea (needed to bring up keyboard in iOS)
|
|
61
|
+
*/
|
|
62
|
+
initHiddenTextarea() {
|
|
63
|
+
const doc =
|
|
64
|
+
(this.canvas && getDocumentFromElement(this.canvas.getElement())) ||
|
|
65
|
+
getFabricDocument();
|
|
66
|
+
const textarea = doc.createElement('textarea');
|
|
67
|
+
Object.entries({
|
|
68
|
+
autocapitalize: 'off',
|
|
69
|
+
autocorrect: 'off',
|
|
70
|
+
autocomplete: 'off',
|
|
71
|
+
spellcheck: 'false',
|
|
72
|
+
'data-fabric': 'textarea',
|
|
73
|
+
wrap: 'off',
|
|
74
|
+
}).map(([attribute, value]) => textarea.setAttribute(attribute, value));
|
|
75
|
+
const { top, left, fontSize } = this._calcTextareaPosition();
|
|
76
|
+
// line-height: 1px; was removed from the style to fix this:
|
|
77
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=870966
|
|
78
|
+
textarea.style.cssText = `position: absolute; top: ${top}; left: ${left}; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; padding-top: ${fontSize};`;
|
|
79
|
+
|
|
80
|
+
(this.hiddenTextareaContainer || doc.body).appendChild(textarea);
|
|
81
|
+
|
|
82
|
+
Object.entries({
|
|
83
|
+
blur: 'blur',
|
|
84
|
+
keydown: 'onKeyDown',
|
|
85
|
+
keyup: 'onKeyUp',
|
|
86
|
+
input: 'onInput',
|
|
87
|
+
copy: 'copy',
|
|
88
|
+
cut: 'copy',
|
|
89
|
+
paste: 'paste',
|
|
90
|
+
compositionstart: 'onCompositionStart',
|
|
91
|
+
compositionupdate: 'onCompositionUpdate',
|
|
92
|
+
compositionend: 'onCompositionEnd',
|
|
93
|
+
} as Record<string, keyof this>).map(([eventName, handler]) =>
|
|
94
|
+
textarea.addEventListener(
|
|
95
|
+
eventName,
|
|
96
|
+
(this[handler] as EventListener).bind(this),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
this.hiddenTextarea = textarea;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Override this method to customize cursor behavior on textbox blur
|
|
104
|
+
*/
|
|
105
|
+
blur() {
|
|
106
|
+
this.abortCursorAnimation();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handles keydown event
|
|
111
|
+
* only used for arrows and combination of modifier keys.
|
|
112
|
+
* @param {KeyboardEvent} e Event object
|
|
113
|
+
*/
|
|
114
|
+
onKeyDown(e: KeyboardEvent) {
|
|
115
|
+
if (!this.isEditing) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap;
|
|
119
|
+
if (e.keyCode in keyMap) {
|
|
120
|
+
(this[keyMap[e.keyCode] as keyof this] as (arg: KeyboardEvent) => void)(
|
|
121
|
+
e,
|
|
122
|
+
);
|
|
123
|
+
} else if (e.keyCode in this.ctrlKeysMapDown && (e.ctrlKey || e.metaKey)) {
|
|
124
|
+
(
|
|
125
|
+
this[this.ctrlKeysMapDown[e.keyCode] as keyof this] as (
|
|
126
|
+
arg: KeyboardEvent,
|
|
127
|
+
) => void
|
|
128
|
+
)(e);
|
|
129
|
+
} else {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
e.stopImmediatePropagation();
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
if (e.keyCode >= 33 && e.keyCode <= 40) {
|
|
135
|
+
// if i press an arrow key just update selection
|
|
136
|
+
this.inCompositionMode = false;
|
|
137
|
+
this.clearContextTop();
|
|
138
|
+
this.renderCursorOrSelection();
|
|
139
|
+
} else {
|
|
140
|
+
this.canvas && this.canvas.requestRenderAll();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handles keyup event
|
|
146
|
+
* We handle KeyUp because ie11 and edge have difficulties copy/pasting
|
|
147
|
+
* if a copy/cut event fired, keyup is dismissed
|
|
148
|
+
* @param {KeyboardEvent} e Event object
|
|
149
|
+
*/
|
|
150
|
+
onKeyUp(e: KeyboardEvent) {
|
|
151
|
+
if (!this.isEditing || this._copyDone || this.inCompositionMode) {
|
|
152
|
+
this._copyDone = false;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (e.keyCode in this.ctrlKeysMapUp && (e.ctrlKey || e.metaKey)) {
|
|
156
|
+
(
|
|
157
|
+
this[this.ctrlKeysMapUp[e.keyCode] as keyof this] as (
|
|
158
|
+
arg: KeyboardEvent,
|
|
159
|
+
) => void
|
|
160
|
+
)(e);
|
|
161
|
+
} else {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
e.stopImmediatePropagation();
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
this.canvas && this.canvas.requestRenderAll();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handles onInput event
|
|
171
|
+
* @param {Event} e Event object
|
|
172
|
+
*/
|
|
173
|
+
onInput(this: this & { hiddenTextarea: HTMLTextAreaElement }, e: Event) {
|
|
174
|
+
const fromPaste = this.fromPaste;
|
|
175
|
+
const { value, selectionStart, selectionEnd } = this.hiddenTextarea;
|
|
176
|
+
this.fromPaste = false;
|
|
177
|
+
e && e.stopPropagation();
|
|
178
|
+
if (!this.isEditing) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Debug log to track the double keypress issue
|
|
183
|
+
console.log('🔤 onInput debug:', {
|
|
184
|
+
fabricText: this.text,
|
|
185
|
+
textareaValue: value,
|
|
186
|
+
fabricSelection: { start: this.selectionStart, end: this.selectionEnd },
|
|
187
|
+
textareaSelection: { start: selectionStart, end: selectionEnd },
|
|
188
|
+
fromPaste,
|
|
189
|
+
inComposition: this.inCompositionMode
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Immediate sync for simple character replacement - fix for double keypress issue
|
|
193
|
+
if (this.text !== value && !this.inCompositionMode) {
|
|
194
|
+
console.log('🔤 Immediate sync: fabric text differs from textarea, syncing immediately');
|
|
195
|
+
console.log('🔤 Before sync - fabric text:', this.text);
|
|
196
|
+
console.log('🔤 Before sync - textarea value:', value);
|
|
197
|
+
console.log('🔤 fromPaste:', fromPaste);
|
|
198
|
+
|
|
199
|
+
// Clear all relevant caches that might prevent visual updates
|
|
200
|
+
this.cursorOffsetCache = {};
|
|
201
|
+
(this as any)._browserWrapCache = null;
|
|
202
|
+
(this as any)._lastDimensionState = null;
|
|
203
|
+
this._forceClearCache = true;
|
|
204
|
+
|
|
205
|
+
console.log('🔤 Cleared all caches');
|
|
206
|
+
|
|
207
|
+
// Use the same logic as updateAndFire but immediately
|
|
208
|
+
this.updateFromTextArea();
|
|
209
|
+
this.fire(CHANGED);
|
|
210
|
+
if (this.canvas) {
|
|
211
|
+
this.canvas.fire('text:changed', { target: this as unknown as IText });
|
|
212
|
+
// ONLY use synchronous rendering to avoid race conditions
|
|
213
|
+
// Remove requestRenderAll() which queues for next animation frame
|
|
214
|
+
this.canvas.renderAll();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log('🔤 After updateFromTextArea - fabric text:', this.text);
|
|
218
|
+
console.log('🔤 Sync complete, caches cleared, synchronous render only');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const updateAndFire = () => {
|
|
223
|
+
this.updateFromTextArea();
|
|
224
|
+
this.fire(CHANGED);
|
|
225
|
+
if (this.canvas) {
|
|
226
|
+
this.canvas.fire('text:changed', { target: this as unknown as IText });
|
|
227
|
+
this.canvas.requestRenderAll();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
if (this.hiddenTextarea.value === '') {
|
|
231
|
+
this.styles = {};
|
|
232
|
+
updateAndFire();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// decisions about style changes.
|
|
236
|
+
const nextText = this._splitTextIntoLines(value).graphemeText,
|
|
237
|
+
charCount = this._text.length,
|
|
238
|
+
nextCharCount = nextText.length,
|
|
239
|
+
_selectionStart = this.selectionStart,
|
|
240
|
+
_selectionEnd = this.selectionEnd,
|
|
241
|
+
selection = _selectionStart !== _selectionEnd;
|
|
242
|
+
let copiedStyle: TextStyleDeclaration[] | undefined,
|
|
243
|
+
removedText,
|
|
244
|
+
charDiff = nextCharCount - charCount,
|
|
245
|
+
removeFrom,
|
|
246
|
+
removeTo;
|
|
247
|
+
|
|
248
|
+
const textareaSelection = this.fromStringToGraphemeSelection(
|
|
249
|
+
selectionStart,
|
|
250
|
+
selectionEnd,
|
|
251
|
+
value,
|
|
252
|
+
);
|
|
253
|
+
const backDelete = _selectionStart > textareaSelection.selectionStart;
|
|
254
|
+
|
|
255
|
+
if (selection) {
|
|
256
|
+
removedText = this._text.slice(_selectionStart, _selectionEnd);
|
|
257
|
+
charDiff += _selectionEnd - _selectionStart;
|
|
258
|
+
} else if (nextCharCount < charCount) {
|
|
259
|
+
if (backDelete) {
|
|
260
|
+
removedText = this._text.slice(_selectionEnd + charDiff, _selectionEnd);
|
|
261
|
+
} else {
|
|
262
|
+
removedText = this._text.slice(
|
|
263
|
+
_selectionStart,
|
|
264
|
+
_selectionStart - charDiff,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const insertedText = nextText.slice(
|
|
269
|
+
textareaSelection.selectionEnd - charDiff,
|
|
270
|
+
textareaSelection.selectionEnd,
|
|
271
|
+
);
|
|
272
|
+
if (removedText && removedText.length) {
|
|
273
|
+
if (insertedText.length) {
|
|
274
|
+
// let's copy some style before deleting.
|
|
275
|
+
// we want to copy the style before the cursor OR the style at the cursor if selection
|
|
276
|
+
// is bigger than 0.
|
|
277
|
+
copiedStyle = this.getSelectionStyles(
|
|
278
|
+
_selectionStart,
|
|
279
|
+
_selectionStart + 1,
|
|
280
|
+
false,
|
|
281
|
+
);
|
|
282
|
+
// now duplicate the style one for each inserted text.
|
|
283
|
+
copiedStyle = insertedText.map(
|
|
284
|
+
() =>
|
|
285
|
+
// this return an array of references, but that is fine since we are
|
|
286
|
+
// copying the style later.
|
|
287
|
+
copiedStyle![0],
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (selection) {
|
|
291
|
+
removeFrom = _selectionStart;
|
|
292
|
+
removeTo = _selectionEnd;
|
|
293
|
+
} else if (backDelete) {
|
|
294
|
+
// detect differences between forwardDelete and backDelete
|
|
295
|
+
removeFrom = _selectionEnd - removedText.length;
|
|
296
|
+
removeTo = _selectionEnd;
|
|
297
|
+
} else {
|
|
298
|
+
removeFrom = _selectionEnd;
|
|
299
|
+
removeTo = _selectionEnd + removedText.length;
|
|
300
|
+
}
|
|
301
|
+
this.removeStyleFromTo(removeFrom, removeTo);
|
|
302
|
+
}
|
|
303
|
+
if (insertedText.length) {
|
|
304
|
+
const { copyPasteData } = getEnv();
|
|
305
|
+
if (
|
|
306
|
+
fromPaste &&
|
|
307
|
+
insertedText.join('') === copyPasteData.copiedText &&
|
|
308
|
+
!config.disableStyleCopyPaste
|
|
309
|
+
) {
|
|
310
|
+
copiedStyle = copyPasteData.copiedTextStyle;
|
|
311
|
+
}
|
|
312
|
+
this.insertNewStyleBlock(insertedText, _selectionStart, copiedStyle);
|
|
313
|
+
}
|
|
314
|
+
updateAndFire();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Composition start
|
|
319
|
+
*/
|
|
320
|
+
onCompositionStart() {
|
|
321
|
+
this.inCompositionMode = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Composition end
|
|
326
|
+
*/
|
|
327
|
+
onCompositionEnd() {
|
|
328
|
+
this.inCompositionMode = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
onCompositionUpdate({ target }: CompositionEvent) {
|
|
332
|
+
const { selectionStart, selectionEnd } = target as HTMLTextAreaElement;
|
|
333
|
+
this.compositionStart = selectionStart;
|
|
334
|
+
this.compositionEnd = selectionEnd;
|
|
335
|
+
this.updateTextareaPosition();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Copies selected text
|
|
340
|
+
*/
|
|
341
|
+
copy() {
|
|
342
|
+
if (this.selectionStart === this.selectionEnd) {
|
|
343
|
+
//do not cut-copy if no selection
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const { copyPasteData } = getEnv();
|
|
347
|
+
copyPasteData.copiedText = this.getSelectedText();
|
|
348
|
+
if (!config.disableStyleCopyPaste) {
|
|
349
|
+
copyPasteData.copiedTextStyle = this.getSelectionStyles(
|
|
350
|
+
this.selectionStart,
|
|
351
|
+
this.selectionEnd,
|
|
352
|
+
true,
|
|
353
|
+
);
|
|
354
|
+
} else {
|
|
355
|
+
copyPasteData.copiedTextStyle = undefined;
|
|
356
|
+
}
|
|
357
|
+
this._copyDone = true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Pastes text
|
|
362
|
+
*/
|
|
363
|
+
paste() {
|
|
364
|
+
this.fromPaste = true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Finds the width in pixels before the cursor on the same line
|
|
369
|
+
* @private
|
|
370
|
+
* @param {Number} lineIndex
|
|
371
|
+
* @param {Number} charIndex
|
|
372
|
+
* @return {Number} widthBeforeCursor width before cursor
|
|
373
|
+
*/
|
|
374
|
+
_getWidthBeforeCursor(lineIndex: number, charIndex: number): number {
|
|
375
|
+
let widthBeforeCursor = this._getLineLeftOffset(lineIndex),
|
|
376
|
+
bound;
|
|
377
|
+
|
|
378
|
+
if (charIndex > 0) {
|
|
379
|
+
bound = this.__charBounds[lineIndex][charIndex - 1];
|
|
380
|
+
widthBeforeCursor += bound.left + bound.width;
|
|
381
|
+
}
|
|
382
|
+
return widthBeforeCursor;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Gets start offset of a selection
|
|
387
|
+
* @param {KeyboardEvent} e Event object
|
|
388
|
+
* @param {Boolean} isRight
|
|
389
|
+
* @return {Number}
|
|
390
|
+
*/
|
|
391
|
+
getDownCursorOffset(e: KeyboardEvent, isRight: boolean): number {
|
|
392
|
+
const selectionProp = this._getSelectionForOffset(e, isRight),
|
|
393
|
+
cursorLocation = this.get2DCursorLocation(selectionProp),
|
|
394
|
+
lineIndex = cursorLocation.lineIndex;
|
|
395
|
+
// if on last line, down cursor goes to end of line
|
|
396
|
+
if (
|
|
397
|
+
lineIndex === this._textLines.length - 1 ||
|
|
398
|
+
e.metaKey ||
|
|
399
|
+
e.keyCode === 34
|
|
400
|
+
) {
|
|
401
|
+
// move to the end of a text
|
|
402
|
+
return this._text.length - selectionProp;
|
|
403
|
+
}
|
|
404
|
+
const charIndex = cursorLocation.charIndex,
|
|
405
|
+
widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
|
|
406
|
+
indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
|
|
407
|
+
textAfterCursor = this._textLines[lineIndex].slice(charIndex);
|
|
408
|
+
return (
|
|
409
|
+
textAfterCursor.length +
|
|
410
|
+
indexOnOtherLine +
|
|
411
|
+
1 +
|
|
412
|
+
this.missingNewlineOffset(lineIndex)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* private
|
|
418
|
+
* Helps finding if the offset should be counted from Start or End
|
|
419
|
+
* @param {KeyboardEvent} e Event object
|
|
420
|
+
* @param {Boolean} isRight
|
|
421
|
+
* @return {Number}
|
|
422
|
+
*/
|
|
423
|
+
_getSelectionForOffset(e: KeyboardEvent, isRight: boolean): number {
|
|
424
|
+
if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
|
|
425
|
+
return this.selectionEnd;
|
|
426
|
+
} else {
|
|
427
|
+
return this.selectionStart;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @param {KeyboardEvent} e Event object
|
|
433
|
+
* @param {Boolean} isRight
|
|
434
|
+
* @return {Number}
|
|
435
|
+
*/
|
|
436
|
+
getUpCursorOffset(e: KeyboardEvent, isRight: boolean): number {
|
|
437
|
+
const selectionProp = this._getSelectionForOffset(e, isRight),
|
|
438
|
+
cursorLocation = this.get2DCursorLocation(selectionProp),
|
|
439
|
+
lineIndex = cursorLocation.lineIndex;
|
|
440
|
+
if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
|
|
441
|
+
// if on first line, up cursor goes to start of line
|
|
442
|
+
return -selectionProp;
|
|
443
|
+
}
|
|
444
|
+
const charIndex = cursorLocation.charIndex,
|
|
445
|
+
widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
|
|
446
|
+
indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor),
|
|
447
|
+
textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),
|
|
448
|
+
missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1);
|
|
449
|
+
// return a negative offset
|
|
450
|
+
return (
|
|
451
|
+
-this._textLines[lineIndex - 1].length +
|
|
452
|
+
indexOnOtherLine -
|
|
453
|
+
textBeforeCursor.length +
|
|
454
|
+
(1 - missingNewlineOffset)
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* for a given width it founds the matching character.
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
_getIndexOnLine(lineIndex: number, width: number) {
|
|
463
|
+
const line = this._textLines[lineIndex],
|
|
464
|
+
lineLeftOffset = this._getLineLeftOffset(lineIndex);
|
|
465
|
+
let widthOfCharsOnLine = lineLeftOffset,
|
|
466
|
+
indexOnLine = 0,
|
|
467
|
+
charWidth,
|
|
468
|
+
foundMatch;
|
|
469
|
+
|
|
470
|
+
for (let j = 0, jlen = line.length; j < jlen; j++) {
|
|
471
|
+
charWidth = this.__charBounds[lineIndex][j].width;
|
|
472
|
+
widthOfCharsOnLine += charWidth;
|
|
473
|
+
if (widthOfCharsOnLine > width) {
|
|
474
|
+
foundMatch = true;
|
|
475
|
+
const leftEdge = widthOfCharsOnLine - charWidth,
|
|
476
|
+
rightEdge = widthOfCharsOnLine,
|
|
477
|
+
offsetFromLeftEdge = Math.abs(leftEdge - width),
|
|
478
|
+
offsetFromRightEdge = Math.abs(rightEdge - width);
|
|
479
|
+
|
|
480
|
+
indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : j - 1;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// reached end
|
|
486
|
+
if (!foundMatch) {
|
|
487
|
+
indexOnLine = line.length - 1;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return indexOnLine;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Moves cursor down
|
|
495
|
+
* @param {KeyboardEvent} e Event object
|
|
496
|
+
*/
|
|
497
|
+
moveCursorDown(e: KeyboardEvent) {
|
|
498
|
+
if (
|
|
499
|
+
this.selectionStart >= this._text.length &&
|
|
500
|
+
this.selectionEnd >= this._text.length
|
|
501
|
+
) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this._moveCursorUpOrDown('Down', e);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Moves cursor up
|
|
509
|
+
* @param {KeyboardEvent} e Event object
|
|
510
|
+
*/
|
|
511
|
+
moveCursorUp(e: KeyboardEvent) {
|
|
512
|
+
if (this.selectionStart === 0 && this.selectionEnd === 0) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
this._moveCursorUpOrDown('Up', e);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Moves cursor up or down, fires the events
|
|
520
|
+
* @param {String} direction 'Up' or 'Down'
|
|
521
|
+
* @param {KeyboardEvent} e Event object
|
|
522
|
+
*/
|
|
523
|
+
_moveCursorUpOrDown(direction: 'Up' | 'Down', e: KeyboardEvent) {
|
|
524
|
+
const offset = this[`get${direction}CursorOffset`](
|
|
525
|
+
e,
|
|
526
|
+
this._selectionDirection === RIGHT,
|
|
527
|
+
);
|
|
528
|
+
if (e.shiftKey) {
|
|
529
|
+
this.moveCursorWithShift(offset);
|
|
530
|
+
} else {
|
|
531
|
+
this.moveCursorWithoutShift(offset);
|
|
532
|
+
}
|
|
533
|
+
if (offset !== 0) {
|
|
534
|
+
const max = this.text.length;
|
|
535
|
+
this.selectionStart = capValue(0, this.selectionStart, max);
|
|
536
|
+
this.selectionEnd = capValue(0, this.selectionEnd, max);
|
|
537
|
+
// TODO fix: abort and init should be an alternative depending
|
|
538
|
+
// on selectionStart/End being equal or different
|
|
539
|
+
this.abortCursorAnimation();
|
|
540
|
+
this.initDelayedCursor();
|
|
541
|
+
this._fireSelectionChanged();
|
|
542
|
+
this._updateTextarea();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Moves cursor with shift
|
|
548
|
+
* @param {Number} offset
|
|
549
|
+
*/
|
|
550
|
+
moveCursorWithShift(offset: number) {
|
|
551
|
+
const newSelection =
|
|
552
|
+
this._selectionDirection === LEFT
|
|
553
|
+
? this.selectionStart + offset
|
|
554
|
+
: this.selectionEnd + offset;
|
|
555
|
+
this.setSelectionStartEndWithShift(
|
|
556
|
+
this.selectionStart,
|
|
557
|
+
this.selectionEnd,
|
|
558
|
+
newSelection,
|
|
559
|
+
);
|
|
560
|
+
return offset !== 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Moves cursor up without shift
|
|
565
|
+
* @param {Number} offset
|
|
566
|
+
*/
|
|
567
|
+
moveCursorWithoutShift(offset: number) {
|
|
568
|
+
if (offset < 0) {
|
|
569
|
+
this.selectionStart += offset;
|
|
570
|
+
this.selectionEnd = this.selectionStart;
|
|
571
|
+
} else {
|
|
572
|
+
this.selectionEnd += offset;
|
|
573
|
+
this.selectionStart = this.selectionEnd;
|
|
574
|
+
}
|
|
575
|
+
return offset !== 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Moves cursor left
|
|
580
|
+
* @param {KeyboardEvent} e Event object
|
|
581
|
+
*/
|
|
582
|
+
moveCursorLeft(e: KeyboardEvent) {
|
|
583
|
+
if (this.selectionStart === 0 && this.selectionEnd === 0) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
this._moveCursorLeftOrRight('Left', e);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* @private
|
|
591
|
+
* @return {Boolean} true if a change happened
|
|
592
|
+
*
|
|
593
|
+
* @todo refactor not to use method name composition
|
|
594
|
+
*/
|
|
595
|
+
_move(
|
|
596
|
+
e: KeyboardEvent,
|
|
597
|
+
prop: 'selectionStart' | 'selectionEnd',
|
|
598
|
+
direction: 'Left' | 'Right',
|
|
599
|
+
): boolean {
|
|
600
|
+
let newValue: number | undefined;
|
|
601
|
+
if (e.altKey) {
|
|
602
|
+
newValue = this[`findWordBoundary${direction}`](this[prop]);
|
|
603
|
+
} else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36) {
|
|
604
|
+
newValue = this[`findLineBoundary${direction}`](this[prop]);
|
|
605
|
+
} else {
|
|
606
|
+
this[prop] += direction === 'Left' ? -1 : 1;
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
if (typeof newValue !== 'undefined' && this[prop] !== newValue) {
|
|
610
|
+
this[prop] = newValue;
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
_moveLeft(e: KeyboardEvent, prop: 'selectionStart' | 'selectionEnd') {
|
|
620
|
+
return this._move(e, prop, 'Left');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
_moveRight(e: KeyboardEvent, prop: 'selectionStart' | 'selectionEnd') {
|
|
627
|
+
return this._move(e, prop, 'Right');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Moves cursor left without keeping selection
|
|
632
|
+
* @param {KeyboardEvent} e
|
|
633
|
+
*/
|
|
634
|
+
moveCursorLeftWithoutShift(e: KeyboardEvent) {
|
|
635
|
+
let change = true;
|
|
636
|
+
this._selectionDirection = LEFT;
|
|
637
|
+
|
|
638
|
+
// only move cursor when there is no selection,
|
|
639
|
+
// otherwise we discard it, and leave cursor on same place
|
|
640
|
+
if (
|
|
641
|
+
this.selectionEnd === this.selectionStart &&
|
|
642
|
+
this.selectionStart !== 0
|
|
643
|
+
) {
|
|
644
|
+
change = this._moveLeft(e, 'selectionStart');
|
|
645
|
+
}
|
|
646
|
+
this.selectionEnd = this.selectionStart;
|
|
647
|
+
return change;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Moves cursor left while keeping selection
|
|
652
|
+
* @param {KeyboardEvent} e
|
|
653
|
+
*/
|
|
654
|
+
moveCursorLeftWithShift(e: KeyboardEvent) {
|
|
655
|
+
if (
|
|
656
|
+
this._selectionDirection === RIGHT &&
|
|
657
|
+
this.selectionStart !== this.selectionEnd
|
|
658
|
+
) {
|
|
659
|
+
return this._moveLeft(e, 'selectionEnd');
|
|
660
|
+
} else if (this.selectionStart !== 0) {
|
|
661
|
+
this._selectionDirection = LEFT;
|
|
662
|
+
return this._moveLeft(e, 'selectionStart');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Moves cursor right
|
|
668
|
+
* @param {KeyboardEvent} e Event object
|
|
669
|
+
*/
|
|
670
|
+
moveCursorRight(e: KeyboardEvent) {
|
|
671
|
+
if (
|
|
672
|
+
this.selectionStart >= this._text.length &&
|
|
673
|
+
this.selectionEnd >= this._text.length
|
|
674
|
+
) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this._moveCursorLeftOrRight('Right', e);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Moves cursor right or Left, fires event
|
|
682
|
+
* @param {String} direction 'Left', 'Right'
|
|
683
|
+
* @param {KeyboardEvent} e Event object
|
|
684
|
+
*/
|
|
685
|
+
_moveCursorLeftOrRight(direction: 'Left' | 'Right', e: KeyboardEvent) {
|
|
686
|
+
const actionName = `moveCursor${direction}${
|
|
687
|
+
e.shiftKey ? 'WithShift' : 'WithoutShift'
|
|
688
|
+
}` as const;
|
|
689
|
+
this._currentCursorOpacity = 1;
|
|
690
|
+
if (this[actionName](e)) {
|
|
691
|
+
// TODO fix: abort and init should be an alternative depending
|
|
692
|
+
// on selectionStart/End being equal or different
|
|
693
|
+
this.abortCursorAnimation();
|
|
694
|
+
this.initDelayedCursor();
|
|
695
|
+
this._fireSelectionChanged();
|
|
696
|
+
this._updateTextarea();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Moves cursor right while keeping selection
|
|
702
|
+
* @param {KeyboardEvent} e
|
|
703
|
+
*/
|
|
704
|
+
moveCursorRightWithShift(e: KeyboardEvent) {
|
|
705
|
+
if (
|
|
706
|
+
this._selectionDirection === LEFT &&
|
|
707
|
+
this.selectionStart !== this.selectionEnd
|
|
708
|
+
) {
|
|
709
|
+
return this._moveRight(e, 'selectionStart');
|
|
710
|
+
} else if (this.selectionEnd !== this._text.length) {
|
|
711
|
+
this._selectionDirection = RIGHT;
|
|
712
|
+
return this._moveRight(e, 'selectionEnd');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Moves cursor right without keeping selection
|
|
718
|
+
* @param {KeyboardEvent} e Event object
|
|
719
|
+
*/
|
|
720
|
+
moveCursorRightWithoutShift(e: KeyboardEvent) {
|
|
721
|
+
let changed = true;
|
|
722
|
+
this._selectionDirection = RIGHT;
|
|
723
|
+
|
|
724
|
+
if (this.selectionStart === this.selectionEnd) {
|
|
725
|
+
changed = this._moveRight(e, 'selectionStart');
|
|
726
|
+
this.selectionEnd = this.selectionStart;
|
|
727
|
+
} else {
|
|
728
|
+
this.selectionStart = this.selectionEnd;
|
|
729
|
+
}
|
|
730
|
+
return changed;
|
|
731
|
+
}
|
|
732
|
+
}
|