@nasser-sw/fabric 7.0.1-beta16 → 7.0.1-beta17
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/.claude/settings.local.json +7 -0
- package/dist/index.js +1982 -649
- 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 +1982 -649
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +1982 -649
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +1982 -649
- 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/IText.d.ts +31 -6
- package/dist/src/shapes/IText/IText.d.ts.map +1 -1
- package/dist/src/shapes/IText/IText.min.mjs +1 -1
- package/dist/src/shapes/IText/IText.min.mjs.map +1 -1
- package/dist/src/shapes/IText/IText.mjs +495 -126
- package/dist/src/shapes/IText/IText.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.d.ts +12 -0
- 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 +127 -36
- package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.mjs +21 -4
- package/dist/src/shapes/IText/ITextClickBehavior.mjs.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 +17 -21
- package/dist/src/shapes/IText/ITextKeyBehavior.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts +69 -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 +374 -60
- package/dist/src/shapes/Text/Text.mjs.map +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 +2 -1
- package/dist/src/shapes/Text/constants.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +8 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +406 -63
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/text/hitTest.min.mjs +1 -1
- package/dist/src/text/hitTest.min.mjs.map +1 -1
- package/dist/src/text/hitTest.mjs +1 -198
- package/dist/src/text/hitTest.mjs.map +1 -1
- package/dist/src/text/layout.min.mjs +1 -1
- package/dist/src/text/layout.min.mjs.map +1 -1
- package/dist/src/text/layout.mjs +122 -5
- package/dist/src/text/layout.mjs.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +132 -142
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/text/unicode.d.ts +28 -0
- package/dist/src/text/unicode.d.ts.map +1 -1
- package/dist/src/text/unicode.min.mjs +1 -1
- package/dist/src/text/unicode.min.mjs.map +1 -1
- package/dist/src/text/unicode.mjs +294 -1
- package/dist/src/text/unicode.mjs.map +1 -1
- package/dist-extensions/src/shapes/IText/IText.d.ts +31 -6
- package/dist-extensions/src/shapes/IText/IText.d.ts.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts +12 -0
- package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts +69 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/constants.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +8 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/text/unicode.d.ts +28 -0
- package/dist-extensions/src/text/unicode.d.ts.map +1 -1
- package/package.json +164 -164
- package/rtl-debug.html +358 -200
- package/src/shapes/IText/IText.ts +524 -110
- package/src/shapes/IText/ITextBehavior.ts +174 -80
- package/src/shapes/IText/ITextClickBehavior.ts +20 -6
- package/src/shapes/IText/ITextKeyBehavior.ts +15 -15
- package/src/shapes/Text/Text.ts +488 -107
- package/src/shapes/Text/constants.ts +4 -2
- package/src/shapes/Textbox.ts +414 -65
- package/src/text/layout.ts +150 -23
- package/src/text/overlayEditor.ts +148 -148
- package/src/text/unicode.ts +177 -2
|
@@ -27,7 +27,11 @@ import { extractLinesFromDOM, storeBrowserLines } from '../../text/browserLines'
|
|
|
27
27
|
* - `\-` Matches a "-" character (char code 45).
|
|
28
28
|
*/
|
|
29
29
|
// eslint-disable-next-line no-useless-escape
|
|
30
|
-
|
|
30
|
+
// Word boundary characters for Latin, Arabic, and Hebrew
|
|
31
|
+
// Latin: space, newline, punctuation
|
|
32
|
+
// Arabic: ، (comma U+060C), ؛ (semicolon U+061B), ؟ (question U+061F), ۔ (full stop U+06D4), ـ (tatweel U+0640)
|
|
33
|
+
// Hebrew: ׃ (sof pasuq U+05C3), ״ (gershayim U+05F4)
|
|
34
|
+
const reNonWord = /[ \n\.,;!\?\-\u060C\u061B\u061F\u06D4\u0640\u05C3\u05F4\u2000-\u206F]/;
|
|
31
35
|
|
|
32
36
|
export type ITextEvents = ObjectEvents & {
|
|
33
37
|
'selection:changed': never;
|
|
@@ -325,12 +329,102 @@ export abstract class ITextBehavior<
|
|
|
325
329
|
|
|
326
330
|
/**
|
|
327
331
|
* Finds index corresponding to beginning or end of a word
|
|
332
|
+
* Uses Intl.Segmenter for proper Unicode word segmentation when available,
|
|
333
|
+
* falls back to regex-based detection for older browsers.
|
|
328
334
|
* @param {Number} selectionStart Index of a character
|
|
329
335
|
* @param {Number} direction 1 or -1
|
|
330
336
|
* @return {Number} Index of the beginning or end of a word
|
|
331
337
|
*/
|
|
332
338
|
searchWordBoundary(selectionStart: number, direction: 1 | -1): number {
|
|
333
|
-
|
|
339
|
+
// Try to use Intl.Segmenter for proper Unicode word segmentation
|
|
340
|
+
if (typeof Intl !== 'undefined' && (Intl as any).Segmenter) {
|
|
341
|
+
return this._searchWordBoundaryWithSegmenter(selectionStart, direction);
|
|
342
|
+
}
|
|
343
|
+
// Fallback to regex-based detection
|
|
344
|
+
return this._searchWordBoundaryWithRegex(selectionStart, direction);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Word boundary search using Intl.Segmenter (proper Unicode support)
|
|
349
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
350
|
+
*/
|
|
351
|
+
private _searchWordBoundaryWithSegmenter(selectionStart: number, direction: 1 | -1): number {
|
|
352
|
+
// Use original text (without kashida) since indices are in original text space
|
|
353
|
+
const originalText = this.text;
|
|
354
|
+
const SegmenterClass = (Intl as any).Segmenter;
|
|
355
|
+
const segmenter = new SegmenterClass(undefined, { granularity: 'word' });
|
|
356
|
+
const segments = Array.from(segmenter.segment(originalText)) as Array<{
|
|
357
|
+
segment: string;
|
|
358
|
+
index: number;
|
|
359
|
+
isWordLike: boolean;
|
|
360
|
+
}>;
|
|
361
|
+
|
|
362
|
+
if (segments.length === 0) {
|
|
363
|
+
return direction === -1 ? 0 : originalText.length;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Find the segment containing the cursor position
|
|
367
|
+
let currentSegmentIdx = 0;
|
|
368
|
+
for (let i = 0; i < segments.length; i++) {
|
|
369
|
+
const seg = segments[i];
|
|
370
|
+
if (selectionStart >= seg.index && selectionStart < seg.index + seg.segment.length) {
|
|
371
|
+
currentSegmentIdx = i;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
if (selectionStart >= seg.index + seg.segment.length) {
|
|
375
|
+
currentSegmentIdx = i;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Find word boundaries
|
|
380
|
+
if (direction === -1) {
|
|
381
|
+
// Search backwards for word start
|
|
382
|
+
let targetIdx = currentSegmentIdx;
|
|
383
|
+
|
|
384
|
+
// If cursor is at the start of a segment, look at previous segment
|
|
385
|
+
if (selectionStart === segments[targetIdx].index && targetIdx > 0) {
|
|
386
|
+
targetIdx--;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Skip non-word segments
|
|
390
|
+
while (targetIdx > 0 && !segments[targetIdx].isWordLike) {
|
|
391
|
+
targetIdx--;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Return the start of the word segment
|
|
395
|
+
if (segments[targetIdx].isWordLike) {
|
|
396
|
+
return segments[targetIdx].index;
|
|
397
|
+
}
|
|
398
|
+
return 0;
|
|
399
|
+
} else {
|
|
400
|
+
// Search forwards for word end
|
|
401
|
+
let targetIdx = currentSegmentIdx;
|
|
402
|
+
|
|
403
|
+
// If we're in a word, find its end
|
|
404
|
+
if (segments[targetIdx].isWordLike) {
|
|
405
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Skip non-word segments to find next word
|
|
409
|
+
while (targetIdx < segments.length && !segments[targetIdx].isWordLike) {
|
|
410
|
+
targetIdx++;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Return the end of the next word segment
|
|
414
|
+
if (targetIdx < segments.length && segments[targetIdx].isWordLike) {
|
|
415
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
416
|
+
}
|
|
417
|
+
return originalText.length;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Word boundary search using regex (fallback for older browsers)
|
|
423
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
424
|
+
*/
|
|
425
|
+
private _searchWordBoundaryWithRegex(selectionStart: number, direction: 1 | -1): number {
|
|
426
|
+
// Use original text as an array of characters
|
|
427
|
+
const text = Array.from(this.text);
|
|
334
428
|
// if we land on a space we move the cursor backwards
|
|
335
429
|
// if we are searching boundary end we move the cursor backwards ONLY if we don't land on a line break
|
|
336
430
|
let index =
|
|
@@ -455,84 +549,84 @@ export abstract class ITextBehavior<
|
|
|
455
549
|
});
|
|
456
550
|
}
|
|
457
551
|
|
|
458
|
-
/**
|
|
459
|
-
* Commit overlay editing changes
|
|
460
|
-
*/
|
|
461
|
-
private commitOverlayEdit(text: string) {
|
|
462
|
-
|
|
463
|
-
// Preserve geometry to avoid nudge when layout recalculates
|
|
464
|
-
const prevLeft = this.left;
|
|
465
|
-
const prevTop = this.top;
|
|
466
|
-
const prevWidth = this.get('width');
|
|
467
|
-
const prevMinWidth = (this as any).dynamicMinWidth;
|
|
468
|
-
const prevUsingBrowserWrap = (this as any)._usingBrowserWrapping;
|
|
469
|
-
const hadLock = (this as any).lockDynamicMinWidth;
|
|
470
|
-
(this as any).lockDynamicMinWidth = true;
|
|
471
|
-
const countKashida = (val?: string) => (val ? (val.match(/\u0640/g) || []).length : 0);
|
|
472
|
-
console.log('[OverlayCommit] pre-layout', {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const overlayEditor = (this as any).__overlayEditor;
|
|
484
|
-
|
|
485
|
-
if (overlayEditor) {
|
|
486
|
-
// Extract browser lines for pixel-perfect rendering
|
|
487
|
-
try {
|
|
552
|
+
/**
|
|
553
|
+
* Commit overlay editing changes
|
|
554
|
+
*/
|
|
555
|
+
private commitOverlayEdit(text: string) {
|
|
556
|
+
|
|
557
|
+
// Preserve geometry to avoid nudge when layout recalculates
|
|
558
|
+
const prevLeft = this.left;
|
|
559
|
+
const prevTop = this.top;
|
|
560
|
+
const prevWidth = this.get('width');
|
|
561
|
+
const prevMinWidth = (this as any).dynamicMinWidth;
|
|
562
|
+
const prevUsingBrowserWrap = (this as any)._usingBrowserWrapping;
|
|
563
|
+
const hadLock = (this as any).lockDynamicMinWidth;
|
|
564
|
+
(this as any).lockDynamicMinWidth = true;
|
|
565
|
+
const countKashida = (val?: string) => (val ? (val.match(/\u0640/g) || []).length : 0);
|
|
566
|
+
// console.log('[OverlayCommit] pre-layout', {
|
|
567
|
+
// textLength: text?.length,
|
|
568
|
+
// kashidas: countKashida(text),
|
|
569
|
+
// prevWidth,
|
|
570
|
+
// prevMinWidth,
|
|
571
|
+
// prevUsingBrowserWrap,
|
|
572
|
+
// hadLock,
|
|
573
|
+
// dir: (this as any).direction,
|
|
574
|
+
// align: (this as any).textAlign,
|
|
575
|
+
// });
|
|
576
|
+
|
|
577
|
+
const overlayEditor = (this as any).__overlayEditor;
|
|
578
|
+
|
|
579
|
+
if (overlayEditor) {
|
|
580
|
+
// Extract browser lines for pixel-perfect rendering
|
|
581
|
+
try {
|
|
488
582
|
const result = extractLinesFromDOM(overlayEditor.textareaElement);
|
|
489
583
|
storeBrowserLines(this, result.lines);
|
|
490
584
|
} catch (error) {
|
|
491
|
-
console.warn('Failed to extract browser lines:', error);
|
|
585
|
+
// console.warn('Failed to extract browser lines:', error);
|
|
492
586
|
}
|
|
493
587
|
}
|
|
494
|
-
|
|
495
|
-
// Update text content and trigger layout recalculation
|
|
496
|
-
this.text = text;
|
|
497
|
-
// Freeze dynamic min width during this layout pass so width doesn't shrink/expand on commit
|
|
498
|
-
if (prevMinWidth !== undefined) {
|
|
499
|
-
(this as any).dynamicMinWidth = Math.max(prevMinWidth || 0, prevWidth || 0);
|
|
500
|
-
}
|
|
501
|
-
// Keep browser wrapping flag stable
|
|
502
|
-
if (prevUsingBrowserWrap !== undefined) {
|
|
503
|
-
(this as any)._usingBrowserWrapping = prevUsingBrowserWrap;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
this.dirty = true;
|
|
507
|
-
this.initDimensions();
|
|
508
|
-
console.log('[OverlayCommit] post-layout', {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
});
|
|
517
|
-
// Restore geometry after layout so the object doesn't drift
|
|
518
|
-
this.set({
|
|
519
|
-
left: prevLeft,
|
|
520
|
-
top: prevTop,
|
|
521
|
-
width: prevWidth,
|
|
522
|
-
});
|
|
523
|
-
this.setCoords();
|
|
524
|
-
this.exitEditing();
|
|
525
|
-
(this as any).lockDynamicMinWidth = hadLock;
|
|
526
|
-
console.log('[OverlayCommit] final', {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
533
|
-
this.fire('changed');
|
|
534
|
-
this.canvas && this.canvas.requestRenderAll();
|
|
535
|
-
}
|
|
588
|
+
|
|
589
|
+
// Update text content and trigger layout recalculation
|
|
590
|
+
this.text = text;
|
|
591
|
+
// Freeze dynamic min width during this layout pass so width doesn't shrink/expand on commit
|
|
592
|
+
if (prevMinWidth !== undefined) {
|
|
593
|
+
(this as any).dynamicMinWidth = Math.max(prevMinWidth || 0, prevWidth || 0);
|
|
594
|
+
}
|
|
595
|
+
// Keep browser wrapping flag stable
|
|
596
|
+
if (prevUsingBrowserWrap !== undefined) {
|
|
597
|
+
(this as any)._usingBrowserWrapping = prevUsingBrowserWrap;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.dirty = true;
|
|
601
|
+
this.initDimensions();
|
|
602
|
+
// console.log('[OverlayCommit] post-layout', {
|
|
603
|
+
// width: this.get('width'),
|
|
604
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
605
|
+
// usingBrowserWrap: (this as any)._usingBrowserWrapping,
|
|
606
|
+
// lockDynamicMinWidth: (this as any).lockDynamicMinWidth,
|
|
607
|
+
// kashidas: countKashida(this.text),
|
|
608
|
+
// left: this.left,
|
|
609
|
+
// top: this.top,
|
|
610
|
+
// });
|
|
611
|
+
// Restore geometry after layout so the object doesn't drift
|
|
612
|
+
this.set({
|
|
613
|
+
left: prevLeft,
|
|
614
|
+
top: prevTop,
|
|
615
|
+
width: prevWidth,
|
|
616
|
+
});
|
|
617
|
+
this.setCoords();
|
|
618
|
+
this.exitEditing();
|
|
619
|
+
(this as any).lockDynamicMinWidth = hadLock;
|
|
620
|
+
// console.log('[OverlayCommit] final', {
|
|
621
|
+
// width: this.get('width'),
|
|
622
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
623
|
+
// lockRestored: hadLock,
|
|
624
|
+
// left: this.left,
|
|
625
|
+
// top: this.top,
|
|
626
|
+
// });
|
|
627
|
+
this.fire('changed');
|
|
628
|
+
this.canvas && this.canvas.requestRenderAll();
|
|
629
|
+
}
|
|
536
630
|
|
|
537
631
|
/**
|
|
538
632
|
* Cancel overlay editing without changes
|
|
@@ -646,7 +740,7 @@ export abstract class ITextBehavior<
|
|
|
646
740
|
* @private
|
|
647
741
|
*/
|
|
648
742
|
_updateTextarea() {
|
|
649
|
-
console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
743
|
+
// console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
650
744
|
this.cursorOffsetCache = {};
|
|
651
745
|
if (!this.hiddenTextarea) {
|
|
652
746
|
return;
|
|
@@ -655,9 +749,9 @@ export abstract class ITextBehavior<
|
|
|
655
749
|
// Sync textarea content with fabric text to prevent double-keypress issues
|
|
656
750
|
const currentFabricText = this.text;
|
|
657
751
|
if (this.hiddenTextarea.value !== currentFabricText) {
|
|
658
|
-
console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
659
|
-
console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
660
|
-
console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
752
|
+
// console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
753
|
+
// console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
754
|
+
// console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
661
755
|
this.hiddenTextarea.value = currentFabricText;
|
|
662
756
|
}
|
|
663
757
|
|
|
@@ -1237,4 +1331,4 @@ export abstract class ITextBehavior<
|
|
|
1237
1331
|
this.selectionEnd = newSelection;
|
|
1238
1332
|
}
|
|
1239
1333
|
}
|
|
1240
|
-
}
|
|
1334
|
+
}
|
|
@@ -232,7 +232,8 @@ export abstract class ITextClickBehavior<
|
|
|
232
232
|
|
|
233
233
|
for (let j = 0; j < charLength; j++) {
|
|
234
234
|
const charStart = lineLeftOffset + chars[j].left;
|
|
235
|
-
|
|
235
|
+
// For last character, use its width to calculate end position
|
|
236
|
+
const charEnd = lineLeftOffset + (chars[j + 1]?.left ?? (chars[j].left + chars[j].kernedWidth));
|
|
236
237
|
const charMiddle = (charStart + charEnd) / 2;
|
|
237
238
|
if (mouseOffset.x <= charMiddle) {
|
|
238
239
|
charIndex = lineStartIndex + j;
|
|
@@ -244,11 +245,24 @@ export abstract class ITextClickBehavior<
|
|
|
244
245
|
charIndex = lineStartIndex + charLength;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
const result = this.flipX
|
|
249
|
-
? lineStartIndex + (charLength - lineCharIndex)
|
|
250
|
-
: charIndex;
|
|
248
|
+
let lineCharIndex = charIndex - lineStartIndex;
|
|
251
249
|
|
|
252
|
-
|
|
250
|
+
// Handle flipX
|
|
251
|
+
if (this.flipX) {
|
|
252
|
+
lineCharIndex = charLength - lineCharIndex;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Convert display index to original index (handles kashida)
|
|
256
|
+
const originalLineCharIndex = (this as any)._displayToOriginalIndex(lineIndex, lineCharIndex);
|
|
257
|
+
|
|
258
|
+
// Calculate original line start (sum of original line lengths before this line)
|
|
259
|
+
let originalLineStart = 0;
|
|
260
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
261
|
+
const originalLineLength = (this as any)._getOriginalLineLength(i);
|
|
262
|
+
originalLineStart += originalLineLength + this.missingNewlineOffset(i);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const originalIndex = originalLineStart + originalLineCharIndex;
|
|
266
|
+
return Math.min(originalIndex, this.text.length);
|
|
253
267
|
}
|
|
254
268
|
}
|
|
@@ -180,21 +180,21 @@ export abstract class ITextKeyBehavior<
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// Debug log to track the double keypress issue
|
|
183
|
-
console.log('🔤 onInput debug:', {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
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
191
|
|
|
192
192
|
// Immediate sync for simple character replacement - fix for double keypress issue
|
|
193
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);
|
|
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
198
|
|
|
199
199
|
// Clear all relevant caches that might prevent visual updates
|
|
200
200
|
this.cursorOffsetCache = {};
|
|
@@ -202,7 +202,7 @@ export abstract class ITextKeyBehavior<
|
|
|
202
202
|
(this as any)._lastDimensionState = null;
|
|
203
203
|
this._forceClearCache = true;
|
|
204
204
|
|
|
205
|
-
console.log('🔤 Cleared all caches');
|
|
205
|
+
// console.log('🔤 Cleared all caches');
|
|
206
206
|
|
|
207
207
|
// Use the same logic as updateAndFire but immediately
|
|
208
208
|
this.updateFromTextArea();
|
|
@@ -214,8 +214,8 @@ export abstract class ITextKeyBehavior<
|
|
|
214
214
|
this.canvas.renderAll();
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
console.log('🔤 After updateFromTextArea - fabric text:', this.text);
|
|
218
|
-
console.log('🔤 Sync complete, caches cleared, synchronous render only');
|
|
217
|
+
// console.log('🔤 After updateFromTextArea - fabric text:', this.text);
|
|
218
|
+
// console.log('🔤 Sync complete, caches cleared, synchronous render only');
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
|