@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 +416 -55
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +416 -55
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +416 -55
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +416 -55
- package/dist/index.node.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +13 -0
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +168 -0
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/text/overlayEditor.d.ts +8 -0
- package/dist/src/text/overlayEditor.d.ts.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +248 -55
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +13 -0
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/text/overlayEditor.d.ts +8 -0
- package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shapes/Textbox.ts +119 -0
- package/src/text/overlayEditor.ts +354 -96
package/dist/index.node.mjs
CHANGED
|
@@ -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
|
-
//
|
|
22055
|
+
// Always update height for responsive controls (especially important for line deletion)
|
|
22052
22056
|
const heightDiff = Math.abs(currentHeight - target.height);
|
|
22053
|
-
const threshold =
|
|
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.
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
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
|
-
//
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
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);
|
|
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 = `${
|
|
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 = '
|
|
22167
|
-
this.textarea.style.fontKerning = '
|
|
22168
|
-
this.textarea.style.
|
|
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.
|
|
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
|
-
//
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
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
|
-
//
|
|
22304
|
-
const
|
|
22305
|
-
|
|
22306
|
-
|
|
22307
|
-
|
|
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
|
-
//
|
|
22311
|
-
const
|
|
22312
|
-
const newHeight =
|
|
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
|
-
|
|
22316
|
-
|
|
22486
|
+
// Check if the height has changed significantly.
|
|
22487
|
+
const heightChanged = Math.abs(newHeight - oldHeight) > 1;
|
|
22317
22488
|
|
|
22318
|
-
// Only update object bounds if
|
|
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
|