@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
package/dist/index.js
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
}
|
|
361
361
|
const cache = new Cache();
|
|
362
362
|
|
|
363
|
-
var version = "7.0.1-
|
|
363
|
+
var version = "7.0.1-beta16";
|
|
364
364
|
|
|
365
365
|
// use this syntax so babel plugin see this import here
|
|
366
366
|
const VERSION = version;
|
|
@@ -4806,7 +4806,7 @@
|
|
|
4806
4806
|
const TEXT_DECORATION_THICKNESS = 'textDecorationThickness';
|
|
4807
4807
|
const fontProperties = ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle'];
|
|
4808
4808
|
const textDecorationProperties = ['underline', 'overline', 'linethrough'];
|
|
4809
|
-
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign', 'wrap', 'ellipsis', 'letterSpacing', 'enableAdvancedLayout', 'verticalAlign'];
|
|
4809
|
+
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign', 'wrap', 'ellipsis', 'letterSpacing', 'enableAdvancedLayout', 'verticalAlign', 'kashida'];
|
|
4810
4810
|
const additionalProps = [...textLayoutProperties, ...textDecorationProperties, 'textBackgroundColor', 'direction', TEXT_DECORATION_THICKNESS, 'useOverlayEditing'];
|
|
4811
4811
|
const styleProperties = [...fontProperties, ...textDecorationProperties, STROKE, 'strokeWidth', FILL, 'deltaY', 'textBackgroundColor', TEXT_DECORATION_THICKNESS];
|
|
4812
4812
|
|
|
@@ -4843,6 +4843,7 @@
|
|
|
4843
4843
|
letterSpacing: 0,
|
|
4844
4844
|
enableAdvancedLayout: false,
|
|
4845
4845
|
verticalAlign: 'top',
|
|
4846
|
+
kashida: 'none',
|
|
4846
4847
|
// Overlay editor properties
|
|
4847
4848
|
useOverlayEditing: false,
|
|
4848
4849
|
CACHE_FONT_SIZE: 400,
|
|
@@ -19675,6 +19676,14 @@
|
|
|
19675
19676
|
* and grapheme cluster boundary detection.
|
|
19676
19677
|
*/
|
|
19677
19678
|
|
|
19679
|
+
// Unicode character categories for text processing
|
|
19680
|
+
const UNICODE_CATEGORIES = {
|
|
19681
|
+
// Bidirectional types
|
|
19682
|
+
L: /[\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]/,
|
|
19683
|
+
R: /[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05F0-\u05F4\u0608\u060B\u060D]/,
|
|
19684
|
+
AL: /[\u0627\u0629-\u063A\u0641-\u064A\u066D-\u066F\u0671-\u06D3\u06D5]/,
|
|
19685
|
+
EN: /[\u0030-\u0039\u00B2\u00B3\u00B9\u06F0-\u06F9]/,
|
|
19686
|
+
AN: /[\u0660-\u0669\u066B\u066C]/};
|
|
19678
19687
|
|
|
19679
19688
|
/**
|
|
19680
19689
|
* Enhanced grapheme segmentation using Intl.Segmenter when available
|
|
@@ -19698,6 +19707,291 @@
|
|
|
19698
19707
|
return graphemeSplit(text);
|
|
19699
19708
|
}
|
|
19700
19709
|
|
|
19710
|
+
/**
|
|
19711
|
+
* Analyze text for bidirectional runs using Unicode BiDi algorithm (simplified)
|
|
19712
|
+
*/
|
|
19713
|
+
function analyzeBiDi(text) {
|
|
19714
|
+
let baseDirection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'ltr';
|
|
19715
|
+
if (!text) return [];
|
|
19716
|
+
const runs = [];
|
|
19717
|
+
const chars = Array.from(text);
|
|
19718
|
+
let currentRun = null;
|
|
19719
|
+
for (let i = 0; i < chars.length; i++) {
|
|
19720
|
+
const char = chars[i];
|
|
19721
|
+
const charDirection = getBidiDirection(char, baseDirection);
|
|
19722
|
+
|
|
19723
|
+
// Start new run if direction changes
|
|
19724
|
+
if (!currentRun || currentRun.direction !== charDirection) {
|
|
19725
|
+
if (currentRun) {
|
|
19726
|
+
runs.push(currentRun);
|
|
19727
|
+
}
|
|
19728
|
+
currentRun = {
|
|
19729
|
+
text: char,
|
|
19730
|
+
direction: charDirection,
|
|
19731
|
+
level: charDirection === 'rtl' ? 1 : 0,
|
|
19732
|
+
start: i,
|
|
19733
|
+
end: i + 1
|
|
19734
|
+
};
|
|
19735
|
+
} else {
|
|
19736
|
+
// Continue current run
|
|
19737
|
+
currentRun.text += char;
|
|
19738
|
+
currentRun.end = i + 1;
|
|
19739
|
+
}
|
|
19740
|
+
}
|
|
19741
|
+
|
|
19742
|
+
// Add final run
|
|
19743
|
+
if (currentRun) {
|
|
19744
|
+
runs.push(currentRun);
|
|
19745
|
+
}
|
|
19746
|
+
return runs.length > 0 ? runs : [{
|
|
19747
|
+
text,
|
|
19748
|
+
direction: baseDirection,
|
|
19749
|
+
level: baseDirection === 'rtl' ? 1 : 0,
|
|
19750
|
+
start: 0,
|
|
19751
|
+
end: text.length
|
|
19752
|
+
}];
|
|
19753
|
+
}
|
|
19754
|
+
|
|
19755
|
+
/**
|
|
19756
|
+
* Character classification functions
|
|
19757
|
+
*/
|
|
19758
|
+
function isWhitespace(grapheme) {
|
|
19759
|
+
return /\s/.test(grapheme);
|
|
19760
|
+
}
|
|
19761
|
+
|
|
19762
|
+
/**
|
|
19763
|
+
* Get bidirectional character type
|
|
19764
|
+
*/
|
|
19765
|
+
function getBidiDirection(char, baseDirection) {
|
|
19766
|
+
// Strong RTL characters
|
|
19767
|
+
if (UNICODE_CATEGORIES.R.test(char) || UNICODE_CATEGORIES.AL.test(char)) {
|
|
19768
|
+
return 'rtl';
|
|
19769
|
+
}
|
|
19770
|
+
|
|
19771
|
+
// Strong LTR characters
|
|
19772
|
+
if (UNICODE_CATEGORIES.L.test(char)) {
|
|
19773
|
+
return 'ltr';
|
|
19774
|
+
}
|
|
19775
|
+
|
|
19776
|
+
// Numbers follow base direction in simplified algorithm
|
|
19777
|
+
if (UNICODE_CATEGORIES.EN.test(char) || UNICODE_CATEGORIES.AN.test(char)) {
|
|
19778
|
+
return baseDirection;
|
|
19779
|
+
}
|
|
19780
|
+
|
|
19781
|
+
// Neutral characters follow context
|
|
19782
|
+
return baseDirection;
|
|
19783
|
+
}
|
|
19784
|
+
|
|
19785
|
+
// ============================================================================
|
|
19786
|
+
// Arabic Kashida (Tatweel) Support
|
|
19787
|
+
// ============================================================================
|
|
19788
|
+
|
|
19789
|
+
/**
|
|
19790
|
+
* Arabic Tatweel (kashida) character used for justification
|
|
19791
|
+
*/
|
|
19792
|
+
const ARABIC_TATWEEL = '\u0640';
|
|
19793
|
+
|
|
19794
|
+
/**
|
|
19795
|
+
* Arabic letters that do NOT connect to the following letter (non-connecting on left).
|
|
19796
|
+
* These letters cannot have kashida inserted after them.
|
|
19797
|
+
* ا (Alef), د (Dal), ذ (Thal), ر (Ra), ز (Zay), و (Waw), ة (Teh Marbuta), ء (Hamza)
|
|
19798
|
+
*/
|
|
19799
|
+
const ARABIC_NON_CONNECTING = new Set(['\u0627',
|
|
19800
|
+
// Alef
|
|
19801
|
+
'\u062F',
|
|
19802
|
+
// Dal
|
|
19803
|
+
'\u0630',
|
|
19804
|
+
// Thal
|
|
19805
|
+
'\u0631',
|
|
19806
|
+
// Ra
|
|
19807
|
+
'\u0632',
|
|
19808
|
+
// Zay
|
|
19809
|
+
'\u0648',
|
|
19810
|
+
// Waw
|
|
19811
|
+
'\u0629',
|
|
19812
|
+
// Teh Marbuta
|
|
19813
|
+
'\u0621',
|
|
19814
|
+
// Hamza
|
|
19815
|
+
'\u0622',
|
|
19816
|
+
// Alef with Madda
|
|
19817
|
+
'\u0623',
|
|
19818
|
+
// Alef with Hamza Above
|
|
19819
|
+
'\u0625',
|
|
19820
|
+
// Alef with Hamza Below
|
|
19821
|
+
'\u0672',
|
|
19822
|
+
// Alef with Wavy Hamza Above
|
|
19823
|
+
'\u0673',
|
|
19824
|
+
// Alef with Wavy Hamza Below
|
|
19825
|
+
'\u0675',
|
|
19826
|
+
// High Hamza Alef
|
|
19827
|
+
'\u0688',
|
|
19828
|
+
// Dal with Small Tah
|
|
19829
|
+
'\u0689',
|
|
19830
|
+
// Dal with Ring
|
|
19831
|
+
'\u068A',
|
|
19832
|
+
// Dal with Dot Below
|
|
19833
|
+
'\u068B',
|
|
19834
|
+
// Dal with Dot Below and Small Tah
|
|
19835
|
+
'\u068C',
|
|
19836
|
+
// Dahal
|
|
19837
|
+
'\u068D',
|
|
19838
|
+
// Ddahal
|
|
19839
|
+
'\u068E',
|
|
19840
|
+
// Dul
|
|
19841
|
+
'\u068F',
|
|
19842
|
+
// Dal with Three Dots Above Downwards
|
|
19843
|
+
'\u0690',
|
|
19844
|
+
// Dal with Four Dots Above
|
|
19845
|
+
'\u0691',
|
|
19846
|
+
// Rreh
|
|
19847
|
+
'\u0692',
|
|
19848
|
+
// Reh with Small V
|
|
19849
|
+
'\u0693',
|
|
19850
|
+
// Reh with Ring
|
|
19851
|
+
'\u0694',
|
|
19852
|
+
// Reh with Dot Below
|
|
19853
|
+
'\u0695',
|
|
19854
|
+
// Reh with Small V Below
|
|
19855
|
+
'\u0696',
|
|
19856
|
+
// Reh with Dot Below and Dot Above
|
|
19857
|
+
'\u0697',
|
|
19858
|
+
// Reh with Two Dots Above
|
|
19859
|
+
'\u0698',
|
|
19860
|
+
// Jeh
|
|
19861
|
+
'\u0699',
|
|
19862
|
+
// Reh with Four Dots Above
|
|
19863
|
+
'\u06C4',
|
|
19864
|
+
// Waw with Ring
|
|
19865
|
+
'\u06C5',
|
|
19866
|
+
// Kirghiz Oe
|
|
19867
|
+
'\u06C6',
|
|
19868
|
+
// Oe
|
|
19869
|
+
'\u06C7',
|
|
19870
|
+
// U
|
|
19871
|
+
'\u06C8',
|
|
19872
|
+
// Yu
|
|
19873
|
+
'\u06C9',
|
|
19874
|
+
// Kirghiz Yu
|
|
19875
|
+
'\u06CA',
|
|
19876
|
+
// Waw with Two Dots Above
|
|
19877
|
+
'\u06CB',
|
|
19878
|
+
// Ve
|
|
19879
|
+
'\u06CD',
|
|
19880
|
+
// Yeh with Tail
|
|
19881
|
+
'\u06CF' // Waw with Dot Above
|
|
19882
|
+
]);
|
|
19883
|
+
|
|
19884
|
+
/**
|
|
19885
|
+
* Check if a character is an Arabic letter (main Arabic block + extended)
|
|
19886
|
+
*/
|
|
19887
|
+
function isArabicLetter(char) {
|
|
19888
|
+
if (!char) return false;
|
|
19889
|
+
const code = char.charCodeAt(0);
|
|
19890
|
+
// Arabic: U+0600-U+06FF (main block)
|
|
19891
|
+
// Arabic Supplement: U+0750-U+077F
|
|
19892
|
+
// Arabic Extended-A: U+08A0-U+08FF
|
|
19893
|
+
return code >= 0x0620 && code <= 0x064A ||
|
|
19894
|
+
// Main letters
|
|
19895
|
+
code >= 0x066E && code <= 0x06D3 ||
|
|
19896
|
+
// Extended letters
|
|
19897
|
+
code >= 0x0750 && code <= 0x077F ||
|
|
19898
|
+
// Arabic Supplement
|
|
19899
|
+
code >= 0x08A0 && code <= 0x08FF // Arabic Extended-A
|
|
19900
|
+
;
|
|
19901
|
+
}
|
|
19902
|
+
|
|
19903
|
+
/**
|
|
19904
|
+
* Check if kashida can be inserted between two characters.
|
|
19905
|
+
* Kashida can only be inserted:
|
|
19906
|
+
* - Between two Arabic letters
|
|
19907
|
+
* - After a letter that connects to the next (not in ARABIC_NON_CONNECTING)
|
|
19908
|
+
* - Not at word boundaries (no whitespace before/after)
|
|
19909
|
+
*/
|
|
19910
|
+
// Alef variants that form ligatures with lam
|
|
19911
|
+
const ARABIC_ALEF_VARIANTS = new Set(['\u0627',
|
|
19912
|
+
// ا ALEF
|
|
19913
|
+
'\u0623',
|
|
19914
|
+
// أ ALEF WITH HAMZA ABOVE
|
|
19915
|
+
'\u0625',
|
|
19916
|
+
// إ ALEF WITH HAMZA BELOW
|
|
19917
|
+
'\u0622',
|
|
19918
|
+
// آ ALEF WITH MADDA ABOVE
|
|
19919
|
+
'\u0671' // ٱ ALEF WASLA
|
|
19920
|
+
]);
|
|
19921
|
+
|
|
19922
|
+
// Lam character
|
|
19923
|
+
const ARABIC_LAM = '\u0644'; // ل
|
|
19924
|
+
|
|
19925
|
+
function canInsertKashida(prevChar, nextChar) {
|
|
19926
|
+
if (!prevChar || !nextChar) return false;
|
|
19927
|
+
|
|
19928
|
+
// Can't insert at whitespace boundaries
|
|
19929
|
+
if (/\s/.test(prevChar) || /\s/.test(nextChar)) return false;
|
|
19930
|
+
|
|
19931
|
+
// Both must be Arabic letters
|
|
19932
|
+
if (!isArabicLetter(prevChar) || !isArabicLetter(nextChar)) return false;
|
|
19933
|
+
|
|
19934
|
+
// Previous char must connect to the next (not be non-connecting)
|
|
19935
|
+
if (ARABIC_NON_CONNECTING.has(prevChar)) return false;
|
|
19936
|
+
|
|
19937
|
+
// NEVER insert kashida between lam and alef - they form a ligature (لا)
|
|
19938
|
+
if (prevChar === ARABIC_LAM && ARABIC_ALEF_VARIANTS.has(nextChar)) return false;
|
|
19939
|
+
return true;
|
|
19940
|
+
}
|
|
19941
|
+
|
|
19942
|
+
/**
|
|
19943
|
+
* Represents a valid kashida insertion point
|
|
19944
|
+
*/
|
|
19945
|
+
|
|
19946
|
+
/**
|
|
19947
|
+
* Find all valid kashida insertion points in a line of text.
|
|
19948
|
+
* Returns points sorted by priority (highest first).
|
|
19949
|
+
*
|
|
19950
|
+
* Priority rules (similar to Adobe Illustrator):
|
|
19951
|
+
* 1. Between connected letters (ب + ب = highest)
|
|
19952
|
+
* 2. Prefer middle of words over edges
|
|
19953
|
+
* 3. Avoid inserting right before/after spaces
|
|
19954
|
+
*/
|
|
19955
|
+
function findKashidaPoints(graphemes) {
|
|
19956
|
+
const points = [];
|
|
19957
|
+
for (let i = 0; i < graphemes.length - 1; i++) {
|
|
19958
|
+
const prev = graphemes[i];
|
|
19959
|
+
const next = graphemes[i + 1];
|
|
19960
|
+
if (canInsertKashida(prev, next)) {
|
|
19961
|
+
// Calculate priority based on position in word
|
|
19962
|
+
let priority = 1;
|
|
19963
|
+
|
|
19964
|
+
// Find word boundaries
|
|
19965
|
+
let wordStart = i;
|
|
19966
|
+
let wordEnd = i + 1;
|
|
19967
|
+
while (wordStart > 0 && !isWhitespace(graphemes[wordStart - 1])) {
|
|
19968
|
+
wordStart--;
|
|
19969
|
+
}
|
|
19970
|
+
while (wordEnd < graphemes.length && !isWhitespace(graphemes[wordEnd])) {
|
|
19971
|
+
wordEnd++;
|
|
19972
|
+
}
|
|
19973
|
+
const wordLength = wordEnd - wordStart;
|
|
19974
|
+
const posInWord = i - wordStart;
|
|
19975
|
+
|
|
19976
|
+
// Higher priority for middle positions
|
|
19977
|
+
const distFromEdge = Math.min(posInWord, wordLength - 1 - posInWord);
|
|
19978
|
+
priority = distFromEdge + 1;
|
|
19979
|
+
|
|
19980
|
+
// Boost priority for longer words
|
|
19981
|
+
if (wordLength > 4) priority += 1;
|
|
19982
|
+
if (wordLength > 6) priority += 1;
|
|
19983
|
+
points.push({
|
|
19984
|
+
charIndex: i,
|
|
19985
|
+
priority
|
|
19986
|
+
});
|
|
19987
|
+
}
|
|
19988
|
+
}
|
|
19989
|
+
|
|
19990
|
+
// Sort by priority descending
|
|
19991
|
+
points.sort((a, b) => b.priority - a.priority);
|
|
19992
|
+
return points;
|
|
19993
|
+
}
|
|
19994
|
+
|
|
19701
19995
|
/**
|
|
19702
19996
|
* Ellipsis Text Truncation System
|
|
19703
19997
|
*
|
|
@@ -20056,7 +20350,7 @@
|
|
|
20056
20350
|
let lineHeight = 0;
|
|
20057
20351
|
let charIndex = textOffset; // Track character position in original text
|
|
20058
20352
|
|
|
20059
|
-
// Measure each grapheme
|
|
20353
|
+
// Measure each grapheme in logical order
|
|
20060
20354
|
for (let i = 0; i < graphemes.length; i++) {
|
|
20061
20355
|
const grapheme = graphemes[i];
|
|
20062
20356
|
const prevGrapheme = i > 0 ? graphemes[i - 1] : undefined;
|
|
@@ -20072,12 +20366,14 @@
|
|
|
20072
20366
|
bounds.push({
|
|
20073
20367
|
grapheme,
|
|
20074
20368
|
x,
|
|
20369
|
+
// Will be updated by BiDi reordering
|
|
20075
20370
|
y: 0,
|
|
20076
20371
|
// Will be adjusted later
|
|
20077
20372
|
width: measurement.width,
|
|
20078
20373
|
height: measurement.height,
|
|
20079
20374
|
kernedWidth: measurement.kernedWidth,
|
|
20080
20375
|
left: x,
|
|
20376
|
+
// Logical position (cumulative)
|
|
20081
20377
|
baseline: measurement.baseline,
|
|
20082
20378
|
charIndex: charIndex,
|
|
20083
20379
|
// Character position in original text
|
|
@@ -20091,6 +20387,9 @@
|
|
|
20091
20387
|
lineHeight = Math.max(lineHeight, measurement.height);
|
|
20092
20388
|
}
|
|
20093
20389
|
|
|
20390
|
+
// Note: BiDi visual reordering is handled by the browser's canvas fillText
|
|
20391
|
+
// The layout stores positions in logical order; hit testing handles the visual mapping
|
|
20392
|
+
|
|
20094
20393
|
// Remove trailing spacing from total width (but keep in bounds for rendering)
|
|
20095
20394
|
if (bounds.length > 0) {
|
|
20096
20395
|
options.letterSpacing || 0;
|
|
@@ -20101,7 +20400,9 @@
|
|
|
20101
20400
|
}
|
|
20102
20401
|
|
|
20103
20402
|
// Apply line height
|
|
20104
|
-
|
|
20403
|
+
// Note: Fabric.js uses _fontSizeMult = 1.13 for line height calculation
|
|
20404
|
+
const fontSizeMult = 1.13;
|
|
20405
|
+
const finalHeight = lineHeight * options.lineHeight * fontSizeMult;
|
|
20105
20406
|
return {
|
|
20106
20407
|
text,
|
|
20107
20408
|
graphemes,
|
|
@@ -20171,11 +20472,119 @@
|
|
|
20171
20472
|
return lines.length > 0 ? lines : [''];
|
|
20172
20473
|
}
|
|
20173
20474
|
|
|
20475
|
+
/**
|
|
20476
|
+
* Apply BiDi visual reordering to calculate correct visual X positions
|
|
20477
|
+
* This implements the Unicode Bidirectional Algorithm for character placement
|
|
20478
|
+
*/
|
|
20479
|
+
function applyBiDiVisualReordering(line, options) {
|
|
20480
|
+
const baseDirection = options.direction === 'inherit' ? 'ltr' : options.direction;
|
|
20481
|
+
|
|
20482
|
+
// Quick check: if all characters are same direction as base, no reordering needed
|
|
20483
|
+
const runs = analyzeBiDi(line.text, baseDirection);
|
|
20484
|
+
const hasMixedBiDi = runs.length > 1 || runs.length === 1 && runs[0].direction !== baseDirection;
|
|
20485
|
+
if (!hasMixedBiDi) {
|
|
20486
|
+
// For pure LTR or pure RTL, just set visual x = logical left
|
|
20487
|
+
// For RTL base direction, we need to flip positions
|
|
20488
|
+
if (baseDirection === 'rtl') {
|
|
20489
|
+
// RTL: rightmost character should be at x=0, leftmost at x=lineWidth
|
|
20490
|
+
line.bounds.forEach(bound => {
|
|
20491
|
+
bound.x = line.width - bound.left - bound.kernedWidth;
|
|
20492
|
+
});
|
|
20493
|
+
}
|
|
20494
|
+
// For LTR, x is already correct (same as left)
|
|
20495
|
+
return line;
|
|
20496
|
+
}
|
|
20497
|
+
|
|
20498
|
+
// Mixed BiDi text - need to reorder runs visually
|
|
20499
|
+
// 1. Build mapping from grapheme index to run
|
|
20500
|
+
const graphemeToRun = [];
|
|
20501
|
+
let runGraphemeStart = 0;
|
|
20502
|
+
for (let runIdx = 0; runIdx < runs.length; runIdx++) {
|
|
20503
|
+
const run = runs[runIdx];
|
|
20504
|
+
const runGraphemes = segmentGraphemes(run.text);
|
|
20505
|
+
for (let i = 0; i < runGraphemes.length; i++) {
|
|
20506
|
+
graphemeToRun.push(runIdx);
|
|
20507
|
+
}
|
|
20508
|
+
runGraphemeStart += runGraphemes.length;
|
|
20509
|
+
}
|
|
20510
|
+
|
|
20511
|
+
// 2. Calculate run widths and positions
|
|
20512
|
+
const runWidths = [];
|
|
20513
|
+
const runStartIndices = [];
|
|
20514
|
+
let currentIdx = 0;
|
|
20515
|
+
for (const run of runs) {
|
|
20516
|
+
runStartIndices.push(currentIdx);
|
|
20517
|
+
const runGraphemes = segmentGraphemes(run.text);
|
|
20518
|
+
let runWidth = 0;
|
|
20519
|
+
for (let i = 0; i < runGraphemes.length; i++) {
|
|
20520
|
+
if (currentIdx + i < line.bounds.length) {
|
|
20521
|
+
const letterSpacing = options.letterSpacing || 0;
|
|
20522
|
+
const charSpacing = options.charSpacing ? options.fontSize * options.charSpacing / 1000 : 0;
|
|
20523
|
+
runWidth += line.bounds[currentIdx + i].kernedWidth + letterSpacing + charSpacing;
|
|
20524
|
+
}
|
|
20525
|
+
}
|
|
20526
|
+
runWidths.push(runWidth);
|
|
20527
|
+
currentIdx += runGraphemes.length;
|
|
20528
|
+
}
|
|
20529
|
+
|
|
20530
|
+
// 3. Determine visual order of runs based on base direction
|
|
20531
|
+
// RTL base: runs display right-to-left (first run on right)
|
|
20532
|
+
// LTR base: runs display left-to-right (first run on left)
|
|
20533
|
+
const visualRunOrder = runs.map((_, i) => i);
|
|
20534
|
+
if (baseDirection === 'rtl') {
|
|
20535
|
+
visualRunOrder.reverse();
|
|
20536
|
+
}
|
|
20537
|
+
|
|
20538
|
+
// 4. Calculate visual X position for each run
|
|
20539
|
+
const runVisualX = new Array(runs.length);
|
|
20540
|
+
let currentX = 0;
|
|
20541
|
+
for (const runIdx of visualRunOrder) {
|
|
20542
|
+
runVisualX[runIdx] = currentX;
|
|
20543
|
+
currentX += runWidths[runIdx];
|
|
20544
|
+
}
|
|
20545
|
+
|
|
20546
|
+
// 5. Assign visual X positions to each grapheme
|
|
20547
|
+
for (let i = 0; i < line.bounds.length; i++) {
|
|
20548
|
+
const runIdx = graphemeToRun[i];
|
|
20549
|
+
if (runIdx === undefined) continue;
|
|
20550
|
+
const run = runs[runIdx];
|
|
20551
|
+
const runStart = runStartIndices[runIdx];
|
|
20552
|
+
|
|
20553
|
+
// Calculate spacing once
|
|
20554
|
+
const letterSpacing = options.letterSpacing || 0;
|
|
20555
|
+
const charSpacing = options.charSpacing ? options.fontSize * options.charSpacing / 1000 : 0;
|
|
20556
|
+
const totalSpacing = letterSpacing + charSpacing;
|
|
20557
|
+
|
|
20558
|
+
// Calculate offset within run (sum of widths of chars before this one)
|
|
20559
|
+
let offsetInRun = 0;
|
|
20560
|
+
for (let j = runStart; j < i; j++) {
|
|
20561
|
+
offsetInRun += line.bounds[j].kernedWidth + totalSpacing;
|
|
20562
|
+
}
|
|
20563
|
+
|
|
20564
|
+
// Character width including spacing
|
|
20565
|
+
const charWidth = line.bounds[i].kernedWidth + totalSpacing;
|
|
20566
|
+
|
|
20567
|
+
// For RTL runs, characters within the run are reversed visually
|
|
20568
|
+
// First logical char appears on the right, last on the left
|
|
20569
|
+
if (run.direction === 'rtl') {
|
|
20570
|
+
// Visual X = run right edge - cumulative width including this char
|
|
20571
|
+
// This places first char at right side of run, last char at left side
|
|
20572
|
+
line.bounds[i].x = runVisualX[runIdx] + runWidths[runIdx] - offsetInRun - charWidth;
|
|
20573
|
+
} else {
|
|
20574
|
+
// LTR run: visual position is run start + offset within run
|
|
20575
|
+
line.bounds[i].x = runVisualX[runIdx] + offsetInRun;
|
|
20576
|
+
}
|
|
20577
|
+
}
|
|
20578
|
+
return line;
|
|
20579
|
+
}
|
|
20580
|
+
|
|
20174
20581
|
/**
|
|
20175
20582
|
* Apply text alignment to lines
|
|
20176
20583
|
*/
|
|
20177
20584
|
function applyAlignment(lines, align, containerWidth, options) {
|
|
20178
20585
|
return lines.map(line => {
|
|
20586
|
+
// First apply BiDi reordering to get correct visual X positions
|
|
20587
|
+
applyBiDiVisualReordering(line, options);
|
|
20179
20588
|
let offsetX = 0;
|
|
20180
20589
|
switch (align) {
|
|
20181
20590
|
case 'center':
|
|
@@ -20195,7 +20604,7 @@
|
|
|
20195
20604
|
break;
|
|
20196
20605
|
}
|
|
20197
20606
|
|
|
20198
|
-
// Apply offset to all bounds
|
|
20607
|
+
// Apply offset to all bounds (both visual x and logical left for alignment)
|
|
20199
20608
|
if (offsetX !== 0) {
|
|
20200
20609
|
line.bounds.forEach(bound => {
|
|
20201
20610
|
bound.x += offsetX;
|
|
@@ -20279,7 +20688,9 @@
|
|
|
20279
20688
|
* Create empty line for empty paragraphs
|
|
20280
20689
|
*/
|
|
20281
20690
|
function createEmptyLine(options) {
|
|
20282
|
-
|
|
20691
|
+
// Fabric.js uses _fontSizeMult = 1.13 for line height calculation
|
|
20692
|
+
const fontSizeMult = 1.13;
|
|
20693
|
+
const height = options.fontSize * options.lineHeight * fontSizeMult;
|
|
20283
20694
|
return {
|
|
20284
20695
|
text: '',
|
|
20285
20696
|
graphemes: [],
|
|
@@ -20765,6 +21176,12 @@
|
|
|
20765
21176
|
* @protected
|
|
20766
21177
|
*/
|
|
20767
21178
|
_defineProperty(this, "__charBounds", []);
|
|
21179
|
+
/**
|
|
21180
|
+
* contains kashida extension info for each line.
|
|
21181
|
+
* Each entry contains { charIndex, width } for characters that have kashida extensions.
|
|
21182
|
+
* @protected
|
|
21183
|
+
*/
|
|
21184
|
+
_defineProperty(this, "__kashidaInfo", []);
|
|
20768
21185
|
Object.assign(this, FabricText.ownDefaults);
|
|
20769
21186
|
this.setOptions(options);
|
|
20770
21187
|
if (!this.styles) {
|
|
@@ -20883,11 +21300,31 @@
|
|
|
20883
21300
|
}
|
|
20884
21301
|
|
|
20885
21302
|
/**
|
|
20886
|
-
* Enlarge space boxes and shift the others for justify alignment
|
|
21303
|
+
* Enlarge space boxes and shift the others for justify alignment.
|
|
21304
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
21305
|
+
* When kashida is enabled, actual tatweel characters are inserted into the text.
|
|
20887
21306
|
*/
|
|
20888
21307
|
enlargeSpaces() {
|
|
20889
|
-
|
|
21308
|
+
// console.log('=== enlargeSpaces START ===');
|
|
21309
|
+
// console.log('this.kashida:', this.kashida);
|
|
21310
|
+
|
|
21311
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
21312
|
+
const kashidaRatios = {
|
|
21313
|
+
none: 0,
|
|
21314
|
+
short: 0.25,
|
|
21315
|
+
medium: 0.5,
|
|
21316
|
+
long: 0.75,
|
|
21317
|
+
stylistic: 1.0
|
|
21318
|
+
};
|
|
21319
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
21320
|
+
// console.log('kashidaRatio:', kashidaRatio);
|
|
21321
|
+
|
|
21322
|
+
// Reset kashida info
|
|
21323
|
+
this.__kashidaInfo = [];
|
|
20890
21324
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
21325
|
+
// Initialize kashida info for this line
|
|
21326
|
+
this.__kashidaInfo[i] = [];
|
|
21327
|
+
|
|
20891
21328
|
// Check if this line should be justified
|
|
20892
21329
|
const hasTextAfter = this._textLines.slice(i + 1).some(line => {
|
|
20893
21330
|
const lineText = Array.isArray(line) ? line.join('') : line;
|
|
@@ -20897,33 +21334,121 @@
|
|
|
20897
21334
|
const isLastLine = i === len - 1 || this.isEndOfWrapping(i) || isVisualLastLine;
|
|
20898
21335
|
const shouldJustifyLine = this.textAlign.includes('justify') && !isLastLine;
|
|
20899
21336
|
if (!shouldJustifyLine) {
|
|
21337
|
+
// console.log(` Line ${i}: skipped (not justified)`);
|
|
20900
21338
|
continue;
|
|
20901
21339
|
}
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
20911
|
-
|
|
20912
|
-
|
|
20913
|
-
|
|
20914
|
-
|
|
20915
|
-
|
|
20916
|
-
|
|
20917
|
-
|
|
20918
|
-
|
|
20919
|
-
|
|
20920
|
-
|
|
20921
|
-
|
|
21340
|
+
const line = this._textLines[i];
|
|
21341
|
+
const currentLineWidth = this.getLineWidth(i);
|
|
21342
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
21343
|
+
// console.log(` Line ${i}: width=${this.width}, lineWidth=${currentLineWidth}, extraSpace=${totalExtraSpace}`);
|
|
21344
|
+
|
|
21345
|
+
if (totalExtraSpace <= 0) {
|
|
21346
|
+
// console.log(` Line ${i}: skipped (no extra space)`);
|
|
21347
|
+
continue;
|
|
21348
|
+
}
|
|
21349
|
+
|
|
21350
|
+
// Find spaces for space expansion
|
|
21351
|
+
const spaces = this.textLines[i].match(this._reSpacesAndTabs);
|
|
21352
|
+
const numberOfSpaces = spaces ? spaces.length : 0;
|
|
21353
|
+
|
|
21354
|
+
// Find kashida points if enabled
|
|
21355
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
21356
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
21357
|
+
|
|
21358
|
+
// Calculate space distribution
|
|
21359
|
+
let kashidaSpace = 0;
|
|
21360
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
21361
|
+
// Distribute between kashida and spaces
|
|
21362
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
21363
|
+
}
|
|
21364
|
+
|
|
21365
|
+
// Calculate per-kashida and per-space widths
|
|
21366
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
21367
|
+
|
|
21368
|
+
// If kashida is enabled, insert tatweel characters into the text
|
|
21369
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
21370
|
+
// console.log(`=== Inserting kashida for line ${i} ===`);
|
|
21371
|
+
// console.log(` kashidaPoints: ${kashidaPoints.length}, perKashidaWidth: ${perKashidaWidth}`);
|
|
21372
|
+
|
|
21373
|
+
// Sort by charIndex descending to insert from end (so indices stay valid)
|
|
21374
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
21375
|
+
|
|
21376
|
+
// Calculate how many tatweels to insert per point
|
|
21377
|
+
// Measure tatweel width to determine count
|
|
21378
|
+
const ctx = getMeasuringContext();
|
|
21379
|
+
// console.log(` getMeasuringContext: ${ctx ? 'OK' : 'NULL'}`);
|
|
21380
|
+
|
|
21381
|
+
if (ctx) {
|
|
21382
|
+
ctx.font = this._getFontDeclaration();
|
|
21383
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
21384
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
21385
|
+
|
|
21386
|
+
if (tatweelWidth > 0) {
|
|
21387
|
+
const newLine = [...line];
|
|
21388
|
+
for (const point of sortedPoints) {
|
|
21389
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
21390
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
21391
|
+
|
|
21392
|
+
// Insert tatweels after the character
|
|
21393
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
21394
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
21395
|
+
}
|
|
21396
|
+
|
|
21397
|
+
// Store kashida info with updated indices and tatweel count
|
|
21398
|
+
this.__kashidaInfo[i].push({
|
|
21399
|
+
charIndex: point.charIndex,
|
|
21400
|
+
width: perKashidaWidth,
|
|
21401
|
+
tatweelCount: tatweelCount
|
|
21402
|
+
});
|
|
21403
|
+
}
|
|
21404
|
+
|
|
21405
|
+
// console.log(` Total inserted: ${insertedCount} tatweels`);
|
|
21406
|
+
// console.log(` Original line length: ${line.length}, new line length: ${newLine.length}`);
|
|
21407
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
21408
|
+
|
|
21409
|
+
// Update _textLines with the new line containing tatweels
|
|
21410
|
+
this._textLines[i] = newLine;
|
|
21411
|
+
|
|
21412
|
+
// Update textLines string version
|
|
21413
|
+
if (this.textLines && this.textLines[i] !== undefined) {
|
|
21414
|
+
this.textLines[i] = newLine.join('');
|
|
20922
21415
|
}
|
|
21416
|
+
|
|
21417
|
+
// Recalculate charBounds for this line since text changed
|
|
21418
|
+
this.__charBounds[i] = [];
|
|
21419
|
+
this.__lineWidths[i] = undefined;
|
|
21420
|
+
this._measureLine(i);
|
|
21421
|
+
|
|
21422
|
+
// console.log(` After remeasure, lineWidth: ${this.__lineWidths[i]}`);
|
|
21423
|
+
}
|
|
21424
|
+
}
|
|
21425
|
+
}
|
|
21426
|
+
|
|
21427
|
+
// Now apply space expansion to remaining extra space
|
|
21428
|
+
const newLineWidth = this.getLineWidth(i);
|
|
21429
|
+
const remainingSpace = this.width - newLineWidth;
|
|
21430
|
+
if (remainingSpace > 0 && numberOfSpaces > 0) {
|
|
21431
|
+
const extraPerSpace = remainingSpace / numberOfSpaces;
|
|
21432
|
+
let accumulatedOffset = 0;
|
|
21433
|
+
for (let j = 0; j < this._textLines[i].length; j++) {
|
|
21434
|
+
const charBound = this.__charBounds[i][j];
|
|
21435
|
+
if (!charBound) continue;
|
|
21436
|
+
charBound.left += accumulatedOffset;
|
|
21437
|
+
if (this._reSpaceAndTab.test(this._textLines[i][j])) {
|
|
21438
|
+
charBound.width += extraPerSpace;
|
|
21439
|
+
charBound.kernedWidth += extraPerSpace;
|
|
21440
|
+
accumulatedOffset += extraPerSpace;
|
|
20923
21441
|
}
|
|
20924
21442
|
}
|
|
20925
21443
|
}
|
|
20926
21444
|
}
|
|
21445
|
+
|
|
21446
|
+
// Final debug log showing kashida state
|
|
21447
|
+
// console.log('=== enlargeSpaces END ===');
|
|
21448
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
21449
|
+
// line: i,
|
|
21450
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
21451
|
+
// }))));
|
|
20927
21452
|
}
|
|
20928
21453
|
|
|
20929
21454
|
/**
|
|
@@ -20943,7 +21468,9 @@
|
|
|
20943
21468
|
return {
|
|
20944
21469
|
text: this.text,
|
|
20945
21470
|
width: this.width,
|
|
20946
|
-
height
|
|
21471
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
21472
|
+
// Only pass height if ellipsis is enabled (need to truncate)
|
|
21473
|
+
height: this.ellipsis ? this.height : undefined,
|
|
20947
21474
|
wrap: this.wrap || 'word',
|
|
20948
21475
|
align: this._mapTextAlignToAlign(this.textAlign),
|
|
20949
21476
|
ellipsis: this.ellipsis || false,
|
|
@@ -20998,9 +21525,13 @@
|
|
|
20998
21525
|
// Convert layout to legacy format for compatibility
|
|
20999
21526
|
this._convertLayoutToLegacyFormat(layout);
|
|
21000
21527
|
|
|
21001
|
-
//
|
|
21002
|
-
//
|
|
21003
|
-
|
|
21528
|
+
// Apply kashida if enabled for justify alignment
|
|
21529
|
+
// This must be called after _convertLayoutToLegacyFormat to ensure __charBounds exists
|
|
21530
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida && this.kashida !== 'none') {
|
|
21531
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
21532
|
+
this.enlargeSpaces();
|
|
21533
|
+
}
|
|
21534
|
+
}
|
|
21004
21535
|
this.dirty = true;
|
|
21005
21536
|
}
|
|
21006
21537
|
|
|
@@ -21012,16 +21543,27 @@
|
|
|
21012
21543
|
this._textLines = layout.lines.map(line => line.graphemes);
|
|
21013
21544
|
this.textLines = layout.lines.map(line => line.text);
|
|
21014
21545
|
|
|
21546
|
+
// Set _text as flat array of all graphemes (required for editing)
|
|
21547
|
+
this._text = layout.lines.flatMap(line => line.graphemes);
|
|
21548
|
+
|
|
21015
21549
|
// Convert bounds to legacy format
|
|
21550
|
+
// IMPORTANT: Preserve both logical (left) and visual (renderLeft) positions
|
|
21551
|
+
// - left: cumulative logical offset (for text editing operations)
|
|
21552
|
+
// - renderLeft: actual visual X position after BiDi reordering and alignment
|
|
21553
|
+
// The renderLeft is critical for correct cursor/selection hit testing in mixed RTL/LTR text
|
|
21016
21554
|
this.__charBounds = layout.lines.map(line => line.bounds.map(bound => ({
|
|
21017
21555
|
left: bound.left,
|
|
21018
21556
|
top: bound.y,
|
|
21019
21557
|
width: bound.width,
|
|
21020
21558
|
height: bound.height,
|
|
21021
21559
|
kernedWidth: bound.kernedWidth,
|
|
21022
|
-
deltaY: bound.deltaY || 0
|
|
21560
|
+
deltaY: bound.deltaY || 0,
|
|
21561
|
+
renderLeft: bound.x // Visual X position for hit testing
|
|
21023
21562
|
})));
|
|
21024
21563
|
|
|
21564
|
+
// Populate line widths cache to prevent getLineWidth from triggering legacy measurement
|
|
21565
|
+
this.__lineWidths = layout.lines.map(line => line.width);
|
|
21566
|
+
|
|
21025
21567
|
// Update grapheme info for compatibility
|
|
21026
21568
|
if (layout.lines.length > 0) {
|
|
21027
21569
|
this._unwrappedTextLines = layout.lines.map(line => line.graphemes);
|
|
@@ -21189,6 +21731,48 @@
|
|
|
21189
21731
|
this._renderChars(method, ctx, line, left, top, lineIndex);
|
|
21190
21732
|
}
|
|
21191
21733
|
|
|
21734
|
+
/**
|
|
21735
|
+
* Build display text lines with kashida characters inserted.
|
|
21736
|
+
* This creates a version of _textLines with tatweel characters added at kashida points.
|
|
21737
|
+
* @private
|
|
21738
|
+
*/
|
|
21739
|
+
_buildKashidaDisplayLines() {
|
|
21740
|
+
if (this.kashida === 'none' || !this.__kashidaInfo) {
|
|
21741
|
+
return this._textLines;
|
|
21742
|
+
}
|
|
21743
|
+
const displayLines = [];
|
|
21744
|
+
for (let lineIndex = 0; lineIndex < this._textLines.length; lineIndex++) {
|
|
21745
|
+
const line = this._textLines[lineIndex];
|
|
21746
|
+
const kashidaInfo = this.__kashidaInfo[lineIndex];
|
|
21747
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
21748
|
+
displayLines.push([...line]);
|
|
21749
|
+
continue;
|
|
21750
|
+
}
|
|
21751
|
+
|
|
21752
|
+
// Sort kashida points by charIndex descending so we can insert from the end
|
|
21753
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => b.charIndex - a.charIndex);
|
|
21754
|
+
|
|
21755
|
+
// Calculate how many tatweels to insert based on width
|
|
21756
|
+
const newLine = [...line];
|
|
21757
|
+
for (const {
|
|
21758
|
+
charIndex,
|
|
21759
|
+
width
|
|
21760
|
+
} of sortedKashida) {
|
|
21761
|
+
if (width <= 0 || charIndex >= newLine.length) continue;
|
|
21762
|
+
|
|
21763
|
+
// Calculate number of tatweel characters based on width
|
|
21764
|
+
// Each tatweel is approximately 5px at font size 24
|
|
21765
|
+
const tatweelCount = Math.max(1, Math.round(width / 3));
|
|
21766
|
+
const tatweels = ARABIC_TATWEEL.repeat(tatweelCount);
|
|
21767
|
+
|
|
21768
|
+
// Insert tatweels after the character at charIndex
|
|
21769
|
+
newLine.splice(charIndex + 1, 0, tatweels);
|
|
21770
|
+
}
|
|
21771
|
+
displayLines.push(newLine);
|
|
21772
|
+
}
|
|
21773
|
+
return displayLines;
|
|
21774
|
+
}
|
|
21775
|
+
|
|
21192
21776
|
/**
|
|
21193
21777
|
* Renders the text background for lines, taking care of style
|
|
21194
21778
|
* @private
|
|
@@ -21269,10 +21853,13 @@
|
|
|
21269
21853
|
const fontCache = cache.getFontCache(charStyle),
|
|
21270
21854
|
fontDeclaration = this._getFontDeclaration(charStyle),
|
|
21271
21855
|
couple = previousChar + _char,
|
|
21272
|
-
|
|
21856
|
+
// Skip kerning for tatweel (kashida) characters - they extend connections
|
|
21857
|
+
// and kerning would make the following character appear too narrow
|
|
21858
|
+
isTatweel = previousChar === '\u0640',
|
|
21859
|
+
stylesAreEqual = previousChar && !isTatweel && fontDeclaration === this._getFontDeclaration(prevCharStyle),
|
|
21273
21860
|
fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE;
|
|
21274
21861
|
let width, coupleWidth, previousWidth, kernedWidth;
|
|
21275
|
-
if (previousChar && fontCache[previousChar] !== undefined) {
|
|
21862
|
+
if (previousChar && !isTatweel && fontCache[previousChar] !== undefined) {
|
|
21276
21863
|
previousWidth = fontCache[previousChar];
|
|
21277
21864
|
}
|
|
21278
21865
|
if (fontCache[_char] !== undefined) {
|
|
@@ -21290,11 +21877,11 @@
|
|
|
21290
21877
|
kernedWidth = width = ctx.measureText(_char).width;
|
|
21291
21878
|
fontCache[_char] = width;
|
|
21292
21879
|
}
|
|
21293
|
-
if (previousWidth === undefined && stylesAreEqual && previousChar) {
|
|
21880
|
+
if (previousWidth === undefined && stylesAreEqual && previousChar && !isTatweel) {
|
|
21294
21881
|
previousWidth = ctx.measureText(previousChar).width;
|
|
21295
21882
|
fontCache[previousChar] = previousWidth;
|
|
21296
21883
|
}
|
|
21297
|
-
if (stylesAreEqual && coupleWidth === undefined) {
|
|
21884
|
+
if (stylesAreEqual && coupleWidth === undefined && !isTatweel) {
|
|
21298
21885
|
// we can measure the kerning couple and subtract the width of the previous character
|
|
21299
21886
|
coupleWidth = ctx.measureText(couple).width;
|
|
21300
21887
|
fontCache[couple] = coupleWidth;
|
|
@@ -21341,10 +21928,7 @@
|
|
|
21341
21928
|
*/
|
|
21342
21929
|
_measureLine(lineIndex) {
|
|
21343
21930
|
// Debug: detect if measureLine is called after justify was applied
|
|
21344
|
-
if (this._justifyApplied)
|
|
21345
|
-
console.warn(`WARNING: _measureLine called for line ${lineIndex} AFTER justify was applied! This will overwrite justified charBounds.`);
|
|
21346
|
-
console.trace('Stack trace:');
|
|
21347
|
-
}
|
|
21931
|
+
if (this._justifyApplied) ;
|
|
21348
21932
|
let width = 0,
|
|
21349
21933
|
prevGrapheme,
|
|
21350
21934
|
graphemeInfo;
|
|
@@ -21519,13 +22103,7 @@
|
|
|
21519
22103
|
top = this._getTopOffset();
|
|
21520
22104
|
|
|
21521
22105
|
// Debug: log once per render
|
|
21522
|
-
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify'))
|
|
21523
|
-
console.log('=== RENDER DEBUG ===');
|
|
21524
|
-
console.log('direction:', this.direction);
|
|
21525
|
-
console.log('textAlign:', this.textAlign);
|
|
21526
|
-
console.log('width:', this.width);
|
|
21527
|
-
console.log('_getLeftOffset:', left);
|
|
21528
|
-
}
|
|
22106
|
+
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify')) ;
|
|
21529
22107
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
21530
22108
|
var _this$textAlign2;
|
|
21531
22109
|
const heightOfLine = this.getHeightOfLine(i),
|
|
@@ -21534,8 +22112,8 @@
|
|
|
21534
22112
|
|
|
21535
22113
|
// Debug: log line offsets for justify
|
|
21536
22114
|
if (method === 'fillText' && (_this$textAlign2 = this.textAlign) !== null && _this$textAlign2 !== void 0 && _this$textAlign2.includes('justify')) {
|
|
21537
|
-
|
|
21538
|
-
console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
22115
|
+
this.getLineWidth(i);
|
|
22116
|
+
// console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
21539
22117
|
}
|
|
21540
22118
|
this._renderTextLine(method, ctx, this._textLines[i], left + leftOffset, top + lineHeights + maxHeight, i);
|
|
21541
22119
|
lineHeights += heightOfLine;
|
|
@@ -21586,12 +22164,18 @@
|
|
|
21586
22164
|
const lineHeight = this.getHeightOfLine(lineIndex),
|
|
21587
22165
|
isJustify = this.textAlign.includes(JUSTIFY),
|
|
21588
22166
|
path = this.path,
|
|
21589
|
-
shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path,
|
|
21590
22167
|
isLtr = this.direction === 'ltr',
|
|
21591
22168
|
sign = this.direction === 'ltr' ? 1 : -1,
|
|
21592
22169
|
// this was changed in the PR #7674
|
|
21593
22170
|
// currentDirection = ctx.canvas.getAttribute('dir');
|
|
21594
22171
|
currentDirection = ctx.direction;
|
|
22172
|
+
|
|
22173
|
+
// Check if we should use BiDi-aware rendering with pre-calculated positions
|
|
22174
|
+
// This is needed for advanced layout with RTL or mixed BiDi text
|
|
22175
|
+
const chars = this.__charBounds[lineIndex];
|
|
22176
|
+
this.enableAdvancedLayout && (chars === null || chars === void 0 ? void 0 : chars.length) > 0 && chars[0].renderLeft !== undefined;
|
|
22177
|
+
|
|
22178
|
+
const shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path;
|
|
21595
22179
|
let actualStyle,
|
|
21596
22180
|
nextStyle,
|
|
21597
22181
|
charsToRender = '',
|
|
@@ -21600,6 +22184,9 @@
|
|
|
21600
22184
|
timeToRender,
|
|
21601
22185
|
drawingLeft;
|
|
21602
22186
|
ctx.save();
|
|
22187
|
+
|
|
22188
|
+
// For BiDi rendering with pre-calculated positions, disable browser BiDi
|
|
22189
|
+
// and render each character at its calculated visual position
|
|
21603
22190
|
if (currentDirection !== this.direction) {
|
|
21604
22191
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
21605
22192
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
@@ -21619,12 +22206,12 @@
|
|
|
21619
22206
|
}
|
|
21620
22207
|
// Debug: Log charBounds being used for first line only during justify
|
|
21621
22208
|
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
21622
|
-
console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
21623
|
-
console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
21624
|
-
console.log('_justifyApplied flag:', this._justifyApplied);
|
|
22209
|
+
// console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
22210
|
+
// console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
22211
|
+
// console.log('_justifyApplied flag:', (this as any)._justifyApplied);
|
|
21625
22212
|
const lineBounds = this.__charBounds[lineIndex];
|
|
21626
|
-
|
|
21627
|
-
console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
22213
|
+
(lineBounds === null || lineBounds === void 0 ? void 0 : lineBounds.reduce((s, b) => s + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0)) || 0;
|
|
22214
|
+
// console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
21628
22215
|
// Log first few space widths to verify expansion
|
|
21629
22216
|
const spaceIndices = [3, 9, 15, 23, 31];
|
|
21630
22217
|
spaceIndices.forEach(idx => {
|
|
@@ -21673,10 +22260,6 @@
|
|
|
21673
22260
|
// For RTL with textAlign='right': x is the right edge, so drawingLeft = left
|
|
21674
22261
|
// Both cases: drawingLeft = left (the text alignment handles the edge correctly)
|
|
21675
22262
|
drawingLeft = left;
|
|
21676
|
-
// Debug: log first chunk positioning for justify
|
|
21677
|
-
if (isJustify && lineIndex === 0 && method === 'fillText' && i < 5) {
|
|
21678
|
-
console.log(` Chunk ending at char ${i}: left=${left.toFixed(2)}, boxWidth=${boxWidth.toFixed(2)}, drawingLeft=${drawingLeft.toFixed(2)}, textAlign=${isLtr ? 'left' : 'right'}`);
|
|
21679
|
-
}
|
|
21680
22263
|
this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top);
|
|
21681
22264
|
}
|
|
21682
22265
|
charsToRender = '';
|
|
@@ -21685,11 +22268,6 @@
|
|
|
21685
22268
|
boxWidth = 0;
|
|
21686
22269
|
}
|
|
21687
22270
|
}
|
|
21688
|
-
// Debug: log final position for justify
|
|
21689
|
-
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
21690
|
-
console.log('Final left position after rendering:', left.toFixed(2));
|
|
21691
|
-
console.log('Expected final position:', (sign > 0 ? this.width / 2 : -this.width / 2).toFixed(2));
|
|
21692
|
-
}
|
|
21693
22271
|
ctx.restore();
|
|
21694
22272
|
}
|
|
21695
22273
|
|
|
@@ -21921,12 +22499,159 @@
|
|
|
21921
22499
|
* @private
|
|
21922
22500
|
*/
|
|
21923
22501
|
_clearCache() {
|
|
22502
|
+
// console.log('🗑️ _clearCache called');
|
|
22503
|
+
// console.trace('🗑️ _clearCache stack trace');
|
|
21924
22504
|
this._forceClearCache = false;
|
|
21925
22505
|
this.__lineWidths = [];
|
|
21926
22506
|
this.__lineHeights = [];
|
|
21927
22507
|
this.__charBounds = [];
|
|
22508
|
+
this.__kashidaInfo = [];
|
|
21928
22509
|
// Reset justify applied flag
|
|
21929
22510
|
this._justifyApplied = false;
|
|
22511
|
+
// Reset dimension state to force recalculation
|
|
22512
|
+
this._lastDimensionState = null;
|
|
22513
|
+
}
|
|
22514
|
+
|
|
22515
|
+
/**
|
|
22516
|
+
* Convert a display character index (in _textLines with tatweels) to original text index.
|
|
22517
|
+
* When kashida is applied, _textLines contains extra tatweel characters that don't exist
|
|
22518
|
+
* in the original text. This method maps back to the original index.
|
|
22519
|
+
* @param lineIndex - The line index
|
|
22520
|
+
* @param displayCharIndex - Character index in the display text (with tatweels)
|
|
22521
|
+
* @returns Original character index (without tatweels)
|
|
22522
|
+
*/
|
|
22523
|
+
_displayToOriginalIndex(lineIndex, displayCharIndex) {
|
|
22524
|
+
var _this$__kashidaInfo;
|
|
22525
|
+
// console.log(`🔄 _displayToOriginalIndex called: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
22526
|
+
// console.log(`🔄 __kashidaInfo exists: ${!!this.__kashidaInfo}, length: ${this.__kashidaInfo?.length}`);
|
|
22527
|
+
// console.log(`🔄 __kashidaInfo raw:`, JSON.stringify(this.__kashidaInfo));
|
|
22528
|
+
|
|
22529
|
+
const kashidaInfo = (_this$__kashidaInfo = this.__kashidaInfo) === null || _this$__kashidaInfo === void 0 ? void 0 : _this$__kashidaInfo[lineIndex];
|
|
22530
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22531
|
+
// No kashida on this line, indices are the same
|
|
22532
|
+
// console.log(`🔄 No kashida info for line ${lineIndex}, returning same index`);
|
|
22533
|
+
return displayCharIndex;
|
|
22534
|
+
}
|
|
22535
|
+
|
|
22536
|
+
// Sort kashida info by charIndex ascending for proper traversal
|
|
22537
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22538
|
+
|
|
22539
|
+
// console.log(`🔄 _displayToOriginalIndex: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
22540
|
+
// console.log(`🔄 kashidaInfo:`, sortedKashida.map(k => `{charIdx:${k.charIndex}, cnt:${k.tatweelCount}}`).join(', '));
|
|
22541
|
+
|
|
22542
|
+
let tatweelsBeforeIndex = 0;
|
|
22543
|
+
for (const k of sortedKashida) {
|
|
22544
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22545
|
+
// Position where tatweels start (after the original character)
|
|
22546
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
22547
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
22548
|
+
|
|
22549
|
+
// console.log(`🔄 k.charIndex=${k.charIndex}, tatweelStartPos=${tatweelStartPos}, tatweelEndPos=${tatweelEndPos}, tatweelsBeforeIndex=${tatweelsBeforeIndex}`);
|
|
22550
|
+
|
|
22551
|
+
if (displayCharIndex < tatweelStartPos) {
|
|
22552
|
+
// Before this kashida point
|
|
22553
|
+
// console.log(`🔄 displayIdx < tatweelStartPos, break`);
|
|
22554
|
+
break;
|
|
22555
|
+
} else if (displayCharIndex < tatweelEndPos) {
|
|
22556
|
+
// Within tatweel characters - map to the character before tatweels
|
|
22557
|
+
// console.log(`🔄 Within tatweel, return ${k.charIndex + 1}`);
|
|
22558
|
+
return k.charIndex + 1;
|
|
22559
|
+
} else {
|
|
22560
|
+
// After this kashida point
|
|
22561
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22562
|
+
// console.log(`🔄 After this kashida, tatweelsBeforeIndex now=${tatweelsBeforeIndex}`);
|
|
22563
|
+
}
|
|
22564
|
+
}
|
|
22565
|
+
|
|
22566
|
+
// Subtract all tatweels that come before this position
|
|
22567
|
+
const result = displayCharIndex - tatweelsBeforeIndex;
|
|
22568
|
+
// console.log(`🔄 Final result: ${displayCharIndex} - ${tatweelsBeforeIndex} = ${result}`);
|
|
22569
|
+
return result;
|
|
22570
|
+
}
|
|
22571
|
+
|
|
22572
|
+
/**
|
|
22573
|
+
* Convert an original text character index to display index (in _textLines with tatweels).
|
|
22574
|
+
* @param lineIndex - The line index
|
|
22575
|
+
* @param originalCharIndex - Character index in the original text (without tatweels)
|
|
22576
|
+
* @returns Display character index (with tatweels)
|
|
22577
|
+
*/
|
|
22578
|
+
_originalToDisplayIndex(lineIndex, originalCharIndex) {
|
|
22579
|
+
var _this$__kashidaInfo2;
|
|
22580
|
+
const kashidaInfo = (_this$__kashidaInfo2 = this.__kashidaInfo) === null || _this$__kashidaInfo2 === void 0 ? void 0 : _this$__kashidaInfo2[lineIndex];
|
|
22581
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22582
|
+
// No kashida on this line, indices are the same
|
|
22583
|
+
return originalCharIndex;
|
|
22584
|
+
}
|
|
22585
|
+
|
|
22586
|
+
// Sort kashida info by charIndex ascending
|
|
22587
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22588
|
+
let tatweelsBeforeIndex = 0;
|
|
22589
|
+
for (const k of sortedKashida) {
|
|
22590
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22591
|
+
// If the original char index is after this kashida insertion point,
|
|
22592
|
+
// add the tatweels to the offset
|
|
22593
|
+
if (originalCharIndex > k.charIndex) {
|
|
22594
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22595
|
+
} else {
|
|
22596
|
+
break;
|
|
22597
|
+
}
|
|
22598
|
+
}
|
|
22599
|
+
return originalCharIndex + tatweelsBeforeIndex;
|
|
22600
|
+
}
|
|
22601
|
+
|
|
22602
|
+
/**
|
|
22603
|
+
* Check if a display character index points to a tatweel character.
|
|
22604
|
+
* @param lineIndex - The line index
|
|
22605
|
+
* @param displayCharIndex - Character index in the display text
|
|
22606
|
+
* @returns True if the character at this index is a tatweel
|
|
22607
|
+
*/
|
|
22608
|
+
_isTatweelAtDisplayIndex(lineIndex, displayCharIndex) {
|
|
22609
|
+
var _this$__kashidaInfo3;
|
|
22610
|
+
const kashidaInfo = (_this$__kashidaInfo3 = this.__kashidaInfo) === null || _this$__kashidaInfo3 === void 0 ? void 0 : _this$__kashidaInfo3[lineIndex];
|
|
22611
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22612
|
+
return false;
|
|
22613
|
+
}
|
|
22614
|
+
|
|
22615
|
+
// Sort kashida info by charIndex ascending
|
|
22616
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22617
|
+
let tatweelsBeforeIndex = 0;
|
|
22618
|
+
for (const k of sortedKashida) {
|
|
22619
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22620
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
22621
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
22622
|
+
if (displayCharIndex >= tatweelStartPos && displayCharIndex < tatweelEndPos) {
|
|
22623
|
+
return true;
|
|
22624
|
+
}
|
|
22625
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22626
|
+
}
|
|
22627
|
+
return false;
|
|
22628
|
+
}
|
|
22629
|
+
|
|
22630
|
+
/**
|
|
22631
|
+
* Get the total number of tatweel characters inserted in a line.
|
|
22632
|
+
* @param lineIndex - The line index
|
|
22633
|
+
* @returns Total number of tatweels in this line
|
|
22634
|
+
*/
|
|
22635
|
+
_getTatweelCountForLine(lineIndex) {
|
|
22636
|
+
var _this$__kashidaInfo4;
|
|
22637
|
+
const kashidaInfo = (_this$__kashidaInfo4 = this.__kashidaInfo) === null || _this$__kashidaInfo4 === void 0 ? void 0 : _this$__kashidaInfo4[lineIndex];
|
|
22638
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22639
|
+
return 0;
|
|
22640
|
+
}
|
|
22641
|
+
return kashidaInfo.reduce((sum, k) => sum + (k.tatweelCount || 0), 0);
|
|
22642
|
+
}
|
|
22643
|
+
|
|
22644
|
+
/**
|
|
22645
|
+
* Get the original line length (without tatweels).
|
|
22646
|
+
* When kashida is applied, _textLines contains extra tatweel characters.
|
|
22647
|
+
* This returns the length as it would be in the original text.
|
|
22648
|
+
* @param lineIndex - The line index
|
|
22649
|
+
* @returns Original line length without tatweels
|
|
22650
|
+
*/
|
|
22651
|
+
_getOriginalLineLength(lineIndex) {
|
|
22652
|
+
var _this$_textLines$line;
|
|
22653
|
+
const displayLength = ((_this$_textLines$line = this._textLines[lineIndex]) === null || _this$_textLines$line === void 0 ? void 0 : _this$_textLines$line.length) || 0;
|
|
22654
|
+
return displayLength - this._getTatweelCountForLine(lineIndex);
|
|
21930
22655
|
}
|
|
21931
22656
|
|
|
21932
22657
|
/**
|
|
@@ -22970,7 +23695,9 @@
|
|
|
22970
23695
|
requestAnimationFrame(() => {
|
|
22971
23696
|
if (!this.isDestroyed) {
|
|
22972
23697
|
this.applyOverlayStyle();
|
|
22973
|
-
console.log(
|
|
23698
|
+
// console.log(
|
|
23699
|
+
// '📐 Height changed - rechecking alignment after repositioning:',
|
|
23700
|
+
// );
|
|
22974
23701
|
}
|
|
22975
23702
|
});
|
|
22976
23703
|
}
|
|
@@ -23066,7 +23793,7 @@
|
|
|
23066
23793
|
|
|
23067
23794
|
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
23068
23795
|
if (target.dirty !== false && target.initDimensions) {
|
|
23069
|
-
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23796
|
+
// console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23070
23797
|
// Force re-initialization if the text object seems to be in a dirty state
|
|
23071
23798
|
target.initDimensions();
|
|
23072
23799
|
}
|
|
@@ -23083,11 +23810,11 @@
|
|
|
23083
23810
|
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
23084
23811
|
|
|
23085
23812
|
// DEBUG: Log alignment details
|
|
23086
|
-
console.log('🔍 ALIGNMENT DEBUG:');
|
|
23087
|
-
console.log(' Fabric textAlign:', textAlign);
|
|
23088
|
-
console.log(' Fabric direction:', target.direction);
|
|
23089
|
-
console.log(' Text content:', JSON.stringify(target.text));
|
|
23090
|
-
console.log(' Detected direction:', autoDetectedDirection);
|
|
23813
|
+
// console.log('🔍 ALIGNMENT DEBUG:');
|
|
23814
|
+
// console.log(' Fabric textAlign:', textAlign);
|
|
23815
|
+
// console.log(' Fabric direction:', (target as any).direction);
|
|
23816
|
+
// console.log(' Text content:', JSON.stringify(target.text));
|
|
23817
|
+
// console.log(' Detected direction:', autoDetectedDirection);
|
|
23091
23818
|
|
|
23092
23819
|
// Map fabric.js justify to CSS
|
|
23093
23820
|
if (textAlign.includes('justify')) {
|
|
@@ -23105,7 +23832,7 @@
|
|
|
23105
23832
|
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
23106
23833
|
if (autoDetectedDirection === 'rtl') {
|
|
23107
23834
|
this.textarea.style.textAlignLast = 'right';
|
|
23108
|
-
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23835
|
+
// console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23109
23836
|
} else {
|
|
23110
23837
|
this.textarea.style.textAlignLast = 'left';
|
|
23111
23838
|
}
|
|
@@ -23113,7 +23840,7 @@
|
|
|
23113
23840
|
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
23114
23841
|
if (autoDetectedDirection === 'ltr') {
|
|
23115
23842
|
this.textarea.style.textAlignLast = 'left';
|
|
23116
|
-
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23843
|
+
// console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23117
23844
|
} else {
|
|
23118
23845
|
this.textarea.style.textAlignLast = 'right';
|
|
23119
23846
|
}
|
|
@@ -23132,16 +23859,17 @@
|
|
|
23132
23859
|
// Try to force better justify behavior
|
|
23133
23860
|
this.textarea.style.textJustifyTrim = 'none';
|
|
23134
23861
|
this.textarea.style.textAutospace = 'none';
|
|
23135
|
-
|
|
23862
|
+
|
|
23863
|
+
// console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
23136
23864
|
} catch (error) {
|
|
23137
|
-
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23865
|
+
// console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23138
23866
|
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
23139
23867
|
}
|
|
23140
23868
|
} else {
|
|
23141
23869
|
this.textarea.style.textAlignLast = 'auto';
|
|
23142
23870
|
this.textarea.style.textJustify = 'auto';
|
|
23143
23871
|
this.textarea.style.wordSpacing = 'normal';
|
|
23144
|
-
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23872
|
+
// console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23145
23873
|
}
|
|
23146
23874
|
this.textarea.style.textAlign = cssTextAlign;
|
|
23147
23875
|
this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
|
|
@@ -23167,37 +23895,31 @@
|
|
|
23167
23895
|
this.textarea.style.hyphens = 'none';
|
|
23168
23896
|
|
|
23169
23897
|
// DEBUG: Log final CSS properties
|
|
23170
|
-
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23171
|
-
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23172
|
-
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23173
|
-
console.log(' direction:', this.textarea.style.direction);
|
|
23174
|
-
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23175
|
-
console.log(' width:', this.textarea.style.width);
|
|
23176
|
-
console.log(' textJustify:', this.textarea.style.textJustify);
|
|
23177
|
-
console.log(' wordSpacing:', this.textarea.style.wordSpacing);
|
|
23178
|
-
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23898
|
+
// console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23899
|
+
// console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23900
|
+
// console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23901
|
+
// console.log(' direction:', this.textarea.style.direction);
|
|
23902
|
+
// console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23903
|
+
// console.log(' width:', this.textarea.style.width);
|
|
23904
|
+
// console.log(' textJustify:', (this.textarea.style as any).textJustify);
|
|
23905
|
+
// console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
|
|
23906
|
+
// console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23179
23907
|
|
|
23180
23908
|
// If justify, log Fabric object dimensions for comparison
|
|
23181
|
-
if (textAlign.includes('justify'))
|
|
23182
|
-
var _calcTextWidth, _ref;
|
|
23183
|
-
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
23184
|
-
console.log(' Fabric width:', target.width);
|
|
23185
|
-
console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
|
|
23186
|
-
console.log(' Fabric textAlign:', target.textAlign);
|
|
23187
|
-
console.log(' Text lines:', target.textLines);
|
|
23188
|
-
}
|
|
23909
|
+
if (textAlign.includes('justify')) ;
|
|
23189
23910
|
|
|
23190
23911
|
// Debug font properties matching
|
|
23191
|
-
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23192
|
-
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23193
|
-
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23194
|
-
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23195
|
-
console.log(' Fabric fontSize:', target.fontSize);
|
|
23196
|
-
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23197
|
-
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23198
|
-
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23199
|
-
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23200
|
-
|
|
23912
|
+
// console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23913
|
+
// console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23914
|
+
// console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23915
|
+
// console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23916
|
+
// console.log(' Fabric fontSize:', target.fontSize);
|
|
23917
|
+
// console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23918
|
+
// console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23919
|
+
// console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23920
|
+
// console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23921
|
+
|
|
23922
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23201
23923
|
|
|
23202
23924
|
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
23203
23925
|
// Default to auto for more natural rendering
|
|
@@ -23212,11 +23934,12 @@
|
|
|
23212
23934
|
if (isBold) {
|
|
23213
23935
|
this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
|
|
23214
23936
|
this.textarea.style.mozOsxFontSmoothing = 'unset';
|
|
23215
|
-
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23937
|
+
// console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23216
23938
|
}
|
|
23217
|
-
|
|
23218
|
-
console.log('
|
|
23219
|
-
console.log('
|
|
23939
|
+
|
|
23940
|
+
// console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
23941
|
+
// console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
|
|
23942
|
+
// console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
|
|
23220
23943
|
|
|
23221
23944
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
23222
23945
|
}
|
|
@@ -23245,137 +23968,130 @@
|
|
|
23245
23968
|
width: canvasBounds.width * zoom,
|
|
23246
23969
|
height: canvasBounds.height * zoom
|
|
23247
23970
|
};
|
|
23248
|
-
|
|
23249
|
-
console.log('
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23971
|
+
|
|
23972
|
+
// console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
23973
|
+
// console.log('📦 Textarea Rect:', {
|
|
23974
|
+
// left: Math.round(textareaRect.left * 100) / 100,
|
|
23975
|
+
// top: Math.round(textareaRect.top * 100) / 100,
|
|
23976
|
+
// width: Math.round(textareaRect.width * 100) / 100,
|
|
23977
|
+
// height: Math.round(textareaRect.height * 100) / 100,
|
|
23978
|
+
// });
|
|
23979
|
+
// console.log('📦 Host Div Rect:', {
|
|
23980
|
+
// left: Math.round(hostRect.left * 100) / 100,
|
|
23981
|
+
// top: Math.round(hostRect.top * 100) / 100,
|
|
23982
|
+
// width: Math.round(hostRect.width * 100) / 100,
|
|
23983
|
+
// height: Math.round(hostRect.height * 100) / 100,
|
|
23984
|
+
// });
|
|
23985
|
+
// console.log('📦 Canvas Object Bounds (screen):', {
|
|
23986
|
+
// left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
23987
|
+
// top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
23988
|
+
// width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
23989
|
+
// height: Math.round(screenObjectBounds.height * 100) / 100,
|
|
23990
|
+
// });
|
|
23991
|
+
// console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
23268
23992
|
|
|
23269
23993
|
// Calculate differences
|
|
23270
|
-
|
|
23994
|
+
({
|
|
23271
23995
|
leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23272
23996
|
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23273
23997
|
widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23274
23998
|
heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
|
|
23275
|
-
};
|
|
23276
|
-
|
|
23999
|
+
});
|
|
24000
|
+
({
|
|
23277
24001
|
leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23278
24002
|
topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23279
24003
|
widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23280
24004
|
heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
|
|
23281
|
-
};
|
|
23282
|
-
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
23283
|
-
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
24005
|
+
});
|
|
23284
24006
|
|
|
23285
|
-
//
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23290
|
-
console.log(
|
|
23291
|
-
|
|
23292
|
-
|
|
24007
|
+
// console.log(
|
|
24008
|
+
// hostAligned
|
|
24009
|
+
// ? '✅ Host Div ALIGNED with canvas object'
|
|
24010
|
+
// : '❌ Host Div MISALIGNED with canvas object',
|
|
24011
|
+
// );
|
|
24012
|
+
// console.log(
|
|
24013
|
+
// textareaAligned
|
|
24014
|
+
// ? '✅ Textarea ALIGNED with canvas object'
|
|
24015
|
+
// : '❌ Textarea MISALIGNED with canvas object',
|
|
24016
|
+
// );
|
|
24017
|
+
// console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
24018
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23293
24019
|
}
|
|
23294
24020
|
|
|
23295
24021
|
/**
|
|
23296
24022
|
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
23297
24023
|
*/
|
|
23298
24024
|
debugTextWrapping() {
|
|
23299
|
-
|
|
24025
|
+
this.target;
|
|
23300
24026
|
const text = this.textarea.value;
|
|
23301
|
-
|
|
23302
|
-
console.log('
|
|
23303
|
-
console.log('📄 Text
|
|
24027
|
+
|
|
24028
|
+
// console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
24029
|
+
// console.log('📄 Text Content:', `"${text}"`);
|
|
24030
|
+
// console.log('📄 Text Length:', text.length);
|
|
23304
24031
|
|
|
23305
24032
|
// Analyze line breaks
|
|
23306
24033
|
const explicitLines = text.split('\n');
|
|
23307
|
-
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
24034
|
+
// console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
23308
24035
|
explicitLines.forEach((line, i) => {
|
|
23309
|
-
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
24036
|
+
// console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
23310
24037
|
});
|
|
23311
24038
|
|
|
23312
24039
|
// Get textarea computed styles for wrapping analysis
|
|
23313
24040
|
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
23314
|
-
console.log('📐 Textarea Wrapping Styles:');
|
|
23315
|
-
console.log(' width:', textareaStyles.width);
|
|
23316
|
-
console.log(' fontSize:', textareaStyles.fontSize);
|
|
23317
|
-
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
23318
|
-
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
23319
|
-
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
23320
|
-
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
23321
|
-
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
23322
|
-
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
23323
|
-
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
23324
|
-
console.log(' direction:', textareaStyles.direction);
|
|
23325
|
-
console.log(' textAlign:', textareaStyles.textAlign);
|
|
24041
|
+
// console.log('📐 Textarea Wrapping Styles:');
|
|
24042
|
+
// console.log(' width:', textareaStyles.width);
|
|
24043
|
+
// console.log(' fontSize:', textareaStyles.fontSize);
|
|
24044
|
+
// console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
24045
|
+
// console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
24046
|
+
// console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
24047
|
+
// console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
24048
|
+
// console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
24049
|
+
// console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
24050
|
+
// console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
24051
|
+
// console.log(' direction:', textareaStyles.direction);
|
|
24052
|
+
// console.log(' textAlign:', textareaStyles.textAlign);
|
|
23326
24053
|
|
|
23327
24054
|
// Get Fabric text object properties for comparison
|
|
23328
|
-
console.log('📐 Fabric Text Object Properties:');
|
|
23329
|
-
console.log(' width:', target.width);
|
|
23330
|
-
console.log(' fontSize:', target.fontSize);
|
|
23331
|
-
console.log(' fontFamily:', target.fontFamily);
|
|
23332
|
-
console.log(' fontWeight:', target.fontWeight);
|
|
23333
|
-
console.log(' charSpacing:', target.charSpacing);
|
|
23334
|
-
console.log(' lineHeight:', target.lineHeight);
|
|
23335
|
-
console.log(' direction:', target.direction);
|
|
23336
|
-
console.log(' textAlign:', target.textAlign);
|
|
23337
|
-
console.log(' scaleX:', target.scaleX);
|
|
23338
|
-
console.log(' scaleY:', target.scaleY);
|
|
24055
|
+
// console.log('📐 Fabric Text Object Properties:');
|
|
24056
|
+
// console.log(' width:', (target as any).width);
|
|
24057
|
+
// console.log(' fontSize:', target.fontSize);
|
|
24058
|
+
// console.log(' fontFamily:', target.fontFamily);
|
|
24059
|
+
// console.log(' fontWeight:', target.fontWeight);
|
|
24060
|
+
// console.log(' charSpacing:', target.charSpacing);
|
|
24061
|
+
// console.log(' lineHeight:', target.lineHeight);
|
|
24062
|
+
// console.log(' direction:', (target as any).direction);
|
|
24063
|
+
// console.log(' textAlign:', (target as any).textAlign);
|
|
24064
|
+
// console.log(' scaleX:', target.scaleX);
|
|
24065
|
+
// console.log(' scaleY:', target.scaleY);
|
|
23339
24066
|
|
|
23340
24067
|
// Calculate effective dimensions for comparison - use actual rendered width
|
|
23341
24068
|
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
23342
|
-
|
|
24069
|
+
this.target.getBoundingRect().width;
|
|
23343
24070
|
// Use the exact width set on textarea for comparison
|
|
23344
24071
|
const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
console.log('
|
|
23348
|
-
console.log('
|
|
23349
|
-
console.log('
|
|
23350
|
-
console.log('
|
|
23351
|
-
console.log(
|
|
23352
|
-
|
|
23353
|
-
// Check text direction and bidi handling
|
|
23354
|
-
const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
23355
|
-
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
23356
|
-
console.log('🌍 Text Direction Analysis:');
|
|
23357
|
-
console.log(' Has RTL characters:', hasRTLText);
|
|
23358
|
-
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
23359
|
-
console.log(' Textarea direction:', textareaStyles.direction);
|
|
23360
|
-
console.log(' Fabric direction:', target.direction || 'auto');
|
|
23361
|
-
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
24072
|
+
textareaComputedWidth / this.canvas.getZoom();
|
|
24073
|
+
|
|
24074
|
+
// console.log('🌍 Text Direction Analysis:');
|
|
24075
|
+
// console.log(' Has RTL characters:', hasRTLText);
|
|
24076
|
+
// console.log(' Has mixed Bidi text:', hasBidiText);
|
|
24077
|
+
// console.log(' Textarea direction:', textareaStyles.direction);
|
|
24078
|
+
// console.log(' Fabric direction:', (target as any).direction || 'auto');
|
|
24079
|
+
// console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
23362
24080
|
|
|
23363
24081
|
// Measure actual rendered line count
|
|
23364
24082
|
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
23365
24083
|
const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
|
|
23366
24084
|
const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
|
|
23367
|
-
|
|
23368
|
-
console.log('
|
|
23369
|
-
console.log(' Textarea
|
|
23370
|
-
console.log('
|
|
23371
|
-
console.log('
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
}
|
|
23378
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
24085
|
+
|
|
24086
|
+
// console.log('📊 Line Count Analysis:');
|
|
24087
|
+
// console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
24088
|
+
// console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
24089
|
+
// console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
24090
|
+
// console.log(' Explicit line breaks:', explicitLines.length);
|
|
24091
|
+
|
|
24092
|
+
if (estimatedTextareaLines > explicitLines.length) ;
|
|
24093
|
+
|
|
24094
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23379
24095
|
}
|
|
23380
24096
|
|
|
23381
24097
|
/**
|
|
@@ -23464,17 +24180,16 @@
|
|
|
23464
24180
|
|
|
23465
24181
|
// Only update direction when not explicitly set on the object
|
|
23466
24182
|
if (!hasExplicitDirection && detectedDirection && detectedDirection !== currentDirection) {
|
|
23467
|
-
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
23468
|
-
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
24183
|
+
// console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
24184
|
+
// console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
23469
24185
|
|
|
23470
24186
|
// Update the fabric object's direction
|
|
23471
24187
|
this.target.set('direction', detectedDirection);
|
|
23472
24188
|
|
|
23473
24189
|
// Force a re-render to apply the direction change
|
|
23474
24190
|
this.canvas.requestRenderAll();
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
24191
|
+
|
|
24192
|
+
// console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
23478
24193
|
}
|
|
23479
24194
|
if (this.onCommit) {
|
|
23480
24195
|
this.onCommit(finalText);
|
|
@@ -23684,7 +24399,11 @@
|
|
|
23684
24399
|
* - `\-` Matches a "-" character (char code 45).
|
|
23685
24400
|
*/
|
|
23686
24401
|
// eslint-disable-next-line no-useless-escape
|
|
23687
|
-
|
|
24402
|
+
// Word boundary characters for Latin, Arabic, and Hebrew
|
|
24403
|
+
// Latin: space, newline, punctuation
|
|
24404
|
+
// Arabic: ، (comma U+060C), ؛ (semicolon U+061B), ؟ (question U+061F), ۔ (full stop U+06D4), ـ (tatweel U+0640)
|
|
24405
|
+
// Hebrew: ׃ (sof pasuq U+05C3), ״ (gershayim U+05F4)
|
|
24406
|
+
const reNonWord = /[ \n\.,;!\?\-\u060C\u061B\u061F\u06D4\u0640\u05C3\u05F4\u2000-\u206F]/;
|
|
23688
24407
|
class ITextBehavior extends FabricText {
|
|
23689
24408
|
constructor() {
|
|
23690
24409
|
super(...arguments);
|
|
@@ -23905,12 +24624,99 @@
|
|
|
23905
24624
|
|
|
23906
24625
|
/**
|
|
23907
24626
|
* Finds index corresponding to beginning or end of a word
|
|
24627
|
+
* Uses Intl.Segmenter for proper Unicode word segmentation when available,
|
|
24628
|
+
* falls back to regex-based detection for older browsers.
|
|
23908
24629
|
* @param {Number} selectionStart Index of a character
|
|
23909
24630
|
* @param {Number} direction 1 or -1
|
|
23910
24631
|
* @return {Number} Index of the beginning or end of a word
|
|
23911
24632
|
*/
|
|
23912
24633
|
searchWordBoundary(selectionStart, direction) {
|
|
23913
|
-
|
|
24634
|
+
// Try to use Intl.Segmenter for proper Unicode word segmentation
|
|
24635
|
+
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
|
24636
|
+
return this._searchWordBoundaryWithSegmenter(selectionStart, direction);
|
|
24637
|
+
}
|
|
24638
|
+
// Fallback to regex-based detection
|
|
24639
|
+
return this._searchWordBoundaryWithRegex(selectionStart, direction);
|
|
24640
|
+
}
|
|
24641
|
+
|
|
24642
|
+
/**
|
|
24643
|
+
* Word boundary search using Intl.Segmenter (proper Unicode support)
|
|
24644
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
24645
|
+
*/
|
|
24646
|
+
_searchWordBoundaryWithSegmenter(selectionStart, direction) {
|
|
24647
|
+
// Use original text (without kashida) since indices are in original text space
|
|
24648
|
+
const originalText = this.text;
|
|
24649
|
+
const SegmenterClass = Intl.Segmenter;
|
|
24650
|
+
const segmenter = new SegmenterClass(undefined, {
|
|
24651
|
+
granularity: 'word'
|
|
24652
|
+
});
|
|
24653
|
+
const segments = Array.from(segmenter.segment(originalText));
|
|
24654
|
+
if (segments.length === 0) {
|
|
24655
|
+
return direction === -1 ? 0 : originalText.length;
|
|
24656
|
+
}
|
|
24657
|
+
|
|
24658
|
+
// Find the segment containing the cursor position
|
|
24659
|
+
let currentSegmentIdx = 0;
|
|
24660
|
+
for (let i = 0; i < segments.length; i++) {
|
|
24661
|
+
const seg = segments[i];
|
|
24662
|
+
if (selectionStart >= seg.index && selectionStart < seg.index + seg.segment.length) {
|
|
24663
|
+
currentSegmentIdx = i;
|
|
24664
|
+
break;
|
|
24665
|
+
}
|
|
24666
|
+
if (selectionStart >= seg.index + seg.segment.length) {
|
|
24667
|
+
currentSegmentIdx = i;
|
|
24668
|
+
}
|
|
24669
|
+
}
|
|
24670
|
+
|
|
24671
|
+
// Find word boundaries
|
|
24672
|
+
if (direction === -1) {
|
|
24673
|
+
// Search backwards for word start
|
|
24674
|
+
let targetIdx = currentSegmentIdx;
|
|
24675
|
+
|
|
24676
|
+
// If cursor is at the start of a segment, look at previous segment
|
|
24677
|
+
if (selectionStart === segments[targetIdx].index && targetIdx > 0) {
|
|
24678
|
+
targetIdx--;
|
|
24679
|
+
}
|
|
24680
|
+
|
|
24681
|
+
// Skip non-word segments
|
|
24682
|
+
while (targetIdx > 0 && !segments[targetIdx].isWordLike) {
|
|
24683
|
+
targetIdx--;
|
|
24684
|
+
}
|
|
24685
|
+
|
|
24686
|
+
// Return the start of the word segment
|
|
24687
|
+
if (segments[targetIdx].isWordLike) {
|
|
24688
|
+
return segments[targetIdx].index;
|
|
24689
|
+
}
|
|
24690
|
+
return 0;
|
|
24691
|
+
} else {
|
|
24692
|
+
// Search forwards for word end
|
|
24693
|
+
let targetIdx = currentSegmentIdx;
|
|
24694
|
+
|
|
24695
|
+
// If we're in a word, find its end
|
|
24696
|
+
if (segments[targetIdx].isWordLike) {
|
|
24697
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
24698
|
+
}
|
|
24699
|
+
|
|
24700
|
+
// Skip non-word segments to find next word
|
|
24701
|
+
while (targetIdx < segments.length && !segments[targetIdx].isWordLike) {
|
|
24702
|
+
targetIdx++;
|
|
24703
|
+
}
|
|
24704
|
+
|
|
24705
|
+
// Return the end of the next word segment
|
|
24706
|
+
if (targetIdx < segments.length && segments[targetIdx].isWordLike) {
|
|
24707
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
24708
|
+
}
|
|
24709
|
+
return originalText.length;
|
|
24710
|
+
}
|
|
24711
|
+
}
|
|
24712
|
+
|
|
24713
|
+
/**
|
|
24714
|
+
* Word boundary search using regex (fallback for older browsers)
|
|
24715
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
24716
|
+
*/
|
|
24717
|
+
_searchWordBoundaryWithRegex(selectionStart, direction) {
|
|
24718
|
+
// Use original text as an array of characters
|
|
24719
|
+
const text = Array.from(this.text);
|
|
23914
24720
|
// if we land on a space we move the cursor backwards
|
|
23915
24721
|
// if we are searching boundary end we move the cursor backwards ONLY if we don't land on a line break
|
|
23916
24722
|
let index = selectionStart > 0 && this._reSpace.test(text[selectionStart]) && (direction === -1 || !reNewline.test(text[selectionStart - 1])) ? selectionStart - 1 : selectionStart,
|
|
@@ -24025,8 +24831,8 @@
|
|
|
24025
24831
|
});
|
|
24026
24832
|
}
|
|
24027
24833
|
|
|
24028
|
-
/**
|
|
24029
|
-
* Commit overlay editing changes
|
|
24834
|
+
/**
|
|
24835
|
+
* Commit overlay editing changes
|
|
24030
24836
|
*/
|
|
24031
24837
|
commitOverlayEdit(text) {
|
|
24032
24838
|
// Preserve geometry to avoid nudge when layout recalculates
|
|
@@ -24037,17 +24843,17 @@
|
|
|
24037
24843
|
const prevUsingBrowserWrap = this._usingBrowserWrapping;
|
|
24038
24844
|
const hadLock = this.lockDynamicMinWidth;
|
|
24039
24845
|
this.lockDynamicMinWidth = true;
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
|
|
24046
|
-
|
|
24047
|
-
|
|
24048
|
-
|
|
24049
|
-
|
|
24050
|
-
|
|
24846
|
+
// console.log('[OverlayCommit] pre-layout', {
|
|
24847
|
+
// textLength: text?.length,
|
|
24848
|
+
// kashidas: countKashida(text),
|
|
24849
|
+
// prevWidth,
|
|
24850
|
+
// prevMinWidth,
|
|
24851
|
+
// prevUsingBrowserWrap,
|
|
24852
|
+
// hadLock,
|
|
24853
|
+
// dir: (this as any).direction,
|
|
24854
|
+
// align: (this as any).textAlign,
|
|
24855
|
+
// });
|
|
24856
|
+
|
|
24051
24857
|
const overlayEditor = this.__overlayEditor;
|
|
24052
24858
|
if (overlayEditor) {
|
|
24053
24859
|
// Extract browser lines for pixel-perfect rendering
|
|
@@ -24055,7 +24861,7 @@
|
|
|
24055
24861
|
const result = extractLinesFromDOM(overlayEditor.textareaElement);
|
|
24056
24862
|
storeBrowserLines(this, result.lines);
|
|
24057
24863
|
} catch (error) {
|
|
24058
|
-
console.warn('Failed to extract browser lines:', error);
|
|
24864
|
+
// console.warn('Failed to extract browser lines:', error);
|
|
24059
24865
|
}
|
|
24060
24866
|
}
|
|
24061
24867
|
|
|
@@ -24071,15 +24877,15 @@
|
|
|
24071
24877
|
}
|
|
24072
24878
|
this.dirty = true;
|
|
24073
24879
|
this.initDimensions();
|
|
24074
|
-
console.log('[OverlayCommit] post-layout', {
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
});
|
|
24880
|
+
// console.log('[OverlayCommit] post-layout', {
|
|
24881
|
+
// width: this.get('width'),
|
|
24882
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
24883
|
+
// usingBrowserWrap: (this as any)._usingBrowserWrapping,
|
|
24884
|
+
// lockDynamicMinWidth: (this as any).lockDynamicMinWidth,
|
|
24885
|
+
// kashidas: countKashida(this.text),
|
|
24886
|
+
// left: this.left,
|
|
24887
|
+
// top: this.top,
|
|
24888
|
+
// });
|
|
24083
24889
|
// Restore geometry after layout so the object doesn't drift
|
|
24084
24890
|
this.set({
|
|
24085
24891
|
left: prevLeft,
|
|
@@ -24089,13 +24895,13 @@
|
|
|
24089
24895
|
this.setCoords();
|
|
24090
24896
|
this.exitEditing();
|
|
24091
24897
|
this.lockDynamicMinWidth = hadLock;
|
|
24092
|
-
console.log('[OverlayCommit] final', {
|
|
24093
|
-
|
|
24094
|
-
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
});
|
|
24898
|
+
// console.log('[OverlayCommit] final', {
|
|
24899
|
+
// width: this.get('width'),
|
|
24900
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
24901
|
+
// lockRestored: hadLock,
|
|
24902
|
+
// left: this.left,
|
|
24903
|
+
// top: this.top,
|
|
24904
|
+
// });
|
|
24099
24905
|
this.fire('changed');
|
|
24100
24906
|
this.canvas && this.canvas.requestRenderAll();
|
|
24101
24907
|
}
|
|
@@ -24195,7 +25001,7 @@
|
|
|
24195
25001
|
* @private
|
|
24196
25002
|
*/
|
|
24197
25003
|
_updateTextarea() {
|
|
24198
|
-
console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
25004
|
+
// console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
24199
25005
|
this.cursorOffsetCache = {};
|
|
24200
25006
|
if (!this.hiddenTextarea) {
|
|
24201
25007
|
return;
|
|
@@ -24204,9 +25010,9 @@
|
|
|
24204
25010
|
// Sync textarea content with fabric text to prevent double-keypress issues
|
|
24205
25011
|
const currentFabricText = this.text;
|
|
24206
25012
|
if (this.hiddenTextarea.value !== currentFabricText) {
|
|
24207
|
-
console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
24208
|
-
console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
24209
|
-
console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
25013
|
+
// console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
25014
|
+
// console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
25015
|
+
// console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
24210
25016
|
this.hiddenTextarea.value = currentFabricText;
|
|
24211
25017
|
}
|
|
24212
25018
|
if (!this.inCompositionMode) {
|
|
@@ -24857,34 +25663,29 @@
|
|
|
24857
25663
|
}
|
|
24858
25664
|
|
|
24859
25665
|
// Debug log to track the double keypress issue
|
|
24860
|
-
console.log('🔤 onInput debug:', {
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
start: selectionStart,
|
|
24869
|
-
end: selectionEnd
|
|
24870
|
-
},
|
|
24871
|
-
fromPaste,
|
|
24872
|
-
inComposition: this.inCompositionMode
|
|
24873
|
-
});
|
|
25666
|
+
// console.log('🔤 onInput debug:', {
|
|
25667
|
+
// fabricText: this.text,
|
|
25668
|
+
// textareaValue: value,
|
|
25669
|
+
// fabricSelection: { start: this.selectionStart, end: this.selectionEnd },
|
|
25670
|
+
// textareaSelection: { start: selectionStart, end: selectionEnd },
|
|
25671
|
+
// fromPaste,
|
|
25672
|
+
// inComposition: this.inCompositionMode
|
|
25673
|
+
// });
|
|
24874
25674
|
|
|
24875
25675
|
// Immediate sync for simple character replacement - fix for double keypress issue
|
|
24876
25676
|
if (this.text !== value && !this.inCompositionMode) {
|
|
24877
|
-
console.log('🔤 Immediate sync: fabric text differs from textarea, syncing immediately');
|
|
24878
|
-
console.log('🔤 Before sync - fabric text:', this.text);
|
|
24879
|
-
console.log('🔤 Before sync - textarea value:', value);
|
|
24880
|
-
console.log('🔤 fromPaste:', fromPaste);
|
|
25677
|
+
// console.log('🔤 Immediate sync: fabric text differs from textarea, syncing immediately');
|
|
25678
|
+
// console.log('🔤 Before sync - fabric text:', this.text);
|
|
25679
|
+
// console.log('🔤 Before sync - textarea value:', value);
|
|
25680
|
+
// console.log('🔤 fromPaste:', fromPaste);
|
|
24881
25681
|
|
|
24882
25682
|
// Clear all relevant caches that might prevent visual updates
|
|
24883
25683
|
this.cursorOffsetCache = {};
|
|
24884
25684
|
this._browserWrapCache = null;
|
|
24885
25685
|
this._lastDimensionState = null;
|
|
24886
25686
|
this._forceClearCache = true;
|
|
24887
|
-
|
|
25687
|
+
|
|
25688
|
+
// console.log('🔤 Cleared all caches');
|
|
24888
25689
|
|
|
24889
25690
|
// Use the same logic as updateAndFire but immediately
|
|
24890
25691
|
this.updateFromTextArea();
|
|
@@ -24897,8 +25698,9 @@
|
|
|
24897
25698
|
// Remove requestRenderAll() which queues for next animation frame
|
|
24898
25699
|
this.canvas.renderAll();
|
|
24899
25700
|
}
|
|
24900
|
-
|
|
24901
|
-
console.log('🔤
|
|
25701
|
+
|
|
25702
|
+
// console.log('🔤 After updateFromTextArea - fabric text:', this.text);
|
|
25703
|
+
// console.log('🔤 Sync complete, caches cleared, synchronous render only');
|
|
24902
25704
|
return;
|
|
24903
25705
|
}
|
|
24904
25706
|
const updateAndFire = () => {
|
|
@@ -25542,8 +26344,10 @@
|
|
|
25542
26344
|
// use character left positions which are always increasing even with RTL segments
|
|
25543
26345
|
|
|
25544
26346
|
for (let j = 0; j < charLength; j++) {
|
|
26347
|
+
var _chars$left, _chars;
|
|
25545
26348
|
const charStart = lineLeftOffset + chars[j].left;
|
|
25546
|
-
|
|
26349
|
+
// For last character, use its width to calculate end position
|
|
26350
|
+
const charEnd = lineLeftOffset + ((_chars$left = (_chars = chars[j + 1]) === null || _chars === void 0 ? void 0 : _chars.left) !== null && _chars$left !== void 0 ? _chars$left : chars[j].left + chars[j].kernedWidth);
|
|
25547
26351
|
const charMiddle = (charStart + charEnd) / 2;
|
|
25548
26352
|
if (mouseOffset.x <= charMiddle) {
|
|
25549
26353
|
charIndex = lineStartIndex + j;
|
|
@@ -25554,9 +26358,24 @@
|
|
|
25554
26358
|
}
|
|
25555
26359
|
charIndex = lineStartIndex + charLength;
|
|
25556
26360
|
}
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
26361
|
+
let lineCharIndex = charIndex - lineStartIndex;
|
|
26362
|
+
|
|
26363
|
+
// Handle flipX
|
|
26364
|
+
if (this.flipX) {
|
|
26365
|
+
lineCharIndex = charLength - lineCharIndex;
|
|
26366
|
+
}
|
|
26367
|
+
|
|
26368
|
+
// Convert display index to original index (handles kashida)
|
|
26369
|
+
const originalLineCharIndex = this._displayToOriginalIndex(lineIndex, lineCharIndex);
|
|
26370
|
+
|
|
26371
|
+
// Calculate original line start (sum of original line lengths before this line)
|
|
26372
|
+
let originalLineStart = 0;
|
|
26373
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
26374
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
26375
|
+
originalLineStart += originalLineLength + this.missingNewlineOffset(i);
|
|
26376
|
+
}
|
|
26377
|
+
const originalIndex = originalLineStart + originalLineCharIndex;
|
|
26378
|
+
return Math.min(originalIndex, this.text.length);
|
|
25560
26379
|
}
|
|
25561
26380
|
}
|
|
25562
26381
|
|
|
@@ -25567,50 +26386,6 @@
|
|
|
25567
26386
|
* for interactive text editing with grapheme-aware boundaries.
|
|
25568
26387
|
*/
|
|
25569
26388
|
|
|
25570
|
-
/**
|
|
25571
|
-
* Hit test a point against laid out text to find insertion position
|
|
25572
|
-
*/
|
|
25573
|
-
function hitTest(x, y, layout, options) {
|
|
25574
|
-
if (layout.lines.length === 0) {
|
|
25575
|
-
return {
|
|
25576
|
-
lineIndex: 0,
|
|
25577
|
-
charIndex: 0,
|
|
25578
|
-
graphemeIndex: 0,
|
|
25579
|
-
isAtLineEnd: true,
|
|
25580
|
-
isAtTextEnd: true,
|
|
25581
|
-
insertionIndex: 0
|
|
25582
|
-
};
|
|
25583
|
-
}
|
|
25584
|
-
|
|
25585
|
-
// Find the line containing the y coordinate
|
|
25586
|
-
const lineResult = findLineAtY(y, layout.lines);
|
|
25587
|
-
const line = layout.lines[lineResult.lineIndex];
|
|
25588
|
-
if (!line || line.bounds.length === 0) {
|
|
25589
|
-
return {
|
|
25590
|
-
lineIndex: lineResult.lineIndex,
|
|
25591
|
-
charIndex: 0,
|
|
25592
|
-
graphemeIndex: 0,
|
|
25593
|
-
isAtLineEnd: true,
|
|
25594
|
-
isAtTextEnd: lineResult.lineIndex >= layout.lines.length - 1,
|
|
25595
|
-
insertionIndex: calculateInsertionIndex(lineResult.lineIndex, 0, layout)
|
|
25596
|
-
};
|
|
25597
|
-
}
|
|
25598
|
-
|
|
25599
|
-
// Find the character position within the line
|
|
25600
|
-
const charResult = findCharAtX(x, line);
|
|
25601
|
-
|
|
25602
|
-
// Calculate total insertion index
|
|
25603
|
-
const insertionIndex = calculateInsertionIndex(lineResult.lineIndex, charResult.graphemeIndex, layout);
|
|
25604
|
-
return {
|
|
25605
|
-
lineIndex: lineResult.lineIndex,
|
|
25606
|
-
charIndex: charResult.charIndex,
|
|
25607
|
-
graphemeIndex: charResult.graphemeIndex,
|
|
25608
|
-
isAtLineEnd: charResult.isAtLineEnd,
|
|
25609
|
-
isAtTextEnd: lineResult.lineIndex >= layout.lines.length - 1 && charResult.isAtLineEnd,
|
|
25610
|
-
insertionIndex,
|
|
25611
|
-
closestBound: charResult.closestBound
|
|
25612
|
-
};
|
|
25613
|
-
}
|
|
25614
26389
|
|
|
25615
26390
|
/**
|
|
25616
26391
|
* Get cursor rectangle for a given insertion index
|
|
@@ -25658,159 +26433,6 @@
|
|
|
25658
26433
|
};
|
|
25659
26434
|
}
|
|
25660
26435
|
|
|
25661
|
-
// Private helper functions
|
|
25662
|
-
|
|
25663
|
-
/**
|
|
25664
|
-
* Find which line contains the given Y coordinate
|
|
25665
|
-
*/
|
|
25666
|
-
function findLineAtY(y, lines) {
|
|
25667
|
-
var _lines;
|
|
25668
|
-
let currentY = 0;
|
|
25669
|
-
for (let i = 0; i < lines.length; i++) {
|
|
25670
|
-
const line = lines[i];
|
|
25671
|
-
if (y >= currentY && y < currentY + line.height) {
|
|
25672
|
-
return {
|
|
25673
|
-
lineIndex: i,
|
|
25674
|
-
offsetY: y - currentY
|
|
25675
|
-
};
|
|
25676
|
-
}
|
|
25677
|
-
currentY += line.height;
|
|
25678
|
-
}
|
|
25679
|
-
|
|
25680
|
-
// Y is past all lines - return last line
|
|
25681
|
-
return {
|
|
25682
|
-
lineIndex: lines.length - 1,
|
|
25683
|
-
offsetY: ((_lines = lines[lines.length - 1]) === null || _lines === void 0 ? void 0 : _lines.height) || 0
|
|
25684
|
-
};
|
|
25685
|
-
}
|
|
25686
|
-
|
|
25687
|
-
/**
|
|
25688
|
-
* Find character position within a line at given X coordinate
|
|
25689
|
-
*/
|
|
25690
|
-
function findCharAtX(x, line, options) {
|
|
25691
|
-
if (line.bounds.length === 0) {
|
|
25692
|
-
return {
|
|
25693
|
-
charIndex: 0,
|
|
25694
|
-
graphemeIndex: 0,
|
|
25695
|
-
isAtLineEnd: true
|
|
25696
|
-
};
|
|
25697
|
-
}
|
|
25698
|
-
|
|
25699
|
-
// Create visual ordering: sort bounds by visual X position (left-to-right)
|
|
25700
|
-
// This handles mixed LTR/RTL content where visual order != logical order
|
|
25701
|
-
const visualBounds = line.bounds.map((bound, logicalIndex) => ({
|
|
25702
|
-
bound,
|
|
25703
|
-
logicalIndex,
|
|
25704
|
-
visualX: bound.x,
|
|
25705
|
-
visualXEnd: bound.x + bound.kernedWidth
|
|
25706
|
-
})).sort((a, b) => a.visualX - b.visualX);
|
|
25707
|
-
|
|
25708
|
-
// Find leftmost and rightmost visual positions
|
|
25709
|
-
const leftmostX = visualBounds[0].visualX;
|
|
25710
|
-
const rightmostX = visualBounds[visualBounds.length - 1].visualXEnd;
|
|
25711
|
-
|
|
25712
|
-
// Handle clicks before the line starts
|
|
25713
|
-
if (x < leftmostX) {
|
|
25714
|
-
// Find the character that appears visually first
|
|
25715
|
-
const firstVisualBound = visualBounds[0];
|
|
25716
|
-
return {
|
|
25717
|
-
charIndex: firstVisualBound.bound.charIndex,
|
|
25718
|
-
graphemeIndex: firstVisualBound.bound.graphemeIndex,
|
|
25719
|
-
isAtLineEnd: false,
|
|
25720
|
-
closestBound: firstVisualBound.bound
|
|
25721
|
-
};
|
|
25722
|
-
}
|
|
25723
|
-
|
|
25724
|
-
// Handle clicks after the line ends
|
|
25725
|
-
if (x >= rightmostX) {
|
|
25726
|
-
// Find the character that appears visually last
|
|
25727
|
-
const lastVisualBound = visualBounds[visualBounds.length - 1];
|
|
25728
|
-
return {
|
|
25729
|
-
charIndex: lastVisualBound.bound.charIndex + 1,
|
|
25730
|
-
graphemeIndex: lastVisualBound.bound.graphemeIndex + 1,
|
|
25731
|
-
isAtLineEnd: true,
|
|
25732
|
-
closestBound: lastVisualBound.bound
|
|
25733
|
-
};
|
|
25734
|
-
}
|
|
25735
|
-
|
|
25736
|
-
// Find the character containing the X coordinate
|
|
25737
|
-
for (let i = 0; i < visualBounds.length; i++) {
|
|
25738
|
-
const {
|
|
25739
|
-
bound,
|
|
25740
|
-
visualX,
|
|
25741
|
-
visualXEnd
|
|
25742
|
-
} = visualBounds[i];
|
|
25743
|
-
if (x >= visualX && x < visualXEnd) {
|
|
25744
|
-
// Determine if closer to start or end of character
|
|
25745
|
-
const midpoint = visualX + (visualXEnd - visualX) / 2;
|
|
25746
|
-
const insertBeforeChar = x < midpoint;
|
|
25747
|
-
if (insertBeforeChar) {
|
|
25748
|
-
return {
|
|
25749
|
-
charIndex: bound.charIndex,
|
|
25750
|
-
graphemeIndex: bound.graphemeIndex,
|
|
25751
|
-
isAtLineEnd: false,
|
|
25752
|
-
closestBound: bound
|
|
25753
|
-
};
|
|
25754
|
-
} else {
|
|
25755
|
-
// Insert after this character
|
|
25756
|
-
return {
|
|
25757
|
-
charIndex: bound.charIndex + 1,
|
|
25758
|
-
graphemeIndex: bound.graphemeIndex + 1,
|
|
25759
|
-
isAtLineEnd: false,
|
|
25760
|
-
closestBound: bound
|
|
25761
|
-
};
|
|
25762
|
-
}
|
|
25763
|
-
}
|
|
25764
|
-
|
|
25765
|
-
// Check if x is in the gap between this character and the next
|
|
25766
|
-
if (i < visualBounds.length - 1) {
|
|
25767
|
-
const nextVisual = visualBounds[i + 1];
|
|
25768
|
-
if (x >= visualXEnd && x < nextVisual.visualX) {
|
|
25769
|
-
// Click in gap - place cursor after current character
|
|
25770
|
-
return {
|
|
25771
|
-
charIndex: bound.charIndex + 1,
|
|
25772
|
-
graphemeIndex: bound.graphemeIndex + 1,
|
|
25773
|
-
isAtLineEnd: false,
|
|
25774
|
-
closestBound: bound
|
|
25775
|
-
};
|
|
25776
|
-
}
|
|
25777
|
-
}
|
|
25778
|
-
}
|
|
25779
|
-
|
|
25780
|
-
// Fallback - find closest character
|
|
25781
|
-
const closestBound = visualBounds.reduce((closest, current) => {
|
|
25782
|
-
const closestDistance = Math.abs((closest.visualX + closest.visualXEnd) / 2 - x);
|
|
25783
|
-
const currentDistance = Math.abs((current.visualX + current.visualXEnd) / 2 - x);
|
|
25784
|
-
return currentDistance < closestDistance ? current : closest;
|
|
25785
|
-
});
|
|
25786
|
-
return {
|
|
25787
|
-
charIndex: closestBound.bound.charIndex,
|
|
25788
|
-
graphemeIndex: closestBound.bound.graphemeIndex,
|
|
25789
|
-
isAtLineEnd: false,
|
|
25790
|
-
closestBound: closestBound.bound
|
|
25791
|
-
};
|
|
25792
|
-
}
|
|
25793
|
-
|
|
25794
|
-
/**
|
|
25795
|
-
* Calculate total insertion index from line and character indices
|
|
25796
|
-
*/
|
|
25797
|
-
function calculateInsertionIndex(lineIndex, graphemeIndex, layout) {
|
|
25798
|
-
let insertionIndex = 0;
|
|
25799
|
-
|
|
25800
|
-
// Add characters from all previous lines
|
|
25801
|
-
for (let i = 0; i < lineIndex && i < layout.lines.length; i++) {
|
|
25802
|
-
insertionIndex += layout.lines[i].graphemes.length;
|
|
25803
|
-
// Add newline character (except for last line)
|
|
25804
|
-
if (i < layout.lines.length - 1) {
|
|
25805
|
-
insertionIndex += 1; // \n character
|
|
25806
|
-
}
|
|
25807
|
-
}
|
|
25808
|
-
|
|
25809
|
-
// Add characters within current line
|
|
25810
|
-
insertionIndex += graphemeIndex;
|
|
25811
|
-
return insertionIndex;
|
|
25812
|
-
}
|
|
25813
|
-
|
|
25814
26436
|
/**
|
|
25815
26437
|
* Find line and grapheme position from insertion index
|
|
25816
26438
|
*/
|
|
@@ -26024,6 +26646,20 @@
|
|
|
26024
26646
|
...IText.ownDefaults,
|
|
26025
26647
|
...options
|
|
26026
26648
|
});
|
|
26649
|
+
/**
|
|
26650
|
+
* Index where text selection starts (or where cursor is when there is no selection)
|
|
26651
|
+
* @type Number
|
|
26652
|
+
*/
|
|
26653
|
+
/**
|
|
26654
|
+
* Index where text selection ends
|
|
26655
|
+
* @type Number
|
|
26656
|
+
*/
|
|
26657
|
+
/**
|
|
26658
|
+
* Cache for visual positions per line to ensure consistency
|
|
26659
|
+
* during selection operations
|
|
26660
|
+
* @private
|
|
26661
|
+
*/
|
|
26662
|
+
_defineProperty(this, "_visualPositionsCache", new Map());
|
|
26027
26663
|
this.initBehavior();
|
|
26028
26664
|
}
|
|
26029
26665
|
|
|
@@ -26147,6 +26783,8 @@
|
|
|
26147
26783
|
// clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
|
|
26148
26784
|
// the correct position but not at every cursor animation.
|
|
26149
26785
|
this.cursorOffsetCache = {};
|
|
26786
|
+
// Clear visual positions cache on full render since dimensions may have changed
|
|
26787
|
+
this._clearVisualPositionsCache();
|
|
26150
26788
|
this.renderCursorOrSelection();
|
|
26151
26789
|
}
|
|
26152
26790
|
|
|
@@ -26174,6 +26812,9 @@
|
|
|
26174
26812
|
if (!ctx) {
|
|
26175
26813
|
return;
|
|
26176
26814
|
}
|
|
26815
|
+
// Clear cache to ensure fresh cursor position calculation
|
|
26816
|
+
// This is important during selection drag when positions change frequently
|
|
26817
|
+
this.cursorOffsetCache = {};
|
|
26177
26818
|
const boundaries = this._getCursorBoundaries();
|
|
26178
26819
|
const ancestors = this.findAncestorsWithClipPath();
|
|
26179
26820
|
const hasAncestorsWithClipping = ancestors.length > 0;
|
|
@@ -26251,12 +26892,8 @@
|
|
|
26251
26892
|
_getCursorBoundaries() {
|
|
26252
26893
|
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.selectionStart;
|
|
26253
26894
|
let skipCaching = arguments.length > 1 ? arguments[1] : undefined;
|
|
26254
|
-
//
|
|
26255
|
-
|
|
26256
|
-
return this._getCursorBoundariesAdvanced(index);
|
|
26257
|
-
}
|
|
26258
|
-
|
|
26259
|
-
// Fall back to original method
|
|
26895
|
+
// Always use original method which uses __charBounds directly
|
|
26896
|
+
// and has proper RTL handling built-in
|
|
26260
26897
|
return this._getCursorBoundariesOriginal(index, skipCaching);
|
|
26261
26898
|
}
|
|
26262
26899
|
|
|
@@ -26295,19 +26932,261 @@
|
|
|
26295
26932
|
}
|
|
26296
26933
|
|
|
26297
26934
|
/**
|
|
26298
|
-
*
|
|
26299
|
-
*
|
|
26935
|
+
* Override selection to use measureText-based visual positions
|
|
26936
|
+
* This ensures hit testing matches actual browser BiDi rendering
|
|
26300
26937
|
*/
|
|
26301
26938
|
getSelectionStartFromPointer(e) {
|
|
26302
|
-
|
|
26303
|
-
|
|
26939
|
+
// Get mouse position in object-local coordinates (origin at center)
|
|
26940
|
+
const scenePoint = this.canvas.getScenePoint(e);
|
|
26941
|
+
const localPoint = scenePoint.transform(invertTransform(this.calcTransformMatrix()));
|
|
26942
|
+
|
|
26943
|
+
// Convert to top-left origin coordinates
|
|
26944
|
+
const mouseX = localPoint.x + this.width / 2;
|
|
26945
|
+
const mouseY = localPoint.y + this.height / 2;
|
|
26946
|
+
|
|
26947
|
+
// Find the line based on Y position
|
|
26948
|
+
let height = 0,
|
|
26949
|
+
lineIndex = 0;
|
|
26950
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
26951
|
+
const lineHeight = this.getHeightOfLine(i);
|
|
26952
|
+
if (mouseY >= height && mouseY < height + lineHeight) {
|
|
26953
|
+
lineIndex = i;
|
|
26954
|
+
break;
|
|
26955
|
+
}
|
|
26956
|
+
height += lineHeight;
|
|
26957
|
+
if (i === this._textLines.length - 1) {
|
|
26958
|
+
lineIndex = i;
|
|
26959
|
+
}
|
|
26304
26960
|
}
|
|
26305
|
-
const mouseOffset = this.canvas.getScenePoint(e).transform(invertTransform(this.calcTransformMatrix())).add(new Point(-this._getLeftOffset(), -this._getTopOffset()));
|
|
26306
26961
|
|
|
26307
|
-
//
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
|
|
26962
|
+
// Calculate line start index using ORIGINAL line lengths (without tatweels)
|
|
26963
|
+
// This ensures selection indices refer to the original text, not the display text
|
|
26964
|
+
let lineStartIndex = 0;
|
|
26965
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
26966
|
+
const origLen = this._getOriginalLineLength(i);
|
|
26967
|
+
const newlineOffset = this.missingNewlineOffset(i);
|
|
26968
|
+
console.log(`📍 Line ${i}: origLen=${origLen}, displayLen=${this._textLines[i].length}, tatweels=${this._getTatweelCountForLine(i)}, newlineOffset=${newlineOffset}`);
|
|
26969
|
+
lineStartIndex += origLen + newlineOffset;
|
|
26970
|
+
}
|
|
26971
|
+
console.log(`📍 Click on line ${lineIndex}, lineStartIndex=${lineStartIndex}`);
|
|
26972
|
+
const line = this._textLines[lineIndex];
|
|
26973
|
+
const lineText = line.join('');
|
|
26974
|
+
const displayCharLength = line.length;
|
|
26975
|
+
const originalCharLength = this._getOriginalLineLength(lineIndex);
|
|
26976
|
+
if (displayCharLength === 0) {
|
|
26977
|
+
return lineStartIndex;
|
|
26978
|
+
}
|
|
26979
|
+
|
|
26980
|
+
// Use measureText to get actual visual character positions
|
|
26981
|
+
// This matches exactly how the canvas renders BiDi text
|
|
26982
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
26983
|
+
|
|
26984
|
+
// Calculate line offset based on alignment
|
|
26985
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
26986
|
+
let lineStartX = 0;
|
|
26987
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
26988
|
+
lineStartX = (this.width - lineWidth) / 2;
|
|
26989
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
26990
|
+
lineStartX = this.width - lineWidth;
|
|
26991
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
26992
|
+
// For RTL with left/justify, text starts from right
|
|
26993
|
+
lineStartX = this.width - lineWidth;
|
|
26994
|
+
}
|
|
26995
|
+
|
|
26996
|
+
// Find which character was clicked based on visual position
|
|
26997
|
+
const clickX = mouseX - lineStartX;
|
|
26998
|
+
|
|
26999
|
+
// Sort positions by visual X for hit testing
|
|
27000
|
+
const sortedPositions = [...visualPositions].sort((a, b) => a.visualX - b.visualX);
|
|
27001
|
+
|
|
27002
|
+
// Handle click before first character
|
|
27003
|
+
if (sortedPositions.length > 0 && clickX < sortedPositions[0].visualX) {
|
|
27004
|
+
// Before first visual character - cursor at visual left edge
|
|
27005
|
+
// For RTL base direction, this means logical end of line
|
|
27006
|
+
return this.direction === 'rtl' ? lineStartIndex + originalCharLength : lineStartIndex;
|
|
27007
|
+
}
|
|
27008
|
+
|
|
27009
|
+
// Handle click after last character
|
|
27010
|
+
if (sortedPositions.length > 0) {
|
|
27011
|
+
const lastPos = sortedPositions[sortedPositions.length - 1];
|
|
27012
|
+
if (clickX > lastPos.visualX + lastPos.width) {
|
|
27013
|
+
// After last visual character - cursor at visual right edge
|
|
27014
|
+
// For RTL base direction, this means logical start of line
|
|
27015
|
+
return this.direction === 'rtl' ? lineStartIndex : lineStartIndex + originalCharLength;
|
|
27016
|
+
}
|
|
27017
|
+
}
|
|
27018
|
+
|
|
27019
|
+
// Find the character at click position
|
|
27020
|
+
for (let i = 0; i < sortedPositions.length; i++) {
|
|
27021
|
+
const pos = sortedPositions[i];
|
|
27022
|
+
const charEnd = pos.visualX + pos.width;
|
|
27023
|
+
if (clickX >= pos.visualX && clickX <= charEnd) {
|
|
27024
|
+
// Convert display index to original index
|
|
27025
|
+
// This also handles tatweels - they map to the character they extend
|
|
27026
|
+
const originalCharIndex = this._displayToOriginalIndex(lineIndex, pos.logicalIndex);
|
|
27027
|
+
|
|
27028
|
+
// Check if this is a tatweel - if so, treat click as clicking on the extended character
|
|
27029
|
+
const isTatweel = this._isTatweelAtDisplayIndex(lineIndex, pos.logicalIndex);
|
|
27030
|
+
console.log(`📍 Hit char: displayIdx=${pos.logicalIndex}, origIdx=${originalCharIndex}, isTatweel=${isTatweel}, char="${this._textLines[lineIndex][pos.logicalIndex]}"`);
|
|
27031
|
+
const charMiddle = pos.visualX + pos.width / 2;
|
|
27032
|
+
const clickedLeftHalf = clickX <= charMiddle;
|
|
27033
|
+
|
|
27034
|
+
// For tatweels, clicking anywhere on it should place cursor after the extended character
|
|
27035
|
+
if (isTatweel) {
|
|
27036
|
+
// Tatweel extends the character before it, so cursor goes after that character
|
|
27037
|
+
// originalCharIndex from _displayToOriginalIndex already maps tatweel to char+1
|
|
27038
|
+
const result = lineStartIndex + originalCharIndex;
|
|
27039
|
+
console.log(`📍 Tatweel click result: ${result}`);
|
|
27040
|
+
return result;
|
|
27041
|
+
}
|
|
27042
|
+
|
|
27043
|
+
// For RTL characters: left visual half means cursor AFTER (higher logical index)
|
|
27044
|
+
// For LTR characters: left visual half means cursor BEFORE (lower logical index)
|
|
27045
|
+
if (pos.isRtl) {
|
|
27046
|
+
// RTL character
|
|
27047
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex + 1 : originalCharIndex);
|
|
27048
|
+
console.log(`📍 RTL char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
27049
|
+
return result;
|
|
27050
|
+
} else {
|
|
27051
|
+
// LTR character
|
|
27052
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex : originalCharIndex + 1);
|
|
27053
|
+
console.log(`📍 LTR char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
27054
|
+
return result;
|
|
27055
|
+
}
|
|
27056
|
+
}
|
|
27057
|
+
}
|
|
27058
|
+
|
|
27059
|
+
// console.log(`📍 No match, returning end: ${lineStartIndex + originalCharLength}`);
|
|
27060
|
+
return lineStartIndex + originalCharLength;
|
|
27061
|
+
}
|
|
27062
|
+
|
|
27063
|
+
/**
|
|
27064
|
+
* Clear the visual positions cache
|
|
27065
|
+
* Should be called when text content or dimensions change
|
|
27066
|
+
*/
|
|
27067
|
+
_clearVisualPositionsCache() {
|
|
27068
|
+
this._visualPositionsCache.clear();
|
|
27069
|
+
}
|
|
27070
|
+
|
|
27071
|
+
/**
|
|
27072
|
+
* Measure visual character positions for hit testing using BiDi analysis
|
|
27073
|
+
* This properly handles mixed RTL/LTR text by analyzing BiDi runs
|
|
27074
|
+
* Results are cached per line for consistency during selection operations
|
|
27075
|
+
*/
|
|
27076
|
+
_measureVisualPositions(lineIndex, lineText) {
|
|
27077
|
+
// Check cache first
|
|
27078
|
+
if (this._visualPositionsCache.has(lineIndex)) {
|
|
27079
|
+
return this._visualPositionsCache.get(lineIndex);
|
|
27080
|
+
}
|
|
27081
|
+
const line = this._textLines[lineIndex];
|
|
27082
|
+
const positions = [];
|
|
27083
|
+
const chars = this.__charBounds[lineIndex];
|
|
27084
|
+
if (!chars || chars.length === 0) {
|
|
27085
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27086
|
+
return positions;
|
|
27087
|
+
}
|
|
27088
|
+
|
|
27089
|
+
// For LTR direction, use logical positions directly
|
|
27090
|
+
if (this.direction !== 'rtl') {
|
|
27091
|
+
for (let i = 0; i < line.length; i++) {
|
|
27092
|
+
var _chars$i, _chars$i2;
|
|
27093
|
+
positions.push({
|
|
27094
|
+
logicalIndex: i,
|
|
27095
|
+
visualX: ((_chars$i = chars[i]) === null || _chars$i === void 0 ? void 0 : _chars$i.left) || 0,
|
|
27096
|
+
width: ((_chars$i2 = chars[i]) === null || _chars$i2 === void 0 ? void 0 : _chars$i2.kernedWidth) || 0,
|
|
27097
|
+
isRtl: false
|
|
27098
|
+
});
|
|
27099
|
+
}
|
|
27100
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27101
|
+
return positions;
|
|
27102
|
+
}
|
|
27103
|
+
|
|
27104
|
+
// For RTL, use BiDi analysis to determine visual positions
|
|
27105
|
+
const runs = analyzeBiDi(lineText, 'rtl');
|
|
27106
|
+
|
|
27107
|
+
// Build mapping from string position to grapheme index
|
|
27108
|
+
// This is needed because analyzeBiDi works on string positions (code points)
|
|
27109
|
+
// but we need grapheme indices for charBounds
|
|
27110
|
+
const stringPosToGrapheme = [];
|
|
27111
|
+
let strPos = 0;
|
|
27112
|
+
for (let gi = 0; gi < line.length; gi++) {
|
|
27113
|
+
const grapheme = line[gi];
|
|
27114
|
+
for (let j = 0; j < grapheme.length; j++) {
|
|
27115
|
+
stringPosToGrapheme[strPos + j] = gi;
|
|
27116
|
+
}
|
|
27117
|
+
strPos += grapheme.length;
|
|
27118
|
+
}
|
|
27119
|
+
|
|
27120
|
+
// Calculate width for each run
|
|
27121
|
+
|
|
27122
|
+
const runInfos = [];
|
|
27123
|
+
for (const run of runs) {
|
|
27124
|
+
const runChars = [];
|
|
27125
|
+
let runWidth = 0;
|
|
27126
|
+
const seenGraphemes = new Set();
|
|
27127
|
+
|
|
27128
|
+
// Map string positions in this run to grapheme indices
|
|
27129
|
+
for (let sp = run.start; sp < run.end; sp++) {
|
|
27130
|
+
const gi = stringPosToGrapheme[sp];
|
|
27131
|
+
if (gi !== undefined && !seenGraphemes.has(gi)) {
|
|
27132
|
+
var _chars$gi;
|
|
27133
|
+
seenGraphemes.add(gi);
|
|
27134
|
+
runChars.push(gi);
|
|
27135
|
+
runWidth += ((_chars$gi = chars[gi]) === null || _chars$gi === void 0 ? void 0 : _chars$gi.kernedWidth) || 0;
|
|
27136
|
+
}
|
|
27137
|
+
}
|
|
27138
|
+
runInfos.push({
|
|
27139
|
+
run,
|
|
27140
|
+
width: runWidth,
|
|
27141
|
+
charIndices: runChars
|
|
27142
|
+
});
|
|
27143
|
+
}
|
|
27144
|
+
|
|
27145
|
+
// For RTL base direction, runs are displayed right-to-left
|
|
27146
|
+
// So first run appears on the right, last run on the left
|
|
27147
|
+
const totalWidth = this.getLineWidth(lineIndex);
|
|
27148
|
+
let visualX = totalWidth; // Start from right edge
|
|
27149
|
+
|
|
27150
|
+
for (const runInfo of runInfos) {
|
|
27151
|
+
visualX -= runInfo.width; // Move left by run width
|
|
27152
|
+
|
|
27153
|
+
const isRtlRun = runInfo.run.direction === 'rtl';
|
|
27154
|
+
if (isRtlRun) {
|
|
27155
|
+
// RTL run: characters displayed right-to-left within run
|
|
27156
|
+
// First char of run at visual right of run, last at visual left
|
|
27157
|
+
let charX = visualX + runInfo.width;
|
|
27158
|
+
for (const idx of runInfo.charIndices) {
|
|
27159
|
+
var _chars$idx;
|
|
27160
|
+
const charWidth = ((_chars$idx = chars[idx]) === null || _chars$idx === void 0 ? void 0 : _chars$idx.kernedWidth) || 0;
|
|
27161
|
+
charX -= charWidth;
|
|
27162
|
+
positions.push({
|
|
27163
|
+
logicalIndex: idx,
|
|
27164
|
+
visualX: charX,
|
|
27165
|
+
width: charWidth,
|
|
27166
|
+
isRtl: true
|
|
27167
|
+
});
|
|
27168
|
+
}
|
|
27169
|
+
} else {
|
|
27170
|
+
// LTR run: characters displayed left-to-right within run
|
|
27171
|
+
// First char of run at visual left of run, last at visual right
|
|
27172
|
+
let charX = visualX;
|
|
27173
|
+
for (const idx of runInfo.charIndices) {
|
|
27174
|
+
var _chars$idx2;
|
|
27175
|
+
const charWidth = ((_chars$idx2 = chars[idx]) === null || _chars$idx2 === void 0 ? void 0 : _chars$idx2.kernedWidth) || 0;
|
|
27176
|
+
positions.push({
|
|
27177
|
+
logicalIndex: idx,
|
|
27178
|
+
visualX: charX,
|
|
27179
|
+
width: charWidth,
|
|
27180
|
+
isRtl: false
|
|
27181
|
+
});
|
|
27182
|
+
charX += charWidth;
|
|
27183
|
+
}
|
|
27184
|
+
}
|
|
27185
|
+
}
|
|
27186
|
+
|
|
27187
|
+
// Cache the result
|
|
27188
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27189
|
+
return positions;
|
|
26311
27190
|
}
|
|
26312
27191
|
|
|
26313
27192
|
/**
|
|
@@ -26327,40 +27206,140 @@
|
|
|
26327
27206
|
}
|
|
26328
27207
|
|
|
26329
27208
|
/**
|
|
26330
|
-
* Calculates cursor left/top offset relative to
|
|
27209
|
+
* Calculates cursor left/top offset relative to _getLeftOffset()
|
|
27210
|
+
* Uses visual positions for BiDi text support
|
|
27211
|
+
* Handles kashida by converting original indices to display indices
|
|
26331
27212
|
* @private
|
|
26332
|
-
* @param {number} index index from start
|
|
27213
|
+
* @param {number} index index from start (in original text space, without tatweels)
|
|
26333
27214
|
*/
|
|
26334
27215
|
__getCursorBoundariesOffsets(index) {
|
|
26335
|
-
let topOffset = 0
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
27216
|
+
let topOffset = 0;
|
|
27217
|
+
|
|
27218
|
+
// Find line index and original char index using original line lengths
|
|
27219
|
+
let lineIndex = 0;
|
|
27220
|
+
let originalCharIndex = index;
|
|
27221
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27222
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27223
|
+
if (originalCharIndex <= originalLineLength) {
|
|
27224
|
+
lineIndex = i;
|
|
27225
|
+
break;
|
|
27226
|
+
}
|
|
27227
|
+
originalCharIndex -= originalLineLength + this.missingNewlineOffset(i);
|
|
27228
|
+
lineIndex = i + 1;
|
|
27229
|
+
}
|
|
27230
|
+
|
|
27231
|
+
// Clamp lineIndex to valid range
|
|
27232
|
+
if (lineIndex >= this._textLines.length) {
|
|
27233
|
+
lineIndex = this._textLines.length - 1;
|
|
27234
|
+
originalCharIndex = this._getOriginalLineLength(lineIndex);
|
|
27235
|
+
}
|
|
26341
27236
|
for (let i = 0; i < lineIndex; i++) {
|
|
26342
27237
|
topOffset += this.getHeightOfLine(i);
|
|
26343
27238
|
}
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
27239
|
+
|
|
27240
|
+
// Convert original char index to display char index for visual lookup
|
|
27241
|
+
const displayCharIndex = this._originalToDisplayIndex(lineIndex, originalCharIndex);
|
|
27242
|
+
|
|
27243
|
+
// Get visual positions for cursor placement
|
|
27244
|
+
const lineText = this._textLines[lineIndex].join('');
|
|
27245
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
27246
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
27247
|
+
this._textLines[lineIndex].length;
|
|
27248
|
+
const originalLineLength = this._getOriginalLineLength(lineIndex);
|
|
27249
|
+
|
|
27250
|
+
// Find visual X position for cursor (0 to lineWidth, from visual left)
|
|
27251
|
+
let visualX = 0;
|
|
27252
|
+
if (visualPositions.length === 0) {
|
|
27253
|
+
// Fallback for empty line
|
|
27254
|
+
return {
|
|
27255
|
+
top: topOffset,
|
|
27256
|
+
left: 0
|
|
27257
|
+
};
|
|
26349
27258
|
}
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
}
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
27259
|
+
if (originalCharIndex === 0) {
|
|
27260
|
+
// Cursor at logical start
|
|
27261
|
+
// For RTL base direction, logical start is at visual right
|
|
27262
|
+
if (this.direction === 'rtl') {
|
|
27263
|
+
visualX = lineWidth; // Right edge
|
|
27264
|
+
} else {
|
|
27265
|
+
visualX = 0; // Left edge
|
|
27266
|
+
}
|
|
27267
|
+
} else if (originalCharIndex >= originalLineLength) {
|
|
27268
|
+
// Cursor at logical end
|
|
27269
|
+
// For RTL base direction, logical end is at visual left
|
|
27270
|
+
if (this.direction === 'rtl') {
|
|
27271
|
+
visualX = 0; // Left edge
|
|
27272
|
+
} else {
|
|
27273
|
+
visualX = lineWidth; // Right edge
|
|
27274
|
+
}
|
|
27275
|
+
} else {
|
|
27276
|
+
// Cursor between characters - find visual position of character at displayCharIndex
|
|
27277
|
+
const charPos = visualPositions.find(p => p.logicalIndex === displayCharIndex);
|
|
27278
|
+
if (charPos) {
|
|
27279
|
+
// Use character's direction to determine cursor position
|
|
27280
|
+
// For RTL char: cursor "before" it appears at its right visual edge
|
|
27281
|
+
// For LTR char: cursor "before" it appears at its left visual edge
|
|
27282
|
+
if (charPos.isRtl) {
|
|
27283
|
+
visualX = charPos.visualX + charPos.width;
|
|
27284
|
+
} else {
|
|
27285
|
+
visualX = charPos.visualX;
|
|
27286
|
+
}
|
|
27287
|
+
} else {
|
|
27288
|
+
// Fallback - try the previous character in display space
|
|
27289
|
+
const prevDisplayIndex = displayCharIndex > 0 ? displayCharIndex - 1 : 0;
|
|
27290
|
+
const prevCharPos = visualPositions.find(p => p.logicalIndex === prevDisplayIndex);
|
|
27291
|
+
if (prevCharPos) {
|
|
27292
|
+
// Cursor after previous character
|
|
27293
|
+
if (prevCharPos.isRtl) {
|
|
27294
|
+
visualX = prevCharPos.visualX;
|
|
27295
|
+
} else {
|
|
27296
|
+
visualX = prevCharPos.visualX + prevCharPos.width;
|
|
27297
|
+
}
|
|
27298
|
+
} else {
|
|
27299
|
+
// Ultimate fallback
|
|
27300
|
+
const bound = this.__charBounds[lineIndex][displayCharIndex];
|
|
27301
|
+
visualX = (bound === null || bound === void 0 ? void 0 : bound.left) || 0;
|
|
27302
|
+
}
|
|
26361
27303
|
}
|
|
26362
27304
|
}
|
|
26363
|
-
|
|
27305
|
+
|
|
27306
|
+
// Calculate alignment offset (how much line is shifted from left edge)
|
|
27307
|
+
let alignOffset = 0;
|
|
27308
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
27309
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
27310
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
27311
|
+
alignOffset = this.width - lineWidth;
|
|
27312
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
27313
|
+
alignOffset = this.width - lineWidth;
|
|
27314
|
+
}
|
|
27315
|
+
|
|
27316
|
+
// The returned left value is added to _getLeftOffset() in _getCursorBoundaries
|
|
27317
|
+
// _getLeftOffset() returns -width/2 for LTR, +width/2 for RTL
|
|
27318
|
+
// Final cursor X = _getLeftOffset() + leftOffset
|
|
27319
|
+
//
|
|
27320
|
+
// For LTR: cursor X = -width/2 + (alignOffset + visualX)
|
|
27321
|
+
// For RTL: cursor X = +width/2 + leftOffset
|
|
27322
|
+
// We want cursor at: -width/2 + alignOffset + visualX
|
|
27323
|
+
// So leftOffset = -width/2 + alignOffset + visualX - width/2 = alignOffset + visualX - width
|
|
27324
|
+
|
|
27325
|
+
let leftOffset;
|
|
27326
|
+
if (this.direction === 'rtl') {
|
|
27327
|
+
// For RTL, _getLeftOffset() = +width/2
|
|
27328
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
27329
|
+
// So: +width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
27330
|
+
// leftOffset = -width + alignOffset + visualX
|
|
27331
|
+
leftOffset = -this.width + alignOffset + visualX;
|
|
27332
|
+
} else {
|
|
27333
|
+
// For LTR, _getLeftOffset() = -width/2
|
|
27334
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
27335
|
+
// So: -width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
27336
|
+
// leftOffset = alignOffset + visualX
|
|
27337
|
+
leftOffset = alignOffset + visualX;
|
|
27338
|
+
}
|
|
27339
|
+
return {
|
|
27340
|
+
top: topOffset,
|
|
27341
|
+
left: leftOffset
|
|
27342
|
+
};
|
|
26364
27343
|
}
|
|
26365
27344
|
|
|
26366
27345
|
/**
|
|
@@ -26452,51 +27431,119 @@
|
|
|
26452
27431
|
}
|
|
26453
27432
|
|
|
26454
27433
|
/**
|
|
26455
|
-
* Renders text selection
|
|
27434
|
+
* Renders text selection using visual positions for BiDi support
|
|
27435
|
+
* Handles kashida by converting original indices to display indices
|
|
26456
27436
|
* @private
|
|
26457
|
-
* @param {{ selectionStart: number, selectionEnd: number }} selection
|
|
27437
|
+
* @param {{ selectionStart: number, selectionEnd: number }} selection (in original text space)
|
|
26458
27438
|
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
|
|
26459
27439
|
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
|
|
26460
27440
|
*/
|
|
26461
27441
|
_renderSelection(ctx, selection, boundaries) {
|
|
26462
27442
|
const selectionStart = selection.selectionStart,
|
|
26463
27443
|
selectionEnd = selection.selectionEnd,
|
|
26464
|
-
isJustify = this.textAlign.includes(JUSTIFY)
|
|
26465
|
-
|
|
26466
|
-
|
|
26467
|
-
|
|
26468
|
-
|
|
26469
|
-
|
|
26470
|
-
|
|
27444
|
+
isJustify = this.textAlign.includes(JUSTIFY);
|
|
27445
|
+
|
|
27446
|
+
// Convert selection indices to line/char using original text space
|
|
27447
|
+
// This handles kashida properly since selection indices don't include tatweels
|
|
27448
|
+
let startLine = 0,
|
|
27449
|
+
endLine = 0;
|
|
27450
|
+
let originalStartChar = selectionStart,
|
|
27451
|
+
originalEndChar = selectionEnd;
|
|
27452
|
+
|
|
27453
|
+
// Find start line and char
|
|
27454
|
+
let charCount = 0;
|
|
27455
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27456
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27457
|
+
if (charCount + originalLineLength >= selectionStart) {
|
|
27458
|
+
startLine = i;
|
|
27459
|
+
originalStartChar = selectionStart - charCount;
|
|
27460
|
+
break;
|
|
27461
|
+
}
|
|
27462
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
27463
|
+
}
|
|
27464
|
+
|
|
27465
|
+
// Find end line and char
|
|
27466
|
+
charCount = 0;
|
|
27467
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27468
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27469
|
+
if (charCount + originalLineLength >= selectionEnd) {
|
|
27470
|
+
endLine = i;
|
|
27471
|
+
originalEndChar = selectionEnd - charCount;
|
|
27472
|
+
break;
|
|
27473
|
+
}
|
|
27474
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
27475
|
+
if (i === this._textLines.length - 1) {
|
|
27476
|
+
endLine = i;
|
|
27477
|
+
originalEndChar = originalLineLength;
|
|
27478
|
+
}
|
|
27479
|
+
}
|
|
26471
27480
|
for (let i = startLine; i <= endLine; i++) {
|
|
26472
|
-
const lineOffset = this._getLineLeftOffset(i) || 0;
|
|
26473
27481
|
let lineHeight = this.getHeightOfLine(i),
|
|
26474
|
-
realLineHeight = 0
|
|
26475
|
-
boxStart = 0,
|
|
26476
|
-
boxEnd = 0;
|
|
27482
|
+
realLineHeight = 0;
|
|
26477
27483
|
|
|
26478
|
-
//
|
|
27484
|
+
// Get visual positions for this line
|
|
27485
|
+
const lineText = this._textLines[i].join('');
|
|
27486
|
+
const visualPositions = this._measureVisualPositions(i, lineText);
|
|
27487
|
+
this._textLines[i].length;
|
|
27488
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27489
|
+
|
|
27490
|
+
// Calculate selection bounds in original space, then convert to display
|
|
27491
|
+
let originalLineStartChar = 0;
|
|
27492
|
+
let originalLineEndChar = originalLineLength;
|
|
26479
27493
|
if (i === startLine) {
|
|
26480
|
-
|
|
27494
|
+
originalLineStartChar = originalStartChar;
|
|
27495
|
+
}
|
|
27496
|
+
if (i === endLine) {
|
|
27497
|
+
originalLineEndChar = originalEndChar;
|
|
27498
|
+
}
|
|
27499
|
+
|
|
27500
|
+
// Convert original char indices to display indices for visual lookup
|
|
27501
|
+
const displayLineStartChar = this._originalToDisplayIndex(i, originalLineStartChar);
|
|
27502
|
+
const displayLineEndChar = this._originalToDisplayIndex(i, originalLineEndChar);
|
|
27503
|
+
|
|
27504
|
+
// Get visual X positions for selection range
|
|
27505
|
+
let minVisualX = Infinity;
|
|
27506
|
+
let maxVisualX = -Infinity;
|
|
27507
|
+
for (const pos of visualPositions) {
|
|
27508
|
+
if (pos.logicalIndex >= displayLineStartChar && pos.logicalIndex < displayLineEndChar) {
|
|
27509
|
+
minVisualX = Math.min(minVisualX, pos.visualX);
|
|
27510
|
+
maxVisualX = Math.max(maxVisualX, pos.visualX + pos.width);
|
|
27511
|
+
}
|
|
26481
27512
|
}
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
if (
|
|
26486
|
-
|
|
27513
|
+
|
|
27514
|
+
// Handle edge cases
|
|
27515
|
+
if (minVisualX === Infinity || maxVisualX === -Infinity) {
|
|
27516
|
+
if (i >= startLine && i < endLine) {
|
|
27517
|
+
// Full line selection
|
|
27518
|
+
minVisualX = 0;
|
|
27519
|
+
maxVisualX = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5;
|
|
26487
27520
|
} else {
|
|
26488
|
-
|
|
26489
|
-
boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width - charSpacing;
|
|
27521
|
+
continue; // No selection on this line
|
|
26490
27522
|
}
|
|
26491
27523
|
}
|
|
26492
27524
|
realLineHeight = lineHeight;
|
|
26493
27525
|
if (this.lineHeight < 1 || i === endLine && this.lineHeight > 1) {
|
|
26494
27526
|
lineHeight /= this.lineHeight;
|
|
26495
27527
|
}
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
27528
|
+
|
|
27529
|
+
// Calculate draw position
|
|
27530
|
+
// Visual positions are relative to line start (0 to lineWidth)
|
|
27531
|
+
// Need to add alignment offset
|
|
27532
|
+
const lineWidth = this.getLineWidth(i);
|
|
27533
|
+
let alignOffset = 0;
|
|
27534
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
27535
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
27536
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
27537
|
+
alignOffset = this.width - lineWidth;
|
|
27538
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
27539
|
+
alignOffset = this.width - lineWidth;
|
|
27540
|
+
}
|
|
27541
|
+
|
|
27542
|
+
// Draw from center origin (-width/2 to width/2)
|
|
27543
|
+
const drawStart = -this.width / 2 + alignOffset + minVisualX;
|
|
27544
|
+
const drawWidth = maxVisualX - minVisualX;
|
|
27545
|
+
let drawHeight = lineHeight;
|
|
27546
|
+
let extraTop = 0;
|
|
26500
27547
|
if (this.inCompositionMode) {
|
|
26501
27548
|
ctx.fillStyle = this.compositionColor || 'black';
|
|
26502
27549
|
drawHeight = 1;
|
|
@@ -26504,15 +27551,6 @@
|
|
|
26504
27551
|
} else {
|
|
26505
27552
|
ctx.fillStyle = this.selectionColor;
|
|
26506
27553
|
}
|
|
26507
|
-
if (this.direction === 'rtl') {
|
|
26508
|
-
if (this.textAlign === RIGHT || this.textAlign === JUSTIFY || this.textAlign === JUSTIFY_RIGHT) {
|
|
26509
|
-
drawStart = this.width - drawStart - drawWidth;
|
|
26510
|
-
} else if (this.textAlign === LEFT || this.textAlign === JUSTIFY_LEFT) {
|
|
26511
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
26512
|
-
} else if (this.textAlign === CENTER || this.textAlign === JUSTIFY_CENTER) {
|
|
26513
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
26514
|
-
}
|
|
26515
|
-
}
|
|
26516
27554
|
ctx.fillRect(drawStart, boundaries.top + boundaries.topOffset + extraTop, drawWidth, drawHeight);
|
|
26517
27555
|
boundaries.topOffset += realLineHeight;
|
|
26518
27556
|
}
|
|
@@ -26561,53 +27599,6 @@
|
|
|
26561
27599
|
super.dispose();
|
|
26562
27600
|
}
|
|
26563
27601
|
}
|
|
26564
|
-
/**
|
|
26565
|
-
* Index where text selection starts (or where cursor is when there is no selection)
|
|
26566
|
-
* @type Number
|
|
26567
|
-
*/
|
|
26568
|
-
/**
|
|
26569
|
-
* Index where text selection ends
|
|
26570
|
-
* @type Number
|
|
26571
|
-
*/
|
|
26572
|
-
/**
|
|
26573
|
-
* Color of text selection
|
|
26574
|
-
* @type String
|
|
26575
|
-
*/
|
|
26576
|
-
/**
|
|
26577
|
-
* Indicates whether text is in editing mode
|
|
26578
|
-
* @type Boolean
|
|
26579
|
-
*/
|
|
26580
|
-
/**
|
|
26581
|
-
* Indicates whether a text can be edited
|
|
26582
|
-
* @type Boolean
|
|
26583
|
-
*/
|
|
26584
|
-
/**
|
|
26585
|
-
* Border color of text object while it's in editing mode
|
|
26586
|
-
* @type String
|
|
26587
|
-
*/
|
|
26588
|
-
/**
|
|
26589
|
-
* Width of cursor (in px)
|
|
26590
|
-
* @type Number
|
|
26591
|
-
*/
|
|
26592
|
-
/**
|
|
26593
|
-
* Color of text cursor color in editing mode.
|
|
26594
|
-
* if not set (default) will take color from the text.
|
|
26595
|
-
* if set to a color value that fabric can understand, it will
|
|
26596
|
-
* be used instead of the color of the text at the current position.
|
|
26597
|
-
* @type String
|
|
26598
|
-
*/
|
|
26599
|
-
/**
|
|
26600
|
-
* Delay between cursor blink (in ms)
|
|
26601
|
-
* @type Number
|
|
26602
|
-
*/
|
|
26603
|
-
/**
|
|
26604
|
-
* Duration of cursor fade in (in ms)
|
|
26605
|
-
* @type Number
|
|
26606
|
-
*/
|
|
26607
|
-
/**
|
|
26608
|
-
* Indicates whether internal text char widths can be cached
|
|
26609
|
-
* @type Boolean
|
|
26610
|
-
*/
|
|
26611
27602
|
_defineProperty(IText, "ownDefaults", iTextDefaultValues);
|
|
26612
27603
|
_defineProperty(IText, "type", 'IText');
|
|
26613
27604
|
classRegistry.setClass(IText);
|
|
@@ -26678,7 +27669,7 @@
|
|
|
26678
27669
|
}
|
|
26679
27670
|
|
|
26680
27671
|
// Skip if nothing changed
|
|
26681
|
-
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
27672
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}|${this.kashida}`;
|
|
26682
27673
|
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
26683
27674
|
return;
|
|
26684
27675
|
}
|
|
@@ -26777,12 +27768,18 @@
|
|
|
26777
27768
|
}
|
|
26778
27769
|
|
|
26779
27770
|
// Use new layout engine
|
|
27771
|
+
// When kashida is enabled, don't let layout engine apply justify - we'll handle it with kashida
|
|
27772
|
+
const useKashidaJustify = this.kashida !== 'none' && this.textAlign.includes(JUSTIFY);
|
|
27773
|
+
const effectiveAlign = useKashidaJustify ? this.direction === 'rtl' ? 'right' : 'left' // Natural alignment, kashida will justify
|
|
27774
|
+
: this._mapTextAlignToAlign(this.textAlign);
|
|
26780
27775
|
const layout = layoutText({
|
|
26781
27776
|
text: this.text,
|
|
26782
27777
|
width: this.width,
|
|
26783
|
-
height
|
|
27778
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
27779
|
+
// Only pass height if explicitly set to constrain (e.g., for ellipsis)
|
|
27780
|
+
height: this.ellipsis ? this.height : undefined,
|
|
26784
27781
|
wrap: this.wrap || 'word',
|
|
26785
|
-
align:
|
|
27782
|
+
align: effectiveAlign,
|
|
26786
27783
|
ellipsis: this.ellipsis || false,
|
|
26787
27784
|
fontSize: this.fontSize,
|
|
26788
27785
|
lineHeight: this.lineHeight,
|
|
@@ -26820,9 +27817,264 @@
|
|
|
26820
27817
|
|
|
26821
27818
|
// Generate style map for compatibility
|
|
26822
27819
|
this._styleMap = this._generateStyleMapFromLayout(layout);
|
|
27820
|
+
|
|
27821
|
+
// Apply kashida for justified text in advanced layout mode
|
|
27822
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida !== 'none') {
|
|
27823
|
+
this._applyKashidaToLayout();
|
|
27824
|
+
}
|
|
26823
27825
|
this.dirty = true;
|
|
26824
27826
|
}
|
|
26825
27827
|
|
|
27828
|
+
/**
|
|
27829
|
+
* Apply kashida (tatweel) characters to layout for Arabic text justification.
|
|
27830
|
+
* This method INSERTS actual tatweel characters into the text lines.
|
|
27831
|
+
* @private
|
|
27832
|
+
*/
|
|
27833
|
+
_applyKashidaToLayout() {
|
|
27834
|
+
if (!this._textLines || !this.__charBounds) {
|
|
27835
|
+
return;
|
|
27836
|
+
}
|
|
27837
|
+
|
|
27838
|
+
// Clear visual positions cache - it becomes stale when kashida is applied
|
|
27839
|
+
// Check if cache exists (it's initialized in IText constructor which runs after this during construction)
|
|
27840
|
+
if (this._visualPositionsCache) {
|
|
27841
|
+
this._clearVisualPositionsCache();
|
|
27842
|
+
}
|
|
27843
|
+
const kashidaRatios = {
|
|
27844
|
+
none: 0,
|
|
27845
|
+
short: 0.25,
|
|
27846
|
+
medium: 0.5,
|
|
27847
|
+
long: 0.75,
|
|
27848
|
+
stylistic: 1.0
|
|
27849
|
+
};
|
|
27850
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
27851
|
+
if (kashidaRatio === 0) {
|
|
27852
|
+
return;
|
|
27853
|
+
}
|
|
27854
|
+
|
|
27855
|
+
// Calculate tatweel width once
|
|
27856
|
+
const canvas = document.createElement('canvas');
|
|
27857
|
+
const ctx = canvas.getContext('2d');
|
|
27858
|
+
if (!ctx) {
|
|
27859
|
+
return;
|
|
27860
|
+
}
|
|
27861
|
+
ctx.font = this._getFontDeclaration();
|
|
27862
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
27863
|
+
if (tatweelWidth <= 0) {
|
|
27864
|
+
return;
|
|
27865
|
+
}
|
|
27866
|
+
|
|
27867
|
+
// Reset kashida info
|
|
27868
|
+
this.__kashidaInfo = [];
|
|
27869
|
+
const totalLines = this._textLines.length;
|
|
27870
|
+
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
27871
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
27872
|
+
const line = this._textLines[lineIndex];
|
|
27873
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
27874
|
+
continue;
|
|
27875
|
+
}
|
|
27876
|
+
|
|
27877
|
+
// Don't apply kashida to the last line
|
|
27878
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
27879
|
+
if (isLastLine) {
|
|
27880
|
+
continue;
|
|
27881
|
+
}
|
|
27882
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
27883
|
+
const lastBound = lineBounds[lineBounds.length - 1];
|
|
27884
|
+
|
|
27885
|
+
// Calculate current line width
|
|
27886
|
+
const currentLineWidth = lastBound ? lastBound.left + lastBound.kernedWidth : 0;
|
|
27887
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
27888
|
+
|
|
27889
|
+
// Only apply kashida if there's significant extra space to fill
|
|
27890
|
+
if (totalExtraSpace <= 2) {
|
|
27891
|
+
continue;
|
|
27892
|
+
}
|
|
27893
|
+
|
|
27894
|
+
// Find kashida points
|
|
27895
|
+
const kashidaPoints = findKashidaPoints(line);
|
|
27896
|
+
if (kashidaPoints.length === 0) {
|
|
27897
|
+
continue;
|
|
27898
|
+
}
|
|
27899
|
+
|
|
27900
|
+
// Calculate kashida space
|
|
27901
|
+
const kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
27902
|
+
|
|
27903
|
+
// Calculate how many tatweels can fit
|
|
27904
|
+
const totalTatweels = Math.floor(kashidaSpace / tatweelWidth);
|
|
27905
|
+
if (totalTatweels === 0) {
|
|
27906
|
+
continue;
|
|
27907
|
+
}
|
|
27908
|
+
|
|
27909
|
+
// Limit kashida points
|
|
27910
|
+
const maxKashidaPoints = Math.min(kashidaPoints.length, totalTatweels);
|
|
27911
|
+
const usedKashidaPoints = kashidaPoints.slice(0, maxKashidaPoints);
|
|
27912
|
+
|
|
27913
|
+
// Distribute tatweels evenly
|
|
27914
|
+
const tatweelsPerPoint = Math.floor(totalTatweels / maxKashidaPoints);
|
|
27915
|
+
const extraTatweels = totalTatweels % maxKashidaPoints;
|
|
27916
|
+
|
|
27917
|
+
// console.log(`=== Inserting Kashida into line ${lineIndex} ===`);
|
|
27918
|
+
// console.log(` totalTatweels: ${totalTatweels}, usedPoints: ${usedKashidaPoints.length}`);
|
|
27919
|
+
|
|
27920
|
+
// Sort by charIndex descending so we insert from the end (prevents index shifting issues)
|
|
27921
|
+
const sortedPoints = [...usedKashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
27922
|
+
|
|
27923
|
+
// Create new line with tatweels inserted
|
|
27924
|
+
const newLine = [...line];
|
|
27925
|
+
for (let i = 0; i < sortedPoints.length; i++) {
|
|
27926
|
+
const point = sortedPoints[i];
|
|
27927
|
+
const originalIndex = usedKashidaPoints.indexOf(point);
|
|
27928
|
+
const count = tatweelsPerPoint + (originalIndex < extraTatweels ? 1 : 0);
|
|
27929
|
+
if (count > 0) {
|
|
27930
|
+
// Insert tatweels AFTER the character at charIndex
|
|
27931
|
+
const tatweels = Array(count).fill(ARABIC_TATWEEL);
|
|
27932
|
+
newLine.splice(point.charIndex + 1, 0, ...tatweels);
|
|
27933
|
+
// console.log(` Inserted ${count} tatweels after char ${point.charIndex}`);
|
|
27934
|
+
|
|
27935
|
+
// Store kashida info for index conversion
|
|
27936
|
+
this.__kashidaInfo[lineIndex].push({
|
|
27937
|
+
charIndex: point.charIndex,
|
|
27938
|
+
width: count * tatweelWidth,
|
|
27939
|
+
tatweelCount: count
|
|
27940
|
+
});
|
|
27941
|
+
}
|
|
27942
|
+
}
|
|
27943
|
+
|
|
27944
|
+
// Update _textLines with the new line containing tatweels
|
|
27945
|
+
this._textLines[lineIndex] = newLine;
|
|
27946
|
+
|
|
27947
|
+
// Update textLines (string version)
|
|
27948
|
+
if (this.textLines) {
|
|
27949
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
27950
|
+
}
|
|
27951
|
+
|
|
27952
|
+
// Clear and recalculate charBounds for this line
|
|
27953
|
+
this.__charBounds[lineIndex] = [];
|
|
27954
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
27955
|
+
this._measureLine(lineIndex);
|
|
27956
|
+
|
|
27957
|
+
// Now expand spaces to fill any remaining gap
|
|
27958
|
+
let newLineBounds = this.__charBounds[lineIndex];
|
|
27959
|
+
if (newLineBounds && newLineBounds.length > 0) {
|
|
27960
|
+
let newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
27961
|
+
let newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
27962
|
+
let remainingGap = this.width - newLineWidth;
|
|
27963
|
+
if (remainingGap > 0.5) {
|
|
27964
|
+
// Count spaces in the new line
|
|
27965
|
+
let spaceCount = 0;
|
|
27966
|
+
for (let i = 0; i < newLine.length; i++) {
|
|
27967
|
+
if (/\s/.test(newLine[i])) {
|
|
27968
|
+
spaceCount++;
|
|
27969
|
+
}
|
|
27970
|
+
}
|
|
27971
|
+
if (spaceCount > 0) {
|
|
27972
|
+
const extraPerSpace = remainingGap / spaceCount;
|
|
27973
|
+
let accumulatedExtra = 0;
|
|
27974
|
+
|
|
27975
|
+
// Expand space widths AND update left positions for subsequent chars
|
|
27976
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
27977
|
+
const bound = newLineBounds[i];
|
|
27978
|
+
if (!bound) continue;
|
|
27979
|
+
|
|
27980
|
+
// Update left position to account for previous space expansions
|
|
27981
|
+
bound.left += accumulatedExtra;
|
|
27982
|
+
|
|
27983
|
+
// If this is a space, expand it
|
|
27984
|
+
if (/\s/.test(newLine[i])) {
|
|
27985
|
+
bound.width += extraPerSpace;
|
|
27986
|
+
bound.kernedWidth += extraPerSpace;
|
|
27987
|
+
accumulatedExtra += extraPerSpace;
|
|
27988
|
+
}
|
|
27989
|
+
}
|
|
27990
|
+
// Update the extra entry at the end (cursor position)
|
|
27991
|
+
if (newLineBounds[newLine.length]) {
|
|
27992
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
27993
|
+
}
|
|
27994
|
+
|
|
27995
|
+
// Recalculate remaining gap after space expansion
|
|
27996
|
+
newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
27997
|
+
newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
27998
|
+
remainingGap = this.width - newLineWidth;
|
|
27999
|
+
}
|
|
28000
|
+
}
|
|
28001
|
+
|
|
28002
|
+
// If there's still a gap after space expansion, distribute it across all kashida points
|
|
28003
|
+
if (remainingGap > 0.5 && this.__kashidaInfo[lineIndex].length > 0) {
|
|
28004
|
+
const kashidaPointCount = this.__kashidaInfo[lineIndex].length;
|
|
28005
|
+
const extraPerKashida = remainingGap / kashidaPointCount;
|
|
28006
|
+
|
|
28007
|
+
// Find kashida positions in newLine and expand their widths
|
|
28008
|
+
let kashidaIndex = 0;
|
|
28009
|
+
let accumulatedExtra = 0;
|
|
28010
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
28011
|
+
const bound = newLineBounds[i];
|
|
28012
|
+
if (!bound) continue;
|
|
28013
|
+
|
|
28014
|
+
// Update left position for accumulated expansion
|
|
28015
|
+
bound.left += accumulatedExtra;
|
|
28016
|
+
|
|
28017
|
+
// Check if this is a tatweel character
|
|
28018
|
+
if (newLine[i] === ARABIC_TATWEEL) {
|
|
28019
|
+
var _this$__kashidaInfo$l;
|
|
28020
|
+
// Distribute extra width among tatweels
|
|
28021
|
+
const extraForThis = extraPerKashida / (((_this$__kashidaInfo$l = this.__kashidaInfo[lineIndex][kashidaIndex]) === null || _this$__kashidaInfo$l === void 0 ? void 0 : _this$__kashidaInfo$l.tatweelCount) || 1);
|
|
28022
|
+
bound.width += extraForThis;
|
|
28023
|
+
bound.kernedWidth += extraForThis;
|
|
28024
|
+
accumulatedExtra += extraForThis;
|
|
28025
|
+
|
|
28026
|
+
// Move to next kashida info when we've passed this group
|
|
28027
|
+
const currentKashidaInfo = this.__kashidaInfo[lineIndex][kashidaIndex];
|
|
28028
|
+
if (currentKashidaInfo && i > 0) {
|
|
28029
|
+
// Check if next char is not tatweel - means we're done with this group
|
|
28030
|
+
if (i + 1 >= newLine.length || newLine[i + 1] !== ARABIC_TATWEEL) {
|
|
28031
|
+
kashidaIndex++;
|
|
28032
|
+
}
|
|
28033
|
+
}
|
|
28034
|
+
}
|
|
28035
|
+
}
|
|
28036
|
+
|
|
28037
|
+
// Update the extra entry at the end
|
|
28038
|
+
if (newLineBounds[newLine.length]) {
|
|
28039
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
28040
|
+
}
|
|
28041
|
+
}
|
|
28042
|
+
}
|
|
28043
|
+
|
|
28044
|
+
// Set line width to textbox width (for justified lines)
|
|
28045
|
+
this.__lineWidths[lineIndex] = this.width;
|
|
28046
|
+
|
|
28047
|
+
// console.log(` New line length: ${newLine.length}, text: ${newLine.join('')}`);
|
|
28048
|
+
}
|
|
28049
|
+
|
|
28050
|
+
// For justified lines with kashida, line width should equal textbox width
|
|
28051
|
+
// Only set undefined widths (non-justified lines without kashida)
|
|
28052
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
28053
|
+
if (this.__lineWidths[i] === undefined && this.__charBounds[i]) {
|
|
28054
|
+
const bounds = this.__charBounds[i];
|
|
28055
|
+
const lastBound = bounds[bounds.length - 1];
|
|
28056
|
+
if (lastBound) {
|
|
28057
|
+
this.__lineWidths[i] = lastBound.left + lastBound.kernedWidth;
|
|
28058
|
+
}
|
|
28059
|
+
}
|
|
28060
|
+
}
|
|
28061
|
+
|
|
28062
|
+
// Update _text to match the new _textLines (required for editing)
|
|
28063
|
+
this._text = this._textLines.flat();
|
|
28064
|
+
|
|
28065
|
+
// DON'T update this.text - keep the original text intact
|
|
28066
|
+
// The tatweels are in _textLines and _text for rendering purposes only
|
|
28067
|
+
|
|
28068
|
+
this._justifyApplied = true;
|
|
28069
|
+
|
|
28070
|
+
// Debug log final kashida state
|
|
28071
|
+
// console.log('=== _applyKashidaToLayout END ===');
|
|
28072
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
28073
|
+
// line: i,
|
|
28074
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
28075
|
+
// }))));
|
|
28076
|
+
}
|
|
28077
|
+
|
|
26826
28078
|
/**
|
|
26827
28079
|
* Generate style map from new layout format
|
|
26828
28080
|
* @private
|
|
@@ -27345,84 +28597,100 @@
|
|
|
27345
28597
|
* @private
|
|
27346
28598
|
*/
|
|
27347
28599
|
_extractJustifySpaceMeasurements(element, lines) {
|
|
27348
|
-
console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
27349
|
-
console.log('Textbox width:', this.width);
|
|
27350
|
-
console.log('Lines count:', lines.length);
|
|
28600
|
+
// console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
28601
|
+
// console.log('Textbox width:', this.width);
|
|
28602
|
+
// console.log('Lines count:', lines.length);
|
|
28603
|
+
|
|
27351
28604
|
const measureCtx = this._browserMeasureCtx || (this._browserMeasureCtx = document.createElement('canvas').getContext('2d'));
|
|
27352
28605
|
if (!measureCtx) {
|
|
27353
|
-
console.log('ERROR: No measure context');
|
|
28606
|
+
// console.log('ERROR: No measure context');
|
|
27354
28607
|
return [];
|
|
27355
28608
|
}
|
|
27356
28609
|
measureCtx.font = `${this.fontStyle || 'normal'} ${this.fontWeight || 'normal'} ${this.fontSize}px "${this.fontFamily}"`;
|
|
27357
28610
|
const normalSpaceWidth = measureCtx.measureText(' ').width || 6;
|
|
27358
|
-
console.log('Font:', measureCtx.font);
|
|
27359
|
-
console.log('Normal space width:', normalSpaceWidth);
|
|
28611
|
+
// console.log('Font:', measureCtx.font);
|
|
28612
|
+
// console.log('Normal space width:', normalSpaceWidth);
|
|
28613
|
+
|
|
27360
28614
|
const spaceWidths = [];
|
|
27361
28615
|
lines.forEach((line, lineIndex) => {
|
|
27362
28616
|
const lineSpaces = [];
|
|
27363
28617
|
const spaceCount = (line.match(/\s/g) || []).length;
|
|
27364
28618
|
const isLastLine = lineIndex === lines.length - 1;
|
|
27365
|
-
|
|
28619
|
+
|
|
28620
|
+
// console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
|
|
28621
|
+
|
|
27366
28622
|
if (spaceCount > 0 && !isLastLine) {
|
|
27367
28623
|
// Don't justify last line
|
|
27368
28624
|
const naturalWidth = measureCtx.measureText(line).width;
|
|
27369
28625
|
const remainingSpace = this.width - naturalWidth;
|
|
27370
28626
|
const extraPerSpace = remainingSpace > 0 ? remainingSpace / spaceCount : 0;
|
|
27371
28627
|
const expandedSpaceWidth = normalSpaceWidth + extraPerSpace;
|
|
27372
|
-
|
|
27373
|
-
console.log(`
|
|
28628
|
+
|
|
28629
|
+
// console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
|
|
28630
|
+
// console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
|
|
28631
|
+
|
|
27374
28632
|
const safeWidth = Math.max(normalSpaceWidth, expandedSpaceWidth);
|
|
27375
28633
|
for (let i = 0; i < spaceCount; i++) {
|
|
27376
28634
|
lineSpaces.push(safeWidth);
|
|
27377
28635
|
}
|
|
27378
28636
|
} else if (spaceCount > 0) {
|
|
27379
28637
|
// Last line: keep natural space width
|
|
27380
|
-
console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
28638
|
+
// console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
27381
28639
|
for (let i = 0; i < spaceCount; i++) {
|
|
27382
28640
|
lineSpaces.push(normalSpaceWidth);
|
|
27383
28641
|
}
|
|
27384
28642
|
}
|
|
27385
28643
|
spaceWidths.push(lineSpaces);
|
|
27386
28644
|
});
|
|
27387
|
-
|
|
27388
|
-
console.log('
|
|
28645
|
+
|
|
28646
|
+
// console.log('\nFinal spaceWidths:', spaceWidths);
|
|
28647
|
+
// console.log('=== _extractJustifySpaceMeasurements END ===\n');
|
|
27389
28648
|
return spaceWidths;
|
|
27390
28649
|
}
|
|
27391
28650
|
|
|
27392
28651
|
/**
|
|
27393
|
-
* Apply justify space expansion using actual charBounds measurements
|
|
28652
|
+
* Apply justify space expansion using actual charBounds measurements.
|
|
28653
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
27394
28654
|
* @private
|
|
27395
28655
|
*/
|
|
27396
28656
|
_applyBrowserJustifySpaces() {
|
|
27397
|
-
var _this$_textLines, _this$__charBounds;
|
|
27398
|
-
console.log('=== _applyBrowserJustifySpaces START ===');
|
|
27399
|
-
console.log('_textLines:', (_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length, 'lines');
|
|
27400
|
-
console.log('__charBounds:', (_this$__charBounds = this.__charBounds) === null || _this$__charBounds === void 0 ? void 0 : _this$__charBounds.length, 'lines');
|
|
27401
|
-
console.log('textbox width:', this.width);
|
|
27402
28657
|
if (!this._textLines || !this.__charBounds) {
|
|
27403
|
-
console.log('EARLY RETURN: _textLines or __charBounds missing');
|
|
27404
28658
|
return;
|
|
27405
28659
|
}
|
|
28660
|
+
|
|
28661
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
28662
|
+
const kashidaRatios = {
|
|
28663
|
+
none: 0,
|
|
28664
|
+
short: 0.25,
|
|
28665
|
+
medium: 0.5,
|
|
28666
|
+
long: 0.75,
|
|
28667
|
+
stylistic: 1.0
|
|
28668
|
+
};
|
|
28669
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
28670
|
+
|
|
28671
|
+
// Reset kashida info
|
|
28672
|
+
this.__kashidaInfo = [];
|
|
27406
28673
|
const totalLines = this._textLines.length;
|
|
27407
28674
|
this._textLines.forEach((line, lineIndex) => {
|
|
27408
|
-
|
|
27409
|
-
|
|
27410
|
-
console.log(`\n--- Line ${lineIndex}: "${lineText}" isLast: ${isLastLine} ---`);
|
|
28675
|
+
// Initialize kashida info for this line
|
|
28676
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
27411
28677
|
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
27412
|
-
console.log(' SKIP: No charBounds for this line');
|
|
27413
28678
|
return;
|
|
27414
28679
|
}
|
|
27415
28680
|
|
|
27416
28681
|
// Don't justify the last line
|
|
28682
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
27417
28683
|
if (isLastLine) {
|
|
27418
|
-
console.log(' SKIP: Last line - no justify');
|
|
27419
28684
|
return;
|
|
27420
28685
|
}
|
|
27421
28686
|
const lineBounds = this.__charBounds[lineIndex];
|
|
27422
28687
|
|
|
27423
28688
|
// Calculate current line width from charBounds
|
|
27424
28689
|
const currentLineWidth = lineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
27425
|
-
|
|
28690
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
28691
|
+
if (totalExtraSpace <= 0) {
|
|
28692
|
+
return;
|
|
28693
|
+
}
|
|
27426
28694
|
|
|
27427
28695
|
// Count spaces and find space indices
|
|
27428
28696
|
const spaceIndices = [];
|
|
@@ -27432,53 +28700,118 @@
|
|
|
27432
28700
|
}
|
|
27433
28701
|
}
|
|
27434
28702
|
const spaceCount = spaceIndices.length;
|
|
27435
|
-
|
|
27436
|
-
|
|
27437
|
-
|
|
27438
|
-
|
|
28703
|
+
|
|
28704
|
+
// Find kashida points if enabled
|
|
28705
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
28706
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
28707
|
+
|
|
28708
|
+
// Calculate space distribution
|
|
28709
|
+
let kashidaSpace = 0;
|
|
28710
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
28711
|
+
// Distribute between kashida and spaces
|
|
28712
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
27439
28713
|
}
|
|
27440
28714
|
|
|
27441
|
-
// Calculate
|
|
27442
|
-
const
|
|
27443
|
-
|
|
27444
|
-
|
|
27445
|
-
|
|
27446
|
-
|
|
28715
|
+
// Calculate per-kashida and per-space widths
|
|
28716
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
28717
|
+
|
|
28718
|
+
// If kashida is enabled, insert actual tatweel characters
|
|
28719
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
28720
|
+
// console.log(`=== Inserting kashida in _applyBrowserJustifySpaces line ${lineIndex} ===`);
|
|
28721
|
+
|
|
28722
|
+
// Sort by charIndex descending to insert from end
|
|
28723
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
28724
|
+
|
|
28725
|
+
// Calculate tatweel width
|
|
28726
|
+
const canvas = document.createElement('canvas');
|
|
28727
|
+
const ctx = canvas.getContext('2d');
|
|
28728
|
+
if (ctx) {
|
|
28729
|
+
ctx.font = this._getFontDeclaration();
|
|
28730
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
28731
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
28732
|
+
|
|
28733
|
+
if (tatweelWidth > 0) {
|
|
28734
|
+
const newLine = [...line];
|
|
28735
|
+
for (const point of sortedPoints) {
|
|
28736
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
28737
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
28738
|
+
|
|
28739
|
+
// Insert tatweels after the character
|
|
28740
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
28741
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
28742
|
+
}
|
|
28743
|
+
|
|
28744
|
+
// Store kashida info with tatweelCount for index conversion
|
|
28745
|
+
this.__kashidaInfo[lineIndex].push({
|
|
28746
|
+
charIndex: point.charIndex,
|
|
28747
|
+
width: perKashidaWidth,
|
|
28748
|
+
tatweelCount: tatweelCount
|
|
28749
|
+
});
|
|
28750
|
+
}
|
|
28751
|
+
|
|
28752
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
28753
|
+
|
|
28754
|
+
// Update _textLines with kashida
|
|
28755
|
+
this._textLines[lineIndex] = newLine;
|
|
28756
|
+
|
|
28757
|
+
// Update textLines string version
|
|
28758
|
+
if (this.textLines && this.textLines[lineIndex] !== undefined) {
|
|
28759
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
28760
|
+
}
|
|
28761
|
+
|
|
28762
|
+
// Recalculate charBounds
|
|
28763
|
+
this.__charBounds[lineIndex] = [];
|
|
28764
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
28765
|
+
this._measureLine(lineIndex);
|
|
28766
|
+
}
|
|
28767
|
+
}
|
|
28768
|
+
} else {
|
|
28769
|
+
// No kashida - just store info for reference (tatweelCount is 0 since no tatweels inserted)
|
|
28770
|
+
for (const point of kashidaPoints) {
|
|
28771
|
+
this.__kashidaInfo[lineIndex].push({
|
|
28772
|
+
charIndex: point.charIndex,
|
|
28773
|
+
width: perKashidaWidth,
|
|
28774
|
+
tatweelCount: 0
|
|
28775
|
+
});
|
|
28776
|
+
}
|
|
27447
28777
|
}
|
|
27448
|
-
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27453
|
-
|
|
27454
|
-
const
|
|
27455
|
-
|
|
27456
|
-
|
|
27457
|
-
|
|
27458
|
-
|
|
27459
|
-
|
|
27460
|
-
|
|
27461
|
-
|
|
27462
|
-
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
console.log(` Space at char ${charIndex}: ${oldWidth.toFixed(2)} -> ${newWidth.toFixed(2)} (accumulated: ${accumulated.toFixed(2)})`);
|
|
28778
|
+
|
|
28779
|
+
// Now apply space expansion to remaining extra space
|
|
28780
|
+
const newLineBounds = this.__charBounds[lineIndex];
|
|
28781
|
+
const newLineWidth = newLineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
28782
|
+
const remainingSpace = this.width - newLineWidth;
|
|
28783
|
+
if (remainingSpace > 0 && spaceCount > 0) {
|
|
28784
|
+
const extraPerSpace = remainingSpace / spaceCount;
|
|
28785
|
+
let accumulated = 0;
|
|
28786
|
+
for (let charIndex = 0; charIndex < this._textLines[lineIndex].length; charIndex++) {
|
|
28787
|
+
const bound = newLineBounds[charIndex];
|
|
28788
|
+
if (!bound) continue;
|
|
28789
|
+
bound.left += accumulated;
|
|
28790
|
+
|
|
28791
|
+
// Check if this is a space (need to check against the updated line)
|
|
28792
|
+
if (/\s/.test(this._textLines[lineIndex][charIndex])) {
|
|
28793
|
+
bound.width += extraPerSpace;
|
|
28794
|
+
bound.kernedWidth += extraPerSpace;
|
|
28795
|
+
accumulated += extraPerSpace;
|
|
28796
|
+
}
|
|
27468
28797
|
}
|
|
27469
28798
|
}
|
|
27470
28799
|
|
|
27471
28800
|
// Update cached line width
|
|
27472
|
-
const
|
|
28801
|
+
const finalLineBounds = this.__charBounds[lineIndex];
|
|
28802
|
+
const finalLineWidth = finalLineBounds.reduce((max, b) => Math.max(max, ((b === null || b === void 0 ? void 0 : b.left) || 0) + ((b === null || b === void 0 ? void 0 : b.width) || 0)), 0);
|
|
27473
28803
|
this.__lineWidths[lineIndex] = finalLineWidth;
|
|
27474
|
-
console.log(' Final line width:', finalLineWidth.toFixed(2), 'target:', this.width);
|
|
27475
28804
|
});
|
|
27476
|
-
console.log('=== _applyBrowserJustifySpaces END ===\n');
|
|
27477
28805
|
this.dirty = true;
|
|
27478
28806
|
// Mark that justify has been applied - for debugging to detect if measureLine overwrites it
|
|
27479
28807
|
this._justifyApplied = true;
|
|
27480
|
-
|
|
27481
|
-
//
|
|
28808
|
+
|
|
28809
|
+
// Debug log final kashida state
|
|
28810
|
+
// console.log('=== _applyBrowserJustifySpaces END ===');
|
|
28811
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
28812
|
+
// line: i,
|
|
28813
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
28814
|
+
// }))));
|
|
27482
28815
|
}
|
|
27483
28816
|
|
|
27484
28817
|
/**
|