@nasser-sw/fabric 7.0.0-beta1 → 7.0.1-beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21902,8 +21902,12 @@ class OverlayEditor {
21902
21902
  this.textarea.style.pointerEvents = 'auto';
21903
21903
  // Set appropriate unicodeBidi based on content and direction
21904
21904
  const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
21905
+ const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
21905
21906
  const isLTRDirection = this.target.direction === 'ltr';
21906
- if (hasArabicText && isLTRDirection) {
21907
+ if (hasArabicText && hasLatinText && isLTRDirection) {
21908
+ // For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
21909
+ this.textarea.style.unicodeBidi = 'embed';
21910
+ } else if (hasArabicText && isLTRDirection) {
21907
21911
  // For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
21908
21912
  this.textarea.style.unicodeBidi = 'embed';
21909
21913
  } else {
@@ -21992,14 +21996,26 @@ class OverlayEditor {
21992
21996
  parseFloat(this.hostDiv.style.width) / zoom;
21993
21997
  const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
21994
21998
 
21995
- // Only update if there's a meaningful change (avoid float precision issues)
21999
+ // Always update height for responsive controls (especially important for line deletion)
21996
22000
  const heightDiff = Math.abs(currentHeight - target.height);
21997
- const threshold = 1; // 1px threshold to avoid micro-changes
22001
+ const threshold = 0.5; // Lower threshold for better responsiveness to line changes
21998
22002
 
21999
22003
  if (heightDiff > threshold) {
22004
+ target.height;
22000
22005
  target.height = currentHeight;
22001
22006
  target.setCoords(); // Update control positions
22007
+
22008
+ // Force dirty to ensure proper re-rendering
22009
+ target.dirty = true;
22002
22010
  this.canvas.requestRenderAll(); // Re-render to show updated selection
22011
+
22012
+ // IMPORTANT: Reposition overlay after height change
22013
+ requestAnimationFrame(() => {
22014
+ if (!this.isDestroyed) {
22015
+ this.applyOverlayStyle();
22016
+ console.log('📐 Height changed - rechecking alignment after repositioning:');
22017
+ }
22018
+ });
22003
22019
  }
22004
22020
  }
22005
22021
 
@@ -22027,14 +22043,6 @@ class OverlayEditor {
22027
22043
  target.setCoords();
22028
22044
  const aCoords = target.aCoords;
22029
22045
 
22030
- // DEBUG: Log dimensions before edit
22031
- console.log('BEFORE EDIT:');
22032
- console.log(' target.width =', target.width);
22033
- console.log(' target.height =', target.height);
22034
- console.log(' target.getScaledWidth() =', target.getScaledWidth());
22035
- console.log(' target.getScaledHeight() =', target.getScaledHeight());
22036
- console.log(' target.padding =', target.padding);
22037
-
22038
22046
  // 2. Get canvas position and scroll offsets (like rtl-test.html)
22039
22047
  const canvasEl = canvas.upperCanvasEl;
22040
22048
  const canvasRect = canvasEl.getBoundingClientRect();
@@ -22057,14 +22065,12 @@ class OverlayEditor {
22057
22065
  const left = canvasRect.left + scrollX + screenPoint.x;
22058
22066
  const top = canvasRect.top + scrollY + screenPoint.y;
22059
22067
 
22060
- // 4. Get dimensions with zoom scaling - use target.width for text wrapping, scaled height for container
22061
- const width = target.width * (target.scaleX || 1) * zoom; // Account for object scale and viewport zoom
22062
- const height = target.height * (target.scaleY || 1) * zoom;
22063
- console.log('WIDTH CALCULATION:');
22064
- console.log(' target.width =', target.width);
22065
- console.log(' scaledWidth =', target.getScaledWidth());
22066
- console.log(' zoom =', zoom);
22067
- console.log(' final width =', width);
22068
+ // 4. Calculate the precise width and height for the container
22069
+ // **THE FIX:** Use getBoundingRect() for BOTH width and height.
22070
+ // This is the most reliable measure of the object's final rendered dimensions.
22071
+ const objectBounds = target.getBoundingRect();
22072
+ const width = Math.round(objectBounds.width * zoom);
22073
+ const height = Math.round(objectBounds.height * zoom);
22068
22074
 
22069
22075
  // 5. Apply styles to host DIV - absolute positioning like rtl-test.html
22070
22076
  this.hostDiv.style.position = 'absolute';
@@ -22088,50 +22094,209 @@ class OverlayEditor {
22088
22094
  const scaleX = target.scaleX || 1;
22089
22095
  const finalFontSize = baseFontSize * scaleX * zoom;
22090
22096
  const fabricLineHeight = target.lineHeight || 1.16;
22091
- // Apply padding and dimensions to textarea
22092
- const textareaWidth = paddingX > 0 ? `calc(100% - ${2 * paddingX}px)` : '100%';
22093
- const textareaHeight = paddingY > 0 ? `calc(100% - ${2 * paddingY}px)` : '100%';
22094
- this.textarea.style.width = textareaWidth;
22095
- this.textarea.style.height = textareaHeight;
22097
+ // **THE FIX:** Use 'border-box' so the width property includes padding.
22098
+ // This makes alignment much easier and more reliable.
22099
+ this.textarea.style.boxSizing = 'border-box';
22100
+
22101
+ // **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
22102
+ // The padding will now be correctly contained *inside* this width.
22103
+ this.textarea.style.width = `${width}px`;
22104
+ this.textarea.style.height = '100%'; // Let hostDiv control height
22096
22105
  this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
22106
+
22107
+ // Apply all other font and text styles to match Fabric
22108
+ const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
22097
22109
  this.textarea.style.fontSize = `${finalFontSize}px`;
22098
- this.textarea.style.lineHeight = String(fabricLineHeight); // Use unit-less multiplier
22110
+ this.textarea.style.lineHeight = String(fabricLineHeight);
22099
22111
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22100
22112
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22101
22113
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22102
22114
  this.textarea.style.textAlign = target.textAlign || 'left';
22103
22115
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22104
- this.textarea.style.letterSpacing = `${(target.charSpacing || 0) / 1000}em`;
22116
+ this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22105
22117
  this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
22106
-
22107
- // Ensure consistent font rendering between Fabric and CSS
22108
22118
  this.textarea.style.fontVariant = 'normal';
22109
22119
  this.textarea.style.fontStretch = 'normal';
22110
- this.textarea.style.textRendering = 'auto';
22111
- this.textarea.style.fontKerning = 'auto';
22112
- this.textarea.style.boxSizing = 'content-box'; // Padding is added outside width/height
22120
+ this.textarea.style.textRendering = 'optimizeLegibility';
22121
+ this.textarea.style.fontKerning = 'normal';
22122
+ this.textarea.style.fontFeatureSettings = 'normal';
22123
+ this.textarea.style.fontVariationSettings = 'normal';
22113
22124
  this.textarea.style.margin = '0';
22114
22125
  this.textarea.style.border = 'none';
22115
22126
  this.textarea.style.outline = 'none';
22116
22127
  this.textarea.style.background = 'transparent';
22117
- this.textarea.style.wordWrap = 'break-word';
22128
+ this.textarea.style.overflowWrap = 'break-word';
22118
22129
  this.textarea.style.whiteSpace = 'pre-wrap';
22130
+ this.textarea.style.hyphens = 'none';
22131
+ this.textarea.style.webkitFontSmoothing = 'antialiased';
22132
+ this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22119
22133
 
22120
- // DEBUG: Log final textarea dimensions
22121
- console.log('TEXTAREA AFTER SETUP:');
22122
- console.log(' textarea width =', this.textarea.style.width);
22123
- console.log(' textarea height =', this.textarea.style.height);
22124
- console.log(' textarea padding =', this.textarea.style.padding);
22125
- console.log(' paddingX =', paddingX, 'paddingY =', paddingY);
22126
- console.log(' baseFontSize =', baseFontSize);
22127
- console.log(' scaleX =', scaleX);
22128
- console.log(' zoom =', zoom);
22129
- console.log(' finalFontSize =', finalFontSize);
22130
- console.log(' fabricLineHeight =', fabricLineHeight);
22134
+ // Debug: Compare textarea and canvas object bounding boxes
22135
+ this.debugBoundingBoxComparison();
22136
+
22137
+ // Debug: Compare text wrapping behavior
22138
+ this.debugTextWrapping();
22131
22139
 
22132
22140
  // Initial bounds are set correctly by Fabric.js - don't force update here
22133
22141
  }
22134
22142
 
22143
+ /**
22144
+ * Debug method to compare textarea and canvas object bounding boxes
22145
+ */
22146
+ debugBoundingBoxComparison() {
22147
+ const target = this.target;
22148
+ const canvas = this.canvas;
22149
+ const zoom = canvas.getZoom();
22150
+
22151
+ // Get textarea bounding box (in screen coordinates)
22152
+ const textareaRect = this.textarea.getBoundingClientRect();
22153
+ const hostRect = this.hostDiv.getBoundingClientRect();
22154
+
22155
+ // Get canvas object bounding box (in screen coordinates)
22156
+ const canvasBounds = target.getBoundingRect();
22157
+ const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
22158
+
22159
+ // Convert canvas object bounds to screen coordinates
22160
+ const vpt = canvas.viewportTransform;
22161
+ const screenObjectBounds = {
22162
+ left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
22163
+ top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
22164
+ width: canvasBounds.width * zoom,
22165
+ height: canvasBounds.height * zoom
22166
+ };
22167
+ console.log('🔍 BOUNDING BOX COMPARISON:');
22168
+ console.log('📦 Textarea Rect:', {
22169
+ left: Math.round(textareaRect.left * 100) / 100,
22170
+ top: Math.round(textareaRect.top * 100) / 100,
22171
+ width: Math.round(textareaRect.width * 100) / 100,
22172
+ height: Math.round(textareaRect.height * 100) / 100
22173
+ });
22174
+ console.log('📦 Host Div Rect:', {
22175
+ left: Math.round(hostRect.left * 100) / 100,
22176
+ top: Math.round(hostRect.top * 100) / 100,
22177
+ width: Math.round(hostRect.width * 100) / 100,
22178
+ height: Math.round(hostRect.height * 100) / 100
22179
+ });
22180
+ console.log('📦 Canvas Object Bounds (screen):', {
22181
+ left: Math.round(screenObjectBounds.left * 100) / 100,
22182
+ top: Math.round(screenObjectBounds.top * 100) / 100,
22183
+ width: Math.round(screenObjectBounds.width * 100) / 100,
22184
+ height: Math.round(screenObjectBounds.height * 100) / 100
22185
+ });
22186
+ console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
22187
+
22188
+ // Calculate differences
22189
+ const hostVsObject = {
22190
+ leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
22191
+ topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
22192
+ widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
22193
+ heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
22194
+ };
22195
+ const textareaVsObject = {
22196
+ leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
22197
+ topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
22198
+ widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
22199
+ heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
22200
+ };
22201
+ console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
22202
+ console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
22203
+
22204
+ // Check if they're aligned (within 2px tolerance)
22205
+ const tolerance = 2;
22206
+ const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
22207
+ const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
22208
+ console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
22209
+ console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
22210
+ console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
22211
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22212
+ }
22213
+
22214
+ /**
22215
+ * Debug method to compare text wrapping between textarea and Fabric text object
22216
+ */
22217
+ debugTextWrapping() {
22218
+ const target = this.target;
22219
+ const text = this.textarea.value;
22220
+ console.log('📝 TEXT WRAPPING COMPARISON:');
22221
+ console.log('📄 Text Content:', `"${text}"`);
22222
+ console.log('📄 Text Length:', text.length);
22223
+
22224
+ // Analyze line breaks
22225
+ const explicitLines = text.split('\n');
22226
+ console.log('📄 Explicit Lines (\\n):', explicitLines.length);
22227
+ explicitLines.forEach((line, i) => {
22228
+ console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
22229
+ });
22230
+
22231
+ // Get textarea computed styles for wrapping analysis
22232
+ const textareaStyles = window.getComputedStyle(this.textarea);
22233
+ console.log('📐 Textarea Wrapping Styles:');
22234
+ console.log(' width:', textareaStyles.width);
22235
+ console.log(' fontSize:', textareaStyles.fontSize);
22236
+ console.log(' fontFamily:', textareaStyles.fontFamily);
22237
+ console.log(' fontWeight:', textareaStyles.fontWeight);
22238
+ console.log(' letterSpacing:', textareaStyles.letterSpacing);
22239
+ console.log(' lineHeight:', textareaStyles.lineHeight);
22240
+ console.log(' whiteSpace:', textareaStyles.whiteSpace);
22241
+ console.log(' wordWrap:', textareaStyles.wordWrap);
22242
+ console.log(' overflowWrap:', textareaStyles.overflowWrap);
22243
+ console.log(' direction:', textareaStyles.direction);
22244
+ console.log(' textAlign:', textareaStyles.textAlign);
22245
+
22246
+ // Get Fabric text object properties for comparison
22247
+ console.log('📐 Fabric Text Object Properties:');
22248
+ console.log(' width:', target.width);
22249
+ console.log(' fontSize:', target.fontSize);
22250
+ console.log(' fontFamily:', target.fontFamily);
22251
+ console.log(' fontWeight:', target.fontWeight);
22252
+ console.log(' charSpacing:', target.charSpacing);
22253
+ console.log(' lineHeight:', target.lineHeight);
22254
+ console.log(' direction:', target.direction);
22255
+ console.log(' textAlign:', target.textAlign);
22256
+ console.log(' scaleX:', target.scaleX);
22257
+ console.log(' scaleY:', target.scaleY);
22258
+
22259
+ // Calculate effective dimensions for comparison - use actual rendered width
22260
+ // **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
22261
+ const fabricEffectiveWidth = this.target.getBoundingRect().width;
22262
+ // Use the exact width set on textarea for comparison
22263
+ const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
22264
+ const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
22265
+ const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
22266
+ console.log('📏 Effective Width Comparison:');
22267
+ console.log(' Textarea Effective Width:', textareaEffectiveWidth);
22268
+ console.log(' Fabric Effective Width:', fabricEffectiveWidth);
22269
+ console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
22270
+ console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
22271
+
22272
+ // Check text direction and bidi handling
22273
+ const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22274
+ const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
22275
+ console.log('🌍 Text Direction Analysis:');
22276
+ console.log(' Has RTL characters:', hasRTLText);
22277
+ console.log(' Has mixed Bidi text:', hasBidiText);
22278
+ console.log(' Textarea direction:', textareaStyles.direction);
22279
+ console.log(' Fabric direction:', target.direction || 'auto');
22280
+ console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
22281
+
22282
+ // Measure actual rendered line count
22283
+ const textareaScrollHeight = this.textarea.scrollHeight;
22284
+ const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
22285
+ const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
22286
+ console.log('📊 Line Count Analysis:');
22287
+ console.log(' Textarea scrollHeight:', textareaScrollHeight);
22288
+ console.log(' Textarea lineHeight:', textareaLineHeight);
22289
+ console.log(' Estimated rendered lines:', estimatedTextareaLines);
22290
+ console.log(' Explicit line breaks:', explicitLines.length);
22291
+ if (estimatedTextareaLines > explicitLines.length) {
22292
+ console.log('🔄 Text wrapping detected in textarea');
22293
+ console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
22294
+ } else {
22295
+ console.log('📏 No text wrapping in textarea');
22296
+ }
22297
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22298
+ }
22299
+
22135
22300
  /**
22136
22301
  * Focus the textarea and position cursor at end
22137
22302
  */
@@ -22244,25 +22409,40 @@ class OverlayEditor {
22244
22409
  }
22245
22410
  }
22246
22411
  autoResizeTextarea() {
22247
- // Allow both vertical growth and shrinking; host width stays fixed
22248
- const oldHeight = parseFloat(window.getComputedStyle(this.textarea).height);
22249
-
22250
- // Reset height to measure actual needed height
22251
- this.textarea.style.height = 'auto';
22412
+ // Store the scroll position and the container's old height for comparison.
22413
+ const scrollTop = this.textarea.scrollTop;
22414
+ const oldHeight = parseFloat(this.hostDiv.style.height || '0');
22415
+
22416
+ // 1. **Force a reliable reflow.**
22417
+ // First, reset the textarea's height to a minimal value. This is the crucial step
22418
+ // that forces the browser to recalculate the content's height from scratch,
22419
+ // ignoring the hostDiv's larger, stale height.
22420
+ this.textarea.style.height = '1px';
22421
+
22422
+ // 2. Read the now-accurate scrollHeight. This value reflects the minimum
22423
+ // height required for the content, whether it's single or multi-line.
22252
22424
  const scrollHeight = this.textarea.scrollHeight;
22253
22425
 
22254
- // Add extra padding to prevent text clipping (especially for line height)
22255
- const lineHeightBuffer = 8; // Extra space to prevent clipping
22256
- const newHeight = Math.max(scrollHeight + lineHeightBuffer, 25); // Minimum height with buffer
22257
- const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
22426
+ // A small buffer for rendering consistency across browsers.
22427
+ const buffer = 2;
22428
+ const newHeight = scrollHeight + buffer;
22258
22429
 
22259
- this.textarea.style.height = `${newHeight}px`;
22260
- this.hostDiv.style.height = `${newHeight}px`; // Match exactly
22430
+ // Check if the height has changed significantly.
22431
+ const heightChanged = Math.abs(newHeight - oldHeight) > 1;
22261
22432
 
22262
- // Only update object bounds if height actually changed
22433
+ // 4. Only update heights and object bounds if there was a change.
22263
22434
  if (heightChanged) {
22435
+ this.textarea.style.height = `${newHeight}px`;
22436
+ this.hostDiv.style.height = `${newHeight}px`;
22264
22437
  this.updateObjectBounds();
22438
+ } else {
22439
+ // If no significant change, ensure the textarea's height matches the container
22440
+ // to prevent any minor visual misalignment.
22441
+ this.textarea.style.height = this.hostDiv.style.height;
22265
22442
  }
22443
+
22444
+ // 5. Restore the original scroll position.
22445
+ this.textarea.scrollTop = scrollTop;
22266
22446
  }
22267
22447
  handleKeyDown(e) {
22268
22448
  if (e.key === 'Escape') {
@@ -22271,6 +22451,19 @@ class OverlayEditor {
22271
22451
  } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
22272
22452
  e.preventDefault();
22273
22453
  this.destroy(true); // Commit
22454
+ } else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
22455
+ // For keys that might change the height, schedule a resize check
22456
+ // Use both immediate and delayed checks to catch all scenarios
22457
+ requestAnimationFrame(() => {
22458
+ if (!this.isDestroyed) {
22459
+ this.autoResizeTextarea();
22460
+ }
22461
+ });
22462
+ setTimeout(() => {
22463
+ if (!this.isDestroyed) {
22464
+ this.autoResizeTextarea();
22465
+ }
22466
+ }, 10); // Small delay to ensure DOM is updated
22274
22467
  }
22275
22468
  }
22276
22469
  handleFocus() {
@@ -25247,6 +25440,7 @@ class Textbox extends IText {
25247
25440
  ...Textbox.ownDefaults,
25248
25441
  ...options
25249
25442
  });
25443
+ this.initializeEventListeners();
25250
25444
  }
25251
25445
 
25252
25446
  /**
@@ -25754,6 +25948,173 @@ class Textbox extends IText {
25754
25948
  }
25755
25949
  }
25756
25950
 
25951
+ /**
25952
+ * Initialize event listeners for safety snap functionality
25953
+ * @private
25954
+ */
25955
+ initializeEventListeners() {
25956
+ var _this$canvas;
25957
+ // Track which side is being used for resize to handle position compensation
25958
+ let resizeOrigin = null;
25959
+
25960
+ // Detect resize origin during resizing
25961
+ this.on('resizing', e => {
25962
+ // Check transform origin to determine which side is being resized
25963
+ console.log('🔍 Resize event data:', e);
25964
+ if (e.transform) {
25965
+ const {
25966
+ originX,
25967
+ originY
25968
+ } = e.transform;
25969
+ console.log('🔍 Transform origins:', {
25970
+ originX,
25971
+ originY
25972
+ });
25973
+ // originX tells us which side is the anchor - opposite side is being dragged
25974
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25975
+ console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25976
+ } else if (e.originX) {
25977
+ const {
25978
+ originX,
25979
+ originY
25980
+ } = e;
25981
+ console.log('🔍 Event origins:', {
25982
+ originX,
25983
+ originY
25984
+ });
25985
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25986
+ console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25987
+ }
25988
+ });
25989
+
25990
+ // Only trigger safety snap after resize is complete (not during)
25991
+ // Use 'modified' event which fires after user releases the mouse
25992
+ this.on('modified', () => {
25993
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
25994
+ console.log('✅ Modified event fired - resize complete, triggering safety snap', {
25995
+ resizeOrigin: currentResizeOrigin
25996
+ });
25997
+ // Small delay to ensure text layout is updated
25998
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
25999
+ resizeOrigin = null; // Reset after capturing
26000
+ });
26001
+
26002
+ // Also listen to canvas-level modified event as backup
26003
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
26004
+ if (e.target === this) {
26005
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26006
+ console.log('✅ Canvas object:modified fired for this textbox');
26007
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26008
+ resizeOrigin = null; // Reset after capturing
26009
+ }
26010
+ });
26011
+ }
26012
+
26013
+ /**
26014
+ * Safety snap to prevent glyph clipping after manual resize.
26015
+ * Similar to Polotno - checks if any glyphs are too close to edges
26016
+ * and automatically expands width if needed.
26017
+ * @private
26018
+ * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26019
+ */
26020
+ safetySnapWidth(resizeOrigin) {
26021
+ var _this$_textLines;
26022
+ console.log('🔍 safetySnapWidth called', {
26023
+ isWrapping: this.isWrapping,
26024
+ hasTextLines: !!this._textLines,
26025
+ lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26026
+ currentWidth: this.width,
26027
+ type: this.type,
26028
+ text: this.text
26029
+ });
26030
+
26031
+ // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26032
+ if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26033
+ var _this$_textLines2;
26034
+ console.log('❌ Early return - missing requirements', {
26035
+ hasTextLines: !!this._textLines,
26036
+ typeMatch: this.type.toLowerCase() === 'textbox',
26037
+ actualType: this.type,
26038
+ hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26039
+ });
26040
+ return;
26041
+ }
26042
+ const lineCount = this._textLines.length;
26043
+ if (lineCount === 0) return;
26044
+
26045
+ // Check all lines, not just the last one
26046
+ let maxActualLineWidth = 0; // Actual measured width without buffers
26047
+ let maxRequiredWidth = 0; // Width including RTL buffer
26048
+
26049
+ for (let i = 0; i < lineCount; i++) {
26050
+ const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26051
+ const lineWidth = this.getLineWidth(i);
26052
+ maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26053
+
26054
+ // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26055
+ const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
26056
+ if (rtlRegex.test(lineText)) {
26057
+ // Add minimal RTL compensation buffer - just enough to prevent clipping
26058
+ const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
26059
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
26060
+ } else {
26061
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
26062
+ }
26063
+ }
26064
+
26065
+ // Safety margin - how close glyphs can get before we snap
26066
+ const safetyThreshold = 2; // px - very subtle trigger
26067
+
26068
+ if (maxRequiredWidth > this.width - safetyThreshold) {
26069
+ var _this$canvas2;
26070
+ // Set width to exactly what's needed + minimal safety margin
26071
+ const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26072
+ console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26073
+ maxActualLineWidth: maxActualLineWidth.toFixed(1),
26074
+ maxRequiredWidth: maxRequiredWidth.toFixed(1),
26075
+ difference: (newWidth - this.width).toFixed(1)
26076
+ });
26077
+
26078
+ // Store original position before width change
26079
+ const originalLeft = this.left;
26080
+ const originalTop = this.top;
26081
+ const widthIncrease = newWidth - this.width;
26082
+
26083
+ // Change width
26084
+ this.set('width', newWidth);
26085
+
26086
+ // Force text layout recalculation
26087
+ this.initDimensions();
26088
+
26089
+ // Only compensate position when resizing from left handle
26090
+ // Right handle resize doesn't shift the text position
26091
+ if (resizeOrigin === 'left') {
26092
+ console.log('🔧 Compensating for left-side resize', {
26093
+ originalLeft,
26094
+ widthIncrease,
26095
+ newLeft: originalLeft - widthIncrease
26096
+ });
26097
+ // When resizing from left, the expansion pushes text right
26098
+ // Compensate by moving the textbox left by the width increase
26099
+ this.set({
26100
+ 'left': originalLeft - widthIncrease,
26101
+ 'top': originalTop
26102
+ });
26103
+ } else {
26104
+ console.log('✅ Right-side resize, no compensation needed');
26105
+ }
26106
+ this.setCoords();
26107
+
26108
+ // Also refresh the overlay editor if it exists
26109
+ if (this.__overlayEditor) {
26110
+ setTimeout(() => {
26111
+ this.__overlayEditor.refresh();
26112
+ }, 0);
26113
+ }
26114
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26115
+ }
26116
+ }
26117
+
25757
26118
  /**
25758
26119
  * Returns object representation of an instance
25759
26120
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output