@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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { defineProperty as _defineProperty } from '../../../_virtual/_rollupPluginBabelHelpers.mjs';
|
|
2
2
|
import { Canvas } from '../../canvas/Canvas.mjs';
|
|
3
3
|
import { ITextClickBehavior } from './ITextClickBehavior.mjs';
|
|
4
|
-
import { getCursorRect
|
|
4
|
+
import { getCursorRect } from '../../text/hitTest.mjs';
|
|
5
|
+
import { analyzeBiDi } from '../../text/unicode.mjs';
|
|
5
6
|
import { ctrlKeysMapUp, ctrlKeysMapDown, keysMapRtl, keysMap } from './constants.mjs';
|
|
6
7
|
import { classRegistry } from '../../ClassRegistry.mjs';
|
|
7
|
-
import { JUSTIFY
|
|
8
|
-
import {
|
|
8
|
+
import { JUSTIFY } from '../Text/constants.mjs';
|
|
9
|
+
import { FILL } from '../../constants.mjs';
|
|
9
10
|
import { createCanvasElementFor } from '../../util/misc/dom.mjs';
|
|
10
11
|
import { applyCanvasTransform } from '../../util/internals/applyCanvasTransform.mjs';
|
|
11
|
-
import { Point } from '../../Point.mjs';
|
|
12
12
|
import { invertTransform } from '../../util/misc/matrix.mjs';
|
|
13
13
|
|
|
14
14
|
// Declare IText protected properties to workaround TS
|
|
@@ -105,6 +105,20 @@ class IText extends ITextClickBehavior {
|
|
|
105
105
|
...IText.ownDefaults,
|
|
106
106
|
...options
|
|
107
107
|
});
|
|
108
|
+
/**
|
|
109
|
+
* Index where text selection starts (or where cursor is when there is no selection)
|
|
110
|
+
* @type Number
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* Index where text selection ends
|
|
114
|
+
* @type Number
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Cache for visual positions per line to ensure consistency
|
|
118
|
+
* during selection operations
|
|
119
|
+
* @private
|
|
120
|
+
*/
|
|
121
|
+
_defineProperty(this, "_visualPositionsCache", new Map());
|
|
108
122
|
this.initBehavior();
|
|
109
123
|
}
|
|
110
124
|
|
|
@@ -228,6 +242,8 @@ class IText extends ITextClickBehavior {
|
|
|
228
242
|
// clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
|
|
229
243
|
// the correct position but not at every cursor animation.
|
|
230
244
|
this.cursorOffsetCache = {};
|
|
245
|
+
// Clear visual positions cache on full render since dimensions may have changed
|
|
246
|
+
this._clearVisualPositionsCache();
|
|
231
247
|
this.renderCursorOrSelection();
|
|
232
248
|
}
|
|
233
249
|
|
|
@@ -255,6 +271,9 @@ class IText extends ITextClickBehavior {
|
|
|
255
271
|
if (!ctx) {
|
|
256
272
|
return;
|
|
257
273
|
}
|
|
274
|
+
// Clear cache to ensure fresh cursor position calculation
|
|
275
|
+
// This is important during selection drag when positions change frequently
|
|
276
|
+
this.cursorOffsetCache = {};
|
|
258
277
|
const boundaries = this._getCursorBoundaries();
|
|
259
278
|
const ancestors = this.findAncestorsWithClipPath();
|
|
260
279
|
const hasAncestorsWithClipping = ancestors.length > 0;
|
|
@@ -332,12 +351,8 @@ class IText extends ITextClickBehavior {
|
|
|
332
351
|
_getCursorBoundaries() {
|
|
333
352
|
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.selectionStart;
|
|
334
353
|
let skipCaching = arguments.length > 1 ? arguments[1] : undefined;
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
return this._getCursorBoundariesAdvanced(index);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Fall back to original method
|
|
354
|
+
// Always use original method which uses __charBounds directly
|
|
355
|
+
// and has proper RTL handling built-in
|
|
341
356
|
return this._getCursorBoundariesOriginal(index, skipCaching);
|
|
342
357
|
}
|
|
343
358
|
|
|
@@ -376,19 +391,261 @@ class IText extends ITextClickBehavior {
|
|
|
376
391
|
}
|
|
377
392
|
|
|
378
393
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
394
|
+
* Override selection to use measureText-based visual positions
|
|
395
|
+
* This ensures hit testing matches actual browser BiDi rendering
|
|
381
396
|
*/
|
|
382
397
|
getSelectionStartFromPointer(e) {
|
|
383
|
-
|
|
384
|
-
|
|
398
|
+
// Get mouse position in object-local coordinates (origin at center)
|
|
399
|
+
const scenePoint = this.canvas.getScenePoint(e);
|
|
400
|
+
const localPoint = scenePoint.transform(invertTransform(this.calcTransformMatrix()));
|
|
401
|
+
|
|
402
|
+
// Convert to top-left origin coordinates
|
|
403
|
+
const mouseX = localPoint.x + this.width / 2;
|
|
404
|
+
const mouseY = localPoint.y + this.height / 2;
|
|
405
|
+
|
|
406
|
+
// Find the line based on Y position
|
|
407
|
+
let height = 0,
|
|
408
|
+
lineIndex = 0;
|
|
409
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
410
|
+
const lineHeight = this.getHeightOfLine(i);
|
|
411
|
+
if (mouseY >= height && mouseY < height + lineHeight) {
|
|
412
|
+
lineIndex = i;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
height += lineHeight;
|
|
416
|
+
if (i === this._textLines.length - 1) {
|
|
417
|
+
lineIndex = i;
|
|
418
|
+
}
|
|
385
419
|
}
|
|
386
|
-
const mouseOffset = this.canvas.getScenePoint(e).transform(invertTransform(this.calcTransformMatrix())).add(new Point(-this._getLeftOffset(), -this._getTopOffset()));
|
|
387
420
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
421
|
+
// Calculate line start index using ORIGINAL line lengths (without tatweels)
|
|
422
|
+
// This ensures selection indices refer to the original text, not the display text
|
|
423
|
+
let lineStartIndex = 0;
|
|
424
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
425
|
+
const origLen = this._getOriginalLineLength(i);
|
|
426
|
+
const newlineOffset = this.missingNewlineOffset(i);
|
|
427
|
+
console.log(`📍 Line ${i}: origLen=${origLen}, displayLen=${this._textLines[i].length}, tatweels=${this._getTatweelCountForLine(i)}, newlineOffset=${newlineOffset}`);
|
|
428
|
+
lineStartIndex += origLen + newlineOffset;
|
|
429
|
+
}
|
|
430
|
+
console.log(`📍 Click on line ${lineIndex}, lineStartIndex=${lineStartIndex}`);
|
|
431
|
+
const line = this._textLines[lineIndex];
|
|
432
|
+
const lineText = line.join('');
|
|
433
|
+
const displayCharLength = line.length;
|
|
434
|
+
const originalCharLength = this._getOriginalLineLength(lineIndex);
|
|
435
|
+
if (displayCharLength === 0) {
|
|
436
|
+
return lineStartIndex;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Use measureText to get actual visual character positions
|
|
440
|
+
// This matches exactly how the canvas renders BiDi text
|
|
441
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
442
|
+
|
|
443
|
+
// Calculate line offset based on alignment
|
|
444
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
445
|
+
let lineStartX = 0;
|
|
446
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
447
|
+
lineStartX = (this.width - lineWidth) / 2;
|
|
448
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
449
|
+
lineStartX = this.width - lineWidth;
|
|
450
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
451
|
+
// For RTL with left/justify, text starts from right
|
|
452
|
+
lineStartX = this.width - lineWidth;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Find which character was clicked based on visual position
|
|
456
|
+
const clickX = mouseX - lineStartX;
|
|
457
|
+
|
|
458
|
+
// Sort positions by visual X for hit testing
|
|
459
|
+
const sortedPositions = [...visualPositions].sort((a, b) => a.visualX - b.visualX);
|
|
460
|
+
|
|
461
|
+
// Handle click before first character
|
|
462
|
+
if (sortedPositions.length > 0 && clickX < sortedPositions[0].visualX) {
|
|
463
|
+
// Before first visual character - cursor at visual left edge
|
|
464
|
+
// For RTL base direction, this means logical end of line
|
|
465
|
+
return this.direction === 'rtl' ? lineStartIndex + originalCharLength : lineStartIndex;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Handle click after last character
|
|
469
|
+
if (sortedPositions.length > 0) {
|
|
470
|
+
const lastPos = sortedPositions[sortedPositions.length - 1];
|
|
471
|
+
if (clickX > lastPos.visualX + lastPos.width) {
|
|
472
|
+
// After last visual character - cursor at visual right edge
|
|
473
|
+
// For RTL base direction, this means logical start of line
|
|
474
|
+
return this.direction === 'rtl' ? lineStartIndex : lineStartIndex + originalCharLength;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Find the character at click position
|
|
479
|
+
for (let i = 0; i < sortedPositions.length; i++) {
|
|
480
|
+
const pos = sortedPositions[i];
|
|
481
|
+
const charEnd = pos.visualX + pos.width;
|
|
482
|
+
if (clickX >= pos.visualX && clickX <= charEnd) {
|
|
483
|
+
// Convert display index to original index
|
|
484
|
+
// This also handles tatweels - they map to the character they extend
|
|
485
|
+
const originalCharIndex = this._displayToOriginalIndex(lineIndex, pos.logicalIndex);
|
|
486
|
+
|
|
487
|
+
// Check if this is a tatweel - if so, treat click as clicking on the extended character
|
|
488
|
+
const isTatweel = this._isTatweelAtDisplayIndex(lineIndex, pos.logicalIndex);
|
|
489
|
+
console.log(`📍 Hit char: displayIdx=${pos.logicalIndex}, origIdx=${originalCharIndex}, isTatweel=${isTatweel}, char="${this._textLines[lineIndex][pos.logicalIndex]}"`);
|
|
490
|
+
const charMiddle = pos.visualX + pos.width / 2;
|
|
491
|
+
const clickedLeftHalf = clickX <= charMiddle;
|
|
492
|
+
|
|
493
|
+
// For tatweels, clicking anywhere on it should place cursor after the extended character
|
|
494
|
+
if (isTatweel) {
|
|
495
|
+
// Tatweel extends the character before it, so cursor goes after that character
|
|
496
|
+
// originalCharIndex from _displayToOriginalIndex already maps tatweel to char+1
|
|
497
|
+
const result = lineStartIndex + originalCharIndex;
|
|
498
|
+
console.log(`📍 Tatweel click result: ${result}`);
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// For RTL characters: left visual half means cursor AFTER (higher logical index)
|
|
503
|
+
// For LTR characters: left visual half means cursor BEFORE (lower logical index)
|
|
504
|
+
if (pos.isRtl) {
|
|
505
|
+
// RTL character
|
|
506
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex + 1 : originalCharIndex);
|
|
507
|
+
console.log(`📍 RTL char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
508
|
+
return result;
|
|
509
|
+
} else {
|
|
510
|
+
// LTR character
|
|
511
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex : originalCharIndex + 1);
|
|
512
|
+
console.log(`📍 LTR char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// console.log(`📍 No match, returning end: ${lineStartIndex + originalCharLength}`);
|
|
519
|
+
return lineStartIndex + originalCharLength;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Clear the visual positions cache
|
|
524
|
+
* Should be called when text content or dimensions change
|
|
525
|
+
*/
|
|
526
|
+
_clearVisualPositionsCache() {
|
|
527
|
+
this._visualPositionsCache.clear();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Measure visual character positions for hit testing using BiDi analysis
|
|
532
|
+
* This properly handles mixed RTL/LTR text by analyzing BiDi runs
|
|
533
|
+
* Results are cached per line for consistency during selection operations
|
|
534
|
+
*/
|
|
535
|
+
_measureVisualPositions(lineIndex, lineText) {
|
|
536
|
+
// Check cache first
|
|
537
|
+
if (this._visualPositionsCache.has(lineIndex)) {
|
|
538
|
+
return this._visualPositionsCache.get(lineIndex);
|
|
539
|
+
}
|
|
540
|
+
const line = this._textLines[lineIndex];
|
|
541
|
+
const positions = [];
|
|
542
|
+
const chars = this.__charBounds[lineIndex];
|
|
543
|
+
if (!chars || chars.length === 0) {
|
|
544
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
545
|
+
return positions;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// For LTR direction, use logical positions directly
|
|
549
|
+
if (this.direction !== 'rtl') {
|
|
550
|
+
for (let i = 0; i < line.length; i++) {
|
|
551
|
+
var _chars$i, _chars$i2;
|
|
552
|
+
positions.push({
|
|
553
|
+
logicalIndex: i,
|
|
554
|
+
visualX: ((_chars$i = chars[i]) === null || _chars$i === void 0 ? void 0 : _chars$i.left) || 0,
|
|
555
|
+
width: ((_chars$i2 = chars[i]) === null || _chars$i2 === void 0 ? void 0 : _chars$i2.kernedWidth) || 0,
|
|
556
|
+
isRtl: false
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
560
|
+
return positions;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// For RTL, use BiDi analysis to determine visual positions
|
|
564
|
+
const runs = analyzeBiDi(lineText, 'rtl');
|
|
565
|
+
|
|
566
|
+
// Build mapping from string position to grapheme index
|
|
567
|
+
// This is needed because analyzeBiDi works on string positions (code points)
|
|
568
|
+
// but we need grapheme indices for charBounds
|
|
569
|
+
const stringPosToGrapheme = [];
|
|
570
|
+
let strPos = 0;
|
|
571
|
+
for (let gi = 0; gi < line.length; gi++) {
|
|
572
|
+
const grapheme = line[gi];
|
|
573
|
+
for (let j = 0; j < grapheme.length; j++) {
|
|
574
|
+
stringPosToGrapheme[strPos + j] = gi;
|
|
575
|
+
}
|
|
576
|
+
strPos += grapheme.length;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Calculate width for each run
|
|
580
|
+
|
|
581
|
+
const runInfos = [];
|
|
582
|
+
for (const run of runs) {
|
|
583
|
+
const runChars = [];
|
|
584
|
+
let runWidth = 0;
|
|
585
|
+
const seenGraphemes = new Set();
|
|
586
|
+
|
|
587
|
+
// Map string positions in this run to grapheme indices
|
|
588
|
+
for (let sp = run.start; sp < run.end; sp++) {
|
|
589
|
+
const gi = stringPosToGrapheme[sp];
|
|
590
|
+
if (gi !== undefined && !seenGraphemes.has(gi)) {
|
|
591
|
+
var _chars$gi;
|
|
592
|
+
seenGraphemes.add(gi);
|
|
593
|
+
runChars.push(gi);
|
|
594
|
+
runWidth += ((_chars$gi = chars[gi]) === null || _chars$gi === void 0 ? void 0 : _chars$gi.kernedWidth) || 0;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
runInfos.push({
|
|
598
|
+
run,
|
|
599
|
+
width: runWidth,
|
|
600
|
+
charIndices: runChars
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// For RTL base direction, runs are displayed right-to-left
|
|
605
|
+
// So first run appears on the right, last run on the left
|
|
606
|
+
const totalWidth = this.getLineWidth(lineIndex);
|
|
607
|
+
let visualX = totalWidth; // Start from right edge
|
|
608
|
+
|
|
609
|
+
for (const runInfo of runInfos) {
|
|
610
|
+
visualX -= runInfo.width; // Move left by run width
|
|
611
|
+
|
|
612
|
+
const isRtlRun = runInfo.run.direction === 'rtl';
|
|
613
|
+
if (isRtlRun) {
|
|
614
|
+
// RTL run: characters displayed right-to-left within run
|
|
615
|
+
// First char of run at visual right of run, last at visual left
|
|
616
|
+
let charX = visualX + runInfo.width;
|
|
617
|
+
for (const idx of runInfo.charIndices) {
|
|
618
|
+
var _chars$idx;
|
|
619
|
+
const charWidth = ((_chars$idx = chars[idx]) === null || _chars$idx === void 0 ? void 0 : _chars$idx.kernedWidth) || 0;
|
|
620
|
+
charX -= charWidth;
|
|
621
|
+
positions.push({
|
|
622
|
+
logicalIndex: idx,
|
|
623
|
+
visualX: charX,
|
|
624
|
+
width: charWidth,
|
|
625
|
+
isRtl: true
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
// LTR run: characters displayed left-to-right within run
|
|
630
|
+
// First char of run at visual left of run, last at visual right
|
|
631
|
+
let charX = visualX;
|
|
632
|
+
for (const idx of runInfo.charIndices) {
|
|
633
|
+
var _chars$idx2;
|
|
634
|
+
const charWidth = ((_chars$idx2 = chars[idx]) === null || _chars$idx2 === void 0 ? void 0 : _chars$idx2.kernedWidth) || 0;
|
|
635
|
+
positions.push({
|
|
636
|
+
logicalIndex: idx,
|
|
637
|
+
visualX: charX,
|
|
638
|
+
width: charWidth,
|
|
639
|
+
isRtl: false
|
|
640
|
+
});
|
|
641
|
+
charX += charWidth;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Cache the result
|
|
647
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
648
|
+
return positions;
|
|
392
649
|
}
|
|
393
650
|
|
|
394
651
|
/**
|
|
@@ -408,40 +665,140 @@ class IText extends ITextClickBehavior {
|
|
|
408
665
|
}
|
|
409
666
|
|
|
410
667
|
/**
|
|
411
|
-
* Calculates cursor left/top offset relative to
|
|
668
|
+
* Calculates cursor left/top offset relative to _getLeftOffset()
|
|
669
|
+
* Uses visual positions for BiDi text support
|
|
670
|
+
* Handles kashida by converting original indices to display indices
|
|
412
671
|
* @private
|
|
413
|
-
* @param {number} index index from start
|
|
672
|
+
* @param {number} index index from start (in original text space, without tatweels)
|
|
414
673
|
*/
|
|
415
674
|
__getCursorBoundariesOffsets(index) {
|
|
416
|
-
let topOffset = 0
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
675
|
+
let topOffset = 0;
|
|
676
|
+
|
|
677
|
+
// Find line index and original char index using original line lengths
|
|
678
|
+
let lineIndex = 0;
|
|
679
|
+
let originalCharIndex = index;
|
|
680
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
681
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
682
|
+
if (originalCharIndex <= originalLineLength) {
|
|
683
|
+
lineIndex = i;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
originalCharIndex -= originalLineLength + this.missingNewlineOffset(i);
|
|
687
|
+
lineIndex = i + 1;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Clamp lineIndex to valid range
|
|
691
|
+
if (lineIndex >= this._textLines.length) {
|
|
692
|
+
lineIndex = this._textLines.length - 1;
|
|
693
|
+
originalCharIndex = this._getOriginalLineLength(lineIndex);
|
|
694
|
+
}
|
|
422
695
|
for (let i = 0; i < lineIndex; i++) {
|
|
423
696
|
topOffset += this.getHeightOfLine(i);
|
|
424
697
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
698
|
+
|
|
699
|
+
// Convert original char index to display char index for visual lookup
|
|
700
|
+
const displayCharIndex = this._originalToDisplayIndex(lineIndex, originalCharIndex);
|
|
701
|
+
|
|
702
|
+
// Get visual positions for cursor placement
|
|
703
|
+
const lineText = this._textLines[lineIndex].join('');
|
|
704
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
705
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
706
|
+
this._textLines[lineIndex].length;
|
|
707
|
+
const originalLineLength = this._getOriginalLineLength(lineIndex);
|
|
708
|
+
|
|
709
|
+
// Find visual X position for cursor (0 to lineWidth, from visual left)
|
|
710
|
+
let visualX = 0;
|
|
711
|
+
if (visualPositions.length === 0) {
|
|
712
|
+
// Fallback for empty line
|
|
713
|
+
return {
|
|
714
|
+
top: topOffset,
|
|
715
|
+
left: 0
|
|
716
|
+
};
|
|
430
717
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
718
|
+
if (originalCharIndex === 0) {
|
|
719
|
+
// Cursor at logical start
|
|
720
|
+
// For RTL base direction, logical start is at visual right
|
|
721
|
+
if (this.direction === 'rtl') {
|
|
722
|
+
visualX = lineWidth; // Right edge
|
|
723
|
+
} else {
|
|
724
|
+
visualX = 0; // Left edge
|
|
725
|
+
}
|
|
726
|
+
} else if (originalCharIndex >= originalLineLength) {
|
|
727
|
+
// Cursor at logical end
|
|
728
|
+
// For RTL base direction, logical end is at visual left
|
|
729
|
+
if (this.direction === 'rtl') {
|
|
730
|
+
visualX = 0; // Left edge
|
|
731
|
+
} else {
|
|
732
|
+
visualX = lineWidth; // Right edge
|
|
442
733
|
}
|
|
734
|
+
} else {
|
|
735
|
+
// Cursor between characters - find visual position of character at displayCharIndex
|
|
736
|
+
const charPos = visualPositions.find(p => p.logicalIndex === displayCharIndex);
|
|
737
|
+
if (charPos) {
|
|
738
|
+
// Use character's direction to determine cursor position
|
|
739
|
+
// For RTL char: cursor "before" it appears at its right visual edge
|
|
740
|
+
// For LTR char: cursor "before" it appears at its left visual edge
|
|
741
|
+
if (charPos.isRtl) {
|
|
742
|
+
visualX = charPos.visualX + charPos.width;
|
|
743
|
+
} else {
|
|
744
|
+
visualX = charPos.visualX;
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
// Fallback - try the previous character in display space
|
|
748
|
+
const prevDisplayIndex = displayCharIndex > 0 ? displayCharIndex - 1 : 0;
|
|
749
|
+
const prevCharPos = visualPositions.find(p => p.logicalIndex === prevDisplayIndex);
|
|
750
|
+
if (prevCharPos) {
|
|
751
|
+
// Cursor after previous character
|
|
752
|
+
if (prevCharPos.isRtl) {
|
|
753
|
+
visualX = prevCharPos.visualX;
|
|
754
|
+
} else {
|
|
755
|
+
visualX = prevCharPos.visualX + prevCharPos.width;
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
// Ultimate fallback
|
|
759
|
+
const bound = this.__charBounds[lineIndex][displayCharIndex];
|
|
760
|
+
visualX = (bound === null || bound === void 0 ? void 0 : bound.left) || 0;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Calculate alignment offset (how much line is shifted from left edge)
|
|
766
|
+
let alignOffset = 0;
|
|
767
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
768
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
769
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
770
|
+
alignOffset = this.width - lineWidth;
|
|
771
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
772
|
+
alignOffset = this.width - lineWidth;
|
|
443
773
|
}
|
|
444
|
-
|
|
774
|
+
|
|
775
|
+
// The returned left value is added to _getLeftOffset() in _getCursorBoundaries
|
|
776
|
+
// _getLeftOffset() returns -width/2 for LTR, +width/2 for RTL
|
|
777
|
+
// Final cursor X = _getLeftOffset() + leftOffset
|
|
778
|
+
//
|
|
779
|
+
// For LTR: cursor X = -width/2 + (alignOffset + visualX)
|
|
780
|
+
// For RTL: cursor X = +width/2 + leftOffset
|
|
781
|
+
// We want cursor at: -width/2 + alignOffset + visualX
|
|
782
|
+
// So leftOffset = -width/2 + alignOffset + visualX - width/2 = alignOffset + visualX - width
|
|
783
|
+
|
|
784
|
+
let leftOffset;
|
|
785
|
+
if (this.direction === 'rtl') {
|
|
786
|
+
// For RTL, _getLeftOffset() = +width/2
|
|
787
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
788
|
+
// So: +width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
789
|
+
// leftOffset = -width + alignOffset + visualX
|
|
790
|
+
leftOffset = -this.width + alignOffset + visualX;
|
|
791
|
+
} else {
|
|
792
|
+
// For LTR, _getLeftOffset() = -width/2
|
|
793
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
794
|
+
// So: -width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
795
|
+
// leftOffset = alignOffset + visualX
|
|
796
|
+
leftOffset = alignOffset + visualX;
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
top: topOffset,
|
|
800
|
+
left: leftOffset
|
|
801
|
+
};
|
|
445
802
|
}
|
|
446
803
|
|
|
447
804
|
/**
|
|
@@ -533,51 +890,119 @@ class IText extends ITextClickBehavior {
|
|
|
533
890
|
}
|
|
534
891
|
|
|
535
892
|
/**
|
|
536
|
-
* Renders text selection
|
|
893
|
+
* Renders text selection using visual positions for BiDi support
|
|
894
|
+
* Handles kashida by converting original indices to display indices
|
|
537
895
|
* @private
|
|
538
|
-
* @param {{ selectionStart: number, selectionEnd: number }} selection
|
|
896
|
+
* @param {{ selectionStart: number, selectionEnd: number }} selection (in original text space)
|
|
539
897
|
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
|
|
540
898
|
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
|
|
541
899
|
*/
|
|
542
900
|
_renderSelection(ctx, selection, boundaries) {
|
|
543
901
|
const selectionStart = selection.selectionStart,
|
|
544
902
|
selectionEnd = selection.selectionEnd,
|
|
545
|
-
isJustify = this.textAlign.includes(JUSTIFY)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
903
|
+
isJustify = this.textAlign.includes(JUSTIFY);
|
|
904
|
+
|
|
905
|
+
// Convert selection indices to line/char using original text space
|
|
906
|
+
// This handles kashida properly since selection indices don't include tatweels
|
|
907
|
+
let startLine = 0,
|
|
908
|
+
endLine = 0;
|
|
909
|
+
let originalStartChar = selectionStart,
|
|
910
|
+
originalEndChar = selectionEnd;
|
|
911
|
+
|
|
912
|
+
// Find start line and char
|
|
913
|
+
let charCount = 0;
|
|
914
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
915
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
916
|
+
if (charCount + originalLineLength >= selectionStart) {
|
|
917
|
+
startLine = i;
|
|
918
|
+
originalStartChar = selectionStart - charCount;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Find end line and char
|
|
925
|
+
charCount = 0;
|
|
926
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
927
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
928
|
+
if (charCount + originalLineLength >= selectionEnd) {
|
|
929
|
+
endLine = i;
|
|
930
|
+
originalEndChar = selectionEnd - charCount;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
934
|
+
if (i === this._textLines.length - 1) {
|
|
935
|
+
endLine = i;
|
|
936
|
+
originalEndChar = originalLineLength;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
552
939
|
for (let i = startLine; i <= endLine; i++) {
|
|
553
|
-
const lineOffset = this._getLineLeftOffset(i) || 0;
|
|
554
940
|
let lineHeight = this.getHeightOfLine(i),
|
|
555
|
-
realLineHeight = 0
|
|
556
|
-
boxStart = 0,
|
|
557
|
-
boxEnd = 0;
|
|
941
|
+
realLineHeight = 0;
|
|
558
942
|
|
|
559
|
-
//
|
|
943
|
+
// Get visual positions for this line
|
|
944
|
+
const lineText = this._textLines[i].join('');
|
|
945
|
+
const visualPositions = this._measureVisualPositions(i, lineText);
|
|
946
|
+
this._textLines[i].length;
|
|
947
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
948
|
+
|
|
949
|
+
// Calculate selection bounds in original space, then convert to display
|
|
950
|
+
let originalLineStartChar = 0;
|
|
951
|
+
let originalLineEndChar = originalLineLength;
|
|
560
952
|
if (i === startLine) {
|
|
561
|
-
|
|
953
|
+
originalLineStartChar = originalStartChar;
|
|
954
|
+
}
|
|
955
|
+
if (i === endLine) {
|
|
956
|
+
originalLineEndChar = originalEndChar;
|
|
562
957
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
958
|
+
|
|
959
|
+
// Convert original char indices to display indices for visual lookup
|
|
960
|
+
const displayLineStartChar = this._originalToDisplayIndex(i, originalLineStartChar);
|
|
961
|
+
const displayLineEndChar = this._originalToDisplayIndex(i, originalLineEndChar);
|
|
962
|
+
|
|
963
|
+
// Get visual X positions for selection range
|
|
964
|
+
let minVisualX = Infinity;
|
|
965
|
+
let maxVisualX = -Infinity;
|
|
966
|
+
for (const pos of visualPositions) {
|
|
967
|
+
if (pos.logicalIndex >= displayLineStartChar && pos.logicalIndex < displayLineEndChar) {
|
|
968
|
+
minVisualX = Math.min(minVisualX, pos.visualX);
|
|
969
|
+
maxVisualX = Math.max(maxVisualX, pos.visualX + pos.width);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Handle edge cases
|
|
974
|
+
if (minVisualX === Infinity || maxVisualX === -Infinity) {
|
|
975
|
+
if (i >= startLine && i < endLine) {
|
|
976
|
+
// Full line selection
|
|
977
|
+
minVisualX = 0;
|
|
978
|
+
maxVisualX = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5;
|
|
568
979
|
} else {
|
|
569
|
-
|
|
570
|
-
boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width - charSpacing;
|
|
980
|
+
continue; // No selection on this line
|
|
571
981
|
}
|
|
572
982
|
}
|
|
573
983
|
realLineHeight = lineHeight;
|
|
574
984
|
if (this.lineHeight < 1 || i === endLine && this.lineHeight > 1) {
|
|
575
985
|
lineHeight /= this.lineHeight;
|
|
576
986
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
987
|
+
|
|
988
|
+
// Calculate draw position
|
|
989
|
+
// Visual positions are relative to line start (0 to lineWidth)
|
|
990
|
+
// Need to add alignment offset
|
|
991
|
+
const lineWidth = this.getLineWidth(i);
|
|
992
|
+
let alignOffset = 0;
|
|
993
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
994
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
995
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
996
|
+
alignOffset = this.width - lineWidth;
|
|
997
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
998
|
+
alignOffset = this.width - lineWidth;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Draw from center origin (-width/2 to width/2)
|
|
1002
|
+
const drawStart = -this.width / 2 + alignOffset + minVisualX;
|
|
1003
|
+
const drawWidth = maxVisualX - minVisualX;
|
|
1004
|
+
let drawHeight = lineHeight;
|
|
1005
|
+
let extraTop = 0;
|
|
581
1006
|
if (this.inCompositionMode) {
|
|
582
1007
|
ctx.fillStyle = this.compositionColor || 'black';
|
|
583
1008
|
drawHeight = 1;
|
|
@@ -585,15 +1010,6 @@ class IText extends ITextClickBehavior {
|
|
|
585
1010
|
} else {
|
|
586
1011
|
ctx.fillStyle = this.selectionColor;
|
|
587
1012
|
}
|
|
588
|
-
if (this.direction === 'rtl') {
|
|
589
|
-
if (this.textAlign === RIGHT || this.textAlign === JUSTIFY || this.textAlign === JUSTIFY_RIGHT) {
|
|
590
|
-
drawStart = this.width - drawStart - drawWidth;
|
|
591
|
-
} else if (this.textAlign === LEFT || this.textAlign === JUSTIFY_LEFT) {
|
|
592
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
593
|
-
} else if (this.textAlign === CENTER || this.textAlign === JUSTIFY_CENTER) {
|
|
594
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
1013
|
ctx.fillRect(drawStart, boundaries.top + boundaries.topOffset + extraTop, drawWidth, drawHeight);
|
|
598
1014
|
boundaries.topOffset += realLineHeight;
|
|
599
1015
|
}
|
|
@@ -642,53 +1058,6 @@ class IText extends ITextClickBehavior {
|
|
|
642
1058
|
super.dispose();
|
|
643
1059
|
}
|
|
644
1060
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Index where text selection starts (or where cursor is when there is no selection)
|
|
647
|
-
* @type Number
|
|
648
|
-
*/
|
|
649
|
-
/**
|
|
650
|
-
* Index where text selection ends
|
|
651
|
-
* @type Number
|
|
652
|
-
*/
|
|
653
|
-
/**
|
|
654
|
-
* Color of text selection
|
|
655
|
-
* @type String
|
|
656
|
-
*/
|
|
657
|
-
/**
|
|
658
|
-
* Indicates whether text is in editing mode
|
|
659
|
-
* @type Boolean
|
|
660
|
-
*/
|
|
661
|
-
/**
|
|
662
|
-
* Indicates whether a text can be edited
|
|
663
|
-
* @type Boolean
|
|
664
|
-
*/
|
|
665
|
-
/**
|
|
666
|
-
* Border color of text object while it's in editing mode
|
|
667
|
-
* @type String
|
|
668
|
-
*/
|
|
669
|
-
/**
|
|
670
|
-
* Width of cursor (in px)
|
|
671
|
-
* @type Number
|
|
672
|
-
*/
|
|
673
|
-
/**
|
|
674
|
-
* Color of text cursor color in editing mode.
|
|
675
|
-
* if not set (default) will take color from the text.
|
|
676
|
-
* if set to a color value that fabric can understand, it will
|
|
677
|
-
* be used instead of the color of the text at the current position.
|
|
678
|
-
* @type String
|
|
679
|
-
*/
|
|
680
|
-
/**
|
|
681
|
-
* Delay between cursor blink (in ms)
|
|
682
|
-
* @type Number
|
|
683
|
-
*/
|
|
684
|
-
/**
|
|
685
|
-
* Duration of cursor fade in (in ms)
|
|
686
|
-
* @type Number
|
|
687
|
-
*/
|
|
688
|
-
/**
|
|
689
|
-
* Indicates whether internal text char widths can be cached
|
|
690
|
-
* @type Boolean
|
|
691
|
-
*/
|
|
692
1061
|
_defineProperty(IText, "ownDefaults", iTextDefaultValues);
|
|
693
1062
|
_defineProperty(IText, "type", 'IText');
|
|
694
1063
|
classRegistry.setClass(IText);
|