@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.
@@ -21960,8 +21960,12 @@ class OverlayEditor {
21960
21960
  this.textarea.style.pointerEvents = 'auto';
21961
21961
  // Set appropriate unicodeBidi based on content and direction
21962
21962
  const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
21963
+ const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
21963
21964
  const isLTRDirection = this.target.direction === 'ltr';
21964
- if (hasArabicText && isLTRDirection) {
21965
+ if (hasArabicText && hasLatinText && isLTRDirection) {
21966
+ // For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
21967
+ this.textarea.style.unicodeBidi = 'embed';
21968
+ } else if (hasArabicText && isLTRDirection) {
21965
21969
  // For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
21966
21970
  this.textarea.style.unicodeBidi = 'embed';
21967
21971
  } else {
@@ -22050,14 +22054,26 @@ class OverlayEditor {
22050
22054
  parseFloat(this.hostDiv.style.width) / zoom;
22051
22055
  const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
22052
22056
 
22053
- // Only update if there's a meaningful change (avoid float precision issues)
22057
+ // Always update height for responsive controls (especially important for line deletion)
22054
22058
  const heightDiff = Math.abs(currentHeight - target.height);
22055
- const threshold = 1; // 1px threshold to avoid micro-changes
22059
+ const threshold = 0.5; // Lower threshold for better responsiveness to line changes
22056
22060
 
22057
22061
  if (heightDiff > threshold) {
22062
+ target.height;
22058
22063
  target.height = currentHeight;
22059
22064
  target.setCoords(); // Update control positions
22065
+
22066
+ // Force dirty to ensure proper re-rendering
22067
+ target.dirty = true;
22060
22068
  this.canvas.requestRenderAll(); // Re-render to show updated selection
22069
+
22070
+ // IMPORTANT: Reposition overlay after height change
22071
+ requestAnimationFrame(() => {
22072
+ if (!this.isDestroyed) {
22073
+ this.applyOverlayStyle();
22074
+ console.log('📐 Height changed - rechecking alignment after repositioning:');
22075
+ }
22076
+ });
22061
22077
  }
22062
22078
  }
22063
22079
 
@@ -22085,14 +22101,6 @@ class OverlayEditor {
22085
22101
  target.setCoords();
22086
22102
  const aCoords = target.aCoords;
22087
22103
 
22088
- // DEBUG: Log dimensions before edit
22089
- console.log('BEFORE EDIT:');
22090
- console.log(' target.width =', target.width);
22091
- console.log(' target.height =', target.height);
22092
- console.log(' target.getScaledWidth() =', target.getScaledWidth());
22093
- console.log(' target.getScaledHeight() =', target.getScaledHeight());
22094
- console.log(' target.padding =', target.padding);
22095
-
22096
22104
  // 2. Get canvas position and scroll offsets (like rtl-test.html)
22097
22105
  const canvasEl = canvas.upperCanvasEl;
22098
22106
  const canvasRect = canvasEl.getBoundingClientRect();
@@ -22115,14 +22123,12 @@ class OverlayEditor {
22115
22123
  const left = canvasRect.left + scrollX + screenPoint.x;
22116
22124
  const top = canvasRect.top + scrollY + screenPoint.y;
22117
22125
 
22118
- // 4. Get dimensions with zoom scaling - use target.width for text wrapping, scaled height for container
22119
- const width = target.width * (target.scaleX || 1) * zoom; // Account for object scale and viewport zoom
22120
- const height = target.height * (target.scaleY || 1) * zoom;
22121
- console.log('WIDTH CALCULATION:');
22122
- console.log(' target.width =', target.width);
22123
- console.log(' scaledWidth =', target.getScaledWidth());
22124
- console.log(' zoom =', zoom);
22125
- console.log(' final width =', width);
22126
+ // 4. Calculate the precise width and height for the container
22127
+ // **THE FIX:** Use getBoundingRect() for BOTH width and height.
22128
+ // This is the most reliable measure of the object's final rendered dimensions.
22129
+ const objectBounds = target.getBoundingRect();
22130
+ const width = Math.round(objectBounds.width * zoom);
22131
+ const height = Math.round(objectBounds.height * zoom);
22126
22132
 
22127
22133
  // 5. Apply styles to host DIV - absolute positioning like rtl-test.html
22128
22134
  this.hostDiv.style.position = 'absolute';
@@ -22146,50 +22152,209 @@ class OverlayEditor {
22146
22152
  const scaleX = target.scaleX || 1;
22147
22153
  const finalFontSize = baseFontSize * scaleX * zoom;
22148
22154
  const fabricLineHeight = target.lineHeight || 1.16;
22149
- // Apply padding and dimensions to textarea
22150
- const textareaWidth = paddingX > 0 ? `calc(100% - ${2 * paddingX}px)` : '100%';
22151
- const textareaHeight = paddingY > 0 ? `calc(100% - ${2 * paddingY}px)` : '100%';
22152
- this.textarea.style.width = textareaWidth;
22153
- this.textarea.style.height = textareaHeight;
22155
+ // **THE FIX:** Use 'border-box' so the width property includes padding.
22156
+ // This makes alignment much easier and more reliable.
22157
+ this.textarea.style.boxSizing = 'border-box';
22158
+
22159
+ // **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
22160
+ // The padding will now be correctly contained *inside* this width.
22161
+ this.textarea.style.width = `${width}px`;
22162
+ this.textarea.style.height = '100%'; // Let hostDiv control height
22154
22163
  this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
22164
+
22165
+ // Apply all other font and text styles to match Fabric
22166
+ const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
22155
22167
  this.textarea.style.fontSize = `${finalFontSize}px`;
22156
- this.textarea.style.lineHeight = String(fabricLineHeight); // Use unit-less multiplier
22168
+ this.textarea.style.lineHeight = String(fabricLineHeight);
22157
22169
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22158
22170
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22159
22171
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22160
22172
  this.textarea.style.textAlign = target.textAlign || 'left';
22161
22173
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22162
- this.textarea.style.letterSpacing = `${(target.charSpacing || 0) / 1000}em`;
22174
+ this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22163
22175
  this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
22164
-
22165
- // Ensure consistent font rendering between Fabric and CSS
22166
22176
  this.textarea.style.fontVariant = 'normal';
22167
22177
  this.textarea.style.fontStretch = 'normal';
22168
- this.textarea.style.textRendering = 'auto';
22169
- this.textarea.style.fontKerning = 'auto';
22170
- this.textarea.style.boxSizing = 'content-box'; // Padding is added outside width/height
22178
+ this.textarea.style.textRendering = 'optimizeLegibility';
22179
+ this.textarea.style.fontKerning = 'normal';
22180
+ this.textarea.style.fontFeatureSettings = 'normal';
22181
+ this.textarea.style.fontVariationSettings = 'normal';
22171
22182
  this.textarea.style.margin = '0';
22172
22183
  this.textarea.style.border = 'none';
22173
22184
  this.textarea.style.outline = 'none';
22174
22185
  this.textarea.style.background = 'transparent';
22175
- this.textarea.style.wordWrap = 'break-word';
22186
+ this.textarea.style.overflowWrap = 'break-word';
22176
22187
  this.textarea.style.whiteSpace = 'pre-wrap';
22188
+ this.textarea.style.hyphens = 'none';
22189
+ this.textarea.style.webkitFontSmoothing = 'antialiased';
22190
+ this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22177
22191
 
22178
- // DEBUG: Log final textarea dimensions
22179
- console.log('TEXTAREA AFTER SETUP:');
22180
- console.log(' textarea width =', this.textarea.style.width);
22181
- console.log(' textarea height =', this.textarea.style.height);
22182
- console.log(' textarea padding =', this.textarea.style.padding);
22183
- console.log(' paddingX =', paddingX, 'paddingY =', paddingY);
22184
- console.log(' baseFontSize =', baseFontSize);
22185
- console.log(' scaleX =', scaleX);
22186
- console.log(' zoom =', zoom);
22187
- console.log(' finalFontSize =', finalFontSize);
22188
- console.log(' fabricLineHeight =', fabricLineHeight);
22192
+ // Debug: Compare textarea and canvas object bounding boxes
22193
+ this.debugBoundingBoxComparison();
22194
+
22195
+ // Debug: Compare text wrapping behavior
22196
+ this.debugTextWrapping();
22189
22197
 
22190
22198
  // Initial bounds are set correctly by Fabric.js - don't force update here
22191
22199
  }
22192
22200
 
22201
+ /**
22202
+ * Debug method to compare textarea and canvas object bounding boxes
22203
+ */
22204
+ debugBoundingBoxComparison() {
22205
+ const target = this.target;
22206
+ const canvas = this.canvas;
22207
+ const zoom = canvas.getZoom();
22208
+
22209
+ // Get textarea bounding box (in screen coordinates)
22210
+ const textareaRect = this.textarea.getBoundingClientRect();
22211
+ const hostRect = this.hostDiv.getBoundingClientRect();
22212
+
22213
+ // Get canvas object bounding box (in screen coordinates)
22214
+ const canvasBounds = target.getBoundingRect();
22215
+ const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
22216
+
22217
+ // Convert canvas object bounds to screen coordinates
22218
+ const vpt = canvas.viewportTransform;
22219
+ const screenObjectBounds = {
22220
+ left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
22221
+ top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
22222
+ width: canvasBounds.width * zoom,
22223
+ height: canvasBounds.height * zoom
22224
+ };
22225
+ console.log('🔍 BOUNDING BOX COMPARISON:');
22226
+ console.log('📦 Textarea Rect:', {
22227
+ left: Math.round(textareaRect.left * 100) / 100,
22228
+ top: Math.round(textareaRect.top * 100) / 100,
22229
+ width: Math.round(textareaRect.width * 100) / 100,
22230
+ height: Math.round(textareaRect.height * 100) / 100
22231
+ });
22232
+ console.log('📦 Host Div Rect:', {
22233
+ left: Math.round(hostRect.left * 100) / 100,
22234
+ top: Math.round(hostRect.top * 100) / 100,
22235
+ width: Math.round(hostRect.width * 100) / 100,
22236
+ height: Math.round(hostRect.height * 100) / 100
22237
+ });
22238
+ console.log('📦 Canvas Object Bounds (screen):', {
22239
+ left: Math.round(screenObjectBounds.left * 100) / 100,
22240
+ top: Math.round(screenObjectBounds.top * 100) / 100,
22241
+ width: Math.round(screenObjectBounds.width * 100) / 100,
22242
+ height: Math.round(screenObjectBounds.height * 100) / 100
22243
+ });
22244
+ console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
22245
+
22246
+ // Calculate differences
22247
+ const hostVsObject = {
22248
+ leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
22249
+ topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
22250
+ widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
22251
+ heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
22252
+ };
22253
+ const textareaVsObject = {
22254
+ leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
22255
+ topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
22256
+ widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
22257
+ heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
22258
+ };
22259
+ console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
22260
+ console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
22261
+
22262
+ // Check if they're aligned (within 2px tolerance)
22263
+ const tolerance = 2;
22264
+ const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
22265
+ const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
22266
+ console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
22267
+ console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
22268
+ console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
22269
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22270
+ }
22271
+
22272
+ /**
22273
+ * Debug method to compare text wrapping between textarea and Fabric text object
22274
+ */
22275
+ debugTextWrapping() {
22276
+ const target = this.target;
22277
+ const text = this.textarea.value;
22278
+ console.log('📝 TEXT WRAPPING COMPARISON:');
22279
+ console.log('📄 Text Content:', `"${text}"`);
22280
+ console.log('📄 Text Length:', text.length);
22281
+
22282
+ // Analyze line breaks
22283
+ const explicitLines = text.split('\n');
22284
+ console.log('📄 Explicit Lines (\\n):', explicitLines.length);
22285
+ explicitLines.forEach((line, i) => {
22286
+ console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
22287
+ });
22288
+
22289
+ // Get textarea computed styles for wrapping analysis
22290
+ const textareaStyles = window.getComputedStyle(this.textarea);
22291
+ console.log('📐 Textarea Wrapping Styles:');
22292
+ console.log(' width:', textareaStyles.width);
22293
+ console.log(' fontSize:', textareaStyles.fontSize);
22294
+ console.log(' fontFamily:', textareaStyles.fontFamily);
22295
+ console.log(' fontWeight:', textareaStyles.fontWeight);
22296
+ console.log(' letterSpacing:', textareaStyles.letterSpacing);
22297
+ console.log(' lineHeight:', textareaStyles.lineHeight);
22298
+ console.log(' whiteSpace:', textareaStyles.whiteSpace);
22299
+ console.log(' wordWrap:', textareaStyles.wordWrap);
22300
+ console.log(' overflowWrap:', textareaStyles.overflowWrap);
22301
+ console.log(' direction:', textareaStyles.direction);
22302
+ console.log(' textAlign:', textareaStyles.textAlign);
22303
+
22304
+ // Get Fabric text object properties for comparison
22305
+ console.log('📐 Fabric Text Object Properties:');
22306
+ console.log(' width:', target.width);
22307
+ console.log(' fontSize:', target.fontSize);
22308
+ console.log(' fontFamily:', target.fontFamily);
22309
+ console.log(' fontWeight:', target.fontWeight);
22310
+ console.log(' charSpacing:', target.charSpacing);
22311
+ console.log(' lineHeight:', target.lineHeight);
22312
+ console.log(' direction:', target.direction);
22313
+ console.log(' textAlign:', target.textAlign);
22314
+ console.log(' scaleX:', target.scaleX);
22315
+ console.log(' scaleY:', target.scaleY);
22316
+
22317
+ // Calculate effective dimensions for comparison - use actual rendered width
22318
+ // **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
22319
+ const fabricEffectiveWidth = this.target.getBoundingRect().width;
22320
+ // Use the exact width set on textarea for comparison
22321
+ const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
22322
+ const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
22323
+ const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
22324
+ console.log('📏 Effective Width Comparison:');
22325
+ console.log(' Textarea Effective Width:', textareaEffectiveWidth);
22326
+ console.log(' Fabric Effective Width:', fabricEffectiveWidth);
22327
+ console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
22328
+ console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
22329
+
22330
+ // Check text direction and bidi handling
22331
+ const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22332
+ const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
22333
+ console.log('🌍 Text Direction Analysis:');
22334
+ console.log(' Has RTL characters:', hasRTLText);
22335
+ console.log(' Has mixed Bidi text:', hasBidiText);
22336
+ console.log(' Textarea direction:', textareaStyles.direction);
22337
+ console.log(' Fabric direction:', target.direction || 'auto');
22338
+ console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
22339
+
22340
+ // Measure actual rendered line count
22341
+ const textareaScrollHeight = this.textarea.scrollHeight;
22342
+ const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
22343
+ const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
22344
+ console.log('📊 Line Count Analysis:');
22345
+ console.log(' Textarea scrollHeight:', textareaScrollHeight);
22346
+ console.log(' Textarea lineHeight:', textareaLineHeight);
22347
+ console.log(' Estimated rendered lines:', estimatedTextareaLines);
22348
+ console.log(' Explicit line breaks:', explicitLines.length);
22349
+ if (estimatedTextareaLines > explicitLines.length) {
22350
+ console.log('🔄 Text wrapping detected in textarea');
22351
+ console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
22352
+ } else {
22353
+ console.log('📏 No text wrapping in textarea');
22354
+ }
22355
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22356
+ }
22357
+
22193
22358
  /**
22194
22359
  * Focus the textarea and position cursor at end
22195
22360
  */
@@ -22302,25 +22467,40 @@ class OverlayEditor {
22302
22467
  }
22303
22468
  }
22304
22469
  autoResizeTextarea() {
22305
- // Allow both vertical growth and shrinking; host width stays fixed
22306
- const oldHeight = parseFloat(window.getComputedStyle(this.textarea).height);
22307
-
22308
- // Reset height to measure actual needed height
22309
- this.textarea.style.height = 'auto';
22470
+ // Store the scroll position and the container's old height for comparison.
22471
+ const scrollTop = this.textarea.scrollTop;
22472
+ const oldHeight = parseFloat(this.hostDiv.style.height || '0');
22473
+
22474
+ // 1. **Force a reliable reflow.**
22475
+ // First, reset the textarea's height to a minimal value. This is the crucial step
22476
+ // that forces the browser to recalculate the content's height from scratch,
22477
+ // ignoring the hostDiv's larger, stale height.
22478
+ this.textarea.style.height = '1px';
22479
+
22480
+ // 2. Read the now-accurate scrollHeight. This value reflects the minimum
22481
+ // height required for the content, whether it's single or multi-line.
22310
22482
  const scrollHeight = this.textarea.scrollHeight;
22311
22483
 
22312
- // Add extra padding to prevent text clipping (especially for line height)
22313
- const lineHeightBuffer = 8; // Extra space to prevent clipping
22314
- const newHeight = Math.max(scrollHeight + lineHeightBuffer, 25); // Minimum height with buffer
22315
- const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
22484
+ // A small buffer for rendering consistency across browsers.
22485
+ const buffer = 2;
22486
+ const newHeight = scrollHeight + buffer;
22316
22487
 
22317
- this.textarea.style.height = `${newHeight}px`;
22318
- this.hostDiv.style.height = `${newHeight}px`; // Match exactly
22488
+ // Check if the height has changed significantly.
22489
+ const heightChanged = Math.abs(newHeight - oldHeight) > 1;
22319
22490
 
22320
- // Only update object bounds if height actually changed
22491
+ // 4. Only update heights and object bounds if there was a change.
22321
22492
  if (heightChanged) {
22493
+ this.textarea.style.height = `${newHeight}px`;
22494
+ this.hostDiv.style.height = `${newHeight}px`;
22322
22495
  this.updateObjectBounds();
22496
+ } else {
22497
+ // If no significant change, ensure the textarea's height matches the container
22498
+ // to prevent any minor visual misalignment.
22499
+ this.textarea.style.height = this.hostDiv.style.height;
22323
22500
  }
22501
+
22502
+ // 5. Restore the original scroll position.
22503
+ this.textarea.scrollTop = scrollTop;
22324
22504
  }
22325
22505
  handleKeyDown(e) {
22326
22506
  if (e.key === 'Escape') {
@@ -22329,6 +22509,19 @@ class OverlayEditor {
22329
22509
  } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
22330
22510
  e.preventDefault();
22331
22511
  this.destroy(true); // Commit
22512
+ } else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
22513
+ // For keys that might change the height, schedule a resize check
22514
+ // Use both immediate and delayed checks to catch all scenarios
22515
+ requestAnimationFrame(() => {
22516
+ if (!this.isDestroyed) {
22517
+ this.autoResizeTextarea();
22518
+ }
22519
+ });
22520
+ setTimeout(() => {
22521
+ if (!this.isDestroyed) {
22522
+ this.autoResizeTextarea();
22523
+ }
22524
+ }, 10); // Small delay to ensure DOM is updated
22332
22525
  }
22333
22526
  }
22334
22527
  handleFocus() {
@@ -25305,6 +25498,7 @@ class Textbox extends IText {
25305
25498
  ...Textbox.ownDefaults,
25306
25499
  ...options
25307
25500
  });
25501
+ this.initializeEventListeners();
25308
25502
  }
25309
25503
 
25310
25504
  /**
@@ -25812,6 +26006,173 @@ class Textbox extends IText {
25812
26006
  }
25813
26007
  }
25814
26008
 
26009
+ /**
26010
+ * Initialize event listeners for safety snap functionality
26011
+ * @private
26012
+ */
26013
+ initializeEventListeners() {
26014
+ var _this$canvas;
26015
+ // Track which side is being used for resize to handle position compensation
26016
+ let resizeOrigin = null;
26017
+
26018
+ // Detect resize origin during resizing
26019
+ this.on('resizing', e => {
26020
+ // Check transform origin to determine which side is being resized
26021
+ console.log('🔍 Resize event data:', e);
26022
+ if (e.transform) {
26023
+ const {
26024
+ originX,
26025
+ originY
26026
+ } = e.transform;
26027
+ console.log('🔍 Transform origins:', {
26028
+ originX,
26029
+ originY
26030
+ });
26031
+ // originX tells us which side is the anchor - opposite side is being dragged
26032
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26033
+ console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26034
+ } else if (e.originX) {
26035
+ const {
26036
+ originX,
26037
+ originY
26038
+ } = e;
26039
+ console.log('🔍 Event origins:', {
26040
+ originX,
26041
+ originY
26042
+ });
26043
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26044
+ console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26045
+ }
26046
+ });
26047
+
26048
+ // Only trigger safety snap after resize is complete (not during)
26049
+ // Use 'modified' event which fires after user releases the mouse
26050
+ this.on('modified', () => {
26051
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26052
+ console.log('✅ Modified event fired - resize complete, triggering safety snap', {
26053
+ resizeOrigin: currentResizeOrigin
26054
+ });
26055
+ // Small delay to ensure text layout is updated
26056
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26057
+ resizeOrigin = null; // Reset after capturing
26058
+ });
26059
+
26060
+ // Also listen to canvas-level modified event as backup
26061
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
26062
+ if (e.target === this) {
26063
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26064
+ console.log('✅ Canvas object:modified fired for this textbox');
26065
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26066
+ resizeOrigin = null; // Reset after capturing
26067
+ }
26068
+ });
26069
+ }
26070
+
26071
+ /**
26072
+ * Safety snap to prevent glyph clipping after manual resize.
26073
+ * Similar to Polotno - checks if any glyphs are too close to edges
26074
+ * and automatically expands width if needed.
26075
+ * @private
26076
+ * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26077
+ */
26078
+ safetySnapWidth(resizeOrigin) {
26079
+ var _this$_textLines;
26080
+ console.log('🔍 safetySnapWidth called', {
26081
+ isWrapping: this.isWrapping,
26082
+ hasTextLines: !!this._textLines,
26083
+ lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26084
+ currentWidth: this.width,
26085
+ type: this.type,
26086
+ text: this.text
26087
+ });
26088
+
26089
+ // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26090
+ if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26091
+ var _this$_textLines2;
26092
+ console.log('❌ Early return - missing requirements', {
26093
+ hasTextLines: !!this._textLines,
26094
+ typeMatch: this.type.toLowerCase() === 'textbox',
26095
+ actualType: this.type,
26096
+ hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26097
+ });
26098
+ return;
26099
+ }
26100
+ const lineCount = this._textLines.length;
26101
+ if (lineCount === 0) return;
26102
+
26103
+ // Check all lines, not just the last one
26104
+ let maxActualLineWidth = 0; // Actual measured width without buffers
26105
+ let maxRequiredWidth = 0; // Width including RTL buffer
26106
+
26107
+ for (let i = 0; i < lineCount; i++) {
26108
+ const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26109
+ const lineWidth = this.getLineWidth(i);
26110
+ maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26111
+
26112
+ // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26113
+ const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
26114
+ if (rtlRegex.test(lineText)) {
26115
+ // Add minimal RTL compensation buffer - just enough to prevent clipping
26116
+ const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
26117
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
26118
+ } else {
26119
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
26120
+ }
26121
+ }
26122
+
26123
+ // Safety margin - how close glyphs can get before we snap
26124
+ const safetyThreshold = 2; // px - very subtle trigger
26125
+
26126
+ if (maxRequiredWidth > this.width - safetyThreshold) {
26127
+ var _this$canvas2;
26128
+ // Set width to exactly what's needed + minimal safety margin
26129
+ const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26130
+ console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26131
+ maxActualLineWidth: maxActualLineWidth.toFixed(1),
26132
+ maxRequiredWidth: maxRequiredWidth.toFixed(1),
26133
+ difference: (newWidth - this.width).toFixed(1)
26134
+ });
26135
+
26136
+ // Store original position before width change
26137
+ const originalLeft = this.left;
26138
+ const originalTop = this.top;
26139
+ const widthIncrease = newWidth - this.width;
26140
+
26141
+ // Change width
26142
+ this.set('width', newWidth);
26143
+
26144
+ // Force text layout recalculation
26145
+ this.initDimensions();
26146
+
26147
+ // Only compensate position when resizing from left handle
26148
+ // Right handle resize doesn't shift the text position
26149
+ if (resizeOrigin === 'left') {
26150
+ console.log('🔧 Compensating for left-side resize', {
26151
+ originalLeft,
26152
+ widthIncrease,
26153
+ newLeft: originalLeft - widthIncrease
26154
+ });
26155
+ // When resizing from left, the expansion pushes text right
26156
+ // Compensate by moving the textbox left by the width increase
26157
+ this.set({
26158
+ 'left': originalLeft - widthIncrease,
26159
+ 'top': originalTop
26160
+ });
26161
+ } else {
26162
+ console.log('✅ Right-side resize, no compensation needed');
26163
+ }
26164
+ this.setCoords();
26165
+
26166
+ // Also refresh the overlay editor if it exists
26167
+ if (this.__overlayEditor) {
26168
+ setTimeout(() => {
26169
+ this.__overlayEditor.refresh();
26170
+ }, 0);
26171
+ }
26172
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26173
+ }
26174
+ }
26175
+
25815
26176
  /**
25816
26177
  * Returns object representation of an instance
25817
26178
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output