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