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