@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
|
@@ -82,10 +82,10 @@ export class OverlayEditor {
|
|
|
82
82
|
if (!container) {
|
|
83
83
|
throw new Error('Canvas must be mounted in DOM to use overlay editing');
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
// Ensure the container is positioned for absolute overlay positioning
|
|
87
87
|
container.style.position = 'relative';
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
return container;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -109,10 +109,17 @@ export class OverlayEditor {
|
|
|
109
109
|
this.textarea.style.resize = 'none';
|
|
110
110
|
this.textarea.style.pointerEvents = 'auto';
|
|
111
111
|
// Set appropriate unicodeBidi based on content and direction
|
|
112
|
-
const hasArabicText =
|
|
112
|
+
const hasArabicText =
|
|
113
|
+
/[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(
|
|
114
|
+
this.target.text || '',
|
|
115
|
+
);
|
|
116
|
+
const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
|
|
113
117
|
const isLTRDirection = (this.target as any).direction === 'ltr';
|
|
114
|
-
|
|
115
|
-
if (hasArabicText && isLTRDirection) {
|
|
118
|
+
|
|
119
|
+
if (hasArabicText && hasLatinText && isLTRDirection) {
|
|
120
|
+
// For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
|
|
121
|
+
this.textarea.style.unicodeBidi = 'embed';
|
|
122
|
+
} else if (hasArabicText && isLTRDirection) {
|
|
116
123
|
// For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
|
|
117
124
|
this.textarea.style.unicodeBidi = 'embed';
|
|
118
125
|
} else {
|
|
@@ -165,7 +172,7 @@ export class OverlayEditor {
|
|
|
165
172
|
this.canvas.on('after:render', this.boundHandlers.onAfterRender);
|
|
166
173
|
this.canvas.on('mouse:wheel', this.boundHandlers.onMouseWheel);
|
|
167
174
|
this.canvas.on('mouse:down', this.boundHandlers.onMouseDown);
|
|
168
|
-
|
|
175
|
+
|
|
169
176
|
// Store original methods to detect viewport changes
|
|
170
177
|
this.setupViewportChangeDetection();
|
|
171
178
|
}
|
|
@@ -190,7 +197,7 @@ export class OverlayEditor {
|
|
|
190
197
|
this.canvas.off('after:render', this.boundHandlers.onAfterRender);
|
|
191
198
|
this.canvas.off('mouse:wheel', this.boundHandlers.onMouseWheel);
|
|
192
199
|
this.canvas.off('mouse:down', this.boundHandlers.onMouseDown);
|
|
193
|
-
|
|
200
|
+
|
|
194
201
|
// Restore original methods
|
|
195
202
|
this.restoreViewportChangeDetection();
|
|
196
203
|
}
|
|
@@ -211,19 +218,33 @@ export class OverlayEditor {
|
|
|
211
218
|
|
|
212
219
|
const target = this.target;
|
|
213
220
|
const zoom = this.canvas.getZoom();
|
|
214
|
-
|
|
221
|
+
|
|
215
222
|
// Get current textbox dimensions from the host div (in canvas coordinates)
|
|
216
223
|
const currentWidth = parseFloat(this.hostDiv.style.width) / zoom;
|
|
217
224
|
const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
|
|
218
|
-
|
|
219
|
-
//
|
|
225
|
+
|
|
226
|
+
// Always update height for responsive controls (especially important for line deletion)
|
|
220
227
|
const heightDiff = Math.abs(currentHeight - target.height);
|
|
221
|
-
const threshold =
|
|
222
|
-
|
|
228
|
+
const threshold = 0.5; // Lower threshold for better responsiveness to line changes
|
|
229
|
+
|
|
223
230
|
if (heightDiff > threshold) {
|
|
231
|
+
const oldHeight = target.height;
|
|
224
232
|
target.height = currentHeight;
|
|
225
233
|
target.setCoords(); // Update control positions
|
|
234
|
+
|
|
235
|
+
// Force dirty to ensure proper re-rendering
|
|
236
|
+
target.dirty = true;
|
|
226
237
|
this.canvas.requestRenderAll(); // Re-render to show updated selection
|
|
238
|
+
|
|
239
|
+
// IMPORTANT: Reposition overlay after height change
|
|
240
|
+
requestAnimationFrame(() => {
|
|
241
|
+
if (!this.isDestroyed) {
|
|
242
|
+
this.applyOverlayStyle();
|
|
243
|
+
console.log(
|
|
244
|
+
'📐 Height changed - rechecking alignment after repositioning:',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
227
248
|
}
|
|
228
249
|
}
|
|
229
250
|
|
|
@@ -251,15 +272,7 @@ export class OverlayEditor {
|
|
|
251
272
|
// 1. Freshen object's transformations - use aCoords like rtl-test.html
|
|
252
273
|
target.setCoords();
|
|
253
274
|
const aCoords = target.aCoords;
|
|
254
|
-
|
|
255
|
-
// DEBUG: Log dimensions before edit
|
|
256
|
-
console.log('BEFORE EDIT:');
|
|
257
|
-
console.log(' target.width =', (target as any).width);
|
|
258
|
-
console.log(' target.height =', target.height);
|
|
259
|
-
console.log(' target.getScaledWidth() =', target.getScaledWidth());
|
|
260
|
-
console.log(' target.getScaledHeight() =', target.getScaledHeight());
|
|
261
|
-
console.log(' target.padding =', (target as any).padding);
|
|
262
|
-
|
|
275
|
+
|
|
263
276
|
// 2. Get canvas position and scroll offsets (like rtl-test.html)
|
|
264
277
|
const canvasEl = canvas.upperCanvasEl;
|
|
265
278
|
const canvasRect = canvasEl.getBoundingClientRect();
|
|
@@ -275,20 +288,20 @@ export class OverlayEditor {
|
|
|
275
288
|
|
|
276
289
|
// Transform object's top-left corner coordinates to screen coordinates using viewport transform
|
|
277
290
|
// aCoords.tl already accounts for object positioning and scaling, just need viewport transform
|
|
278
|
-
const screenPoint = transformPoint(
|
|
279
|
-
|
|
291
|
+
const screenPoint = transformPoint(
|
|
292
|
+
{ x: aCoords.tl.x, y: aCoords.tl.y },
|
|
293
|
+
vpt,
|
|
294
|
+
);
|
|
295
|
+
|
|
280
296
|
const left = canvasRect.left + scrollX + screenPoint.x;
|
|
281
297
|
const top = canvasRect.top + scrollY + screenPoint.y;
|
|
282
298
|
|
|
283
|
-
// 4.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
console.log(' scaledWidth =', target.getScaledWidth());
|
|
290
|
-
console.log(' zoom =', zoom);
|
|
291
|
-
console.log(' final width =', width);
|
|
299
|
+
// 4. Calculate the precise width and height for the container
|
|
300
|
+
// **THE FIX:** Use getBoundingRect() for BOTH width and height.
|
|
301
|
+
// This is the most reliable measure of the object's final rendered dimensions.
|
|
302
|
+
const objectBounds = target.getBoundingRect();
|
|
303
|
+
const width = Math.round(objectBounds.width * zoom);
|
|
304
|
+
const height = Math.round(objectBounds.height * zoom);
|
|
292
305
|
|
|
293
306
|
// 5. Apply styles to host DIV - absolute positioning like rtl-test.html
|
|
294
307
|
this.hostDiv.style.position = 'absolute';
|
|
@@ -307,99 +320,308 @@ export class OverlayEditor {
|
|
|
307
320
|
}
|
|
308
321
|
|
|
309
322
|
// 6. Style the textarea - match Fabric's exact rendering with padding
|
|
310
|
-
const baseFontSize =
|
|
323
|
+
const baseFontSize = target.fontSize ?? 16;
|
|
311
324
|
// Use scaleX for font scaling to match Fabric text scaling exactly
|
|
312
325
|
const scaleX = target.scaleX || 1;
|
|
313
326
|
const finalFontSize = baseFontSize * scaleX * zoom;
|
|
314
327
|
const fabricLineHeight = target.lineHeight || 1.16;
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.
|
|
328
|
+
// **THE FIX:** Use 'border-box' so the width property includes padding.
|
|
329
|
+
// This makes alignment much easier and more reliable.
|
|
330
|
+
this.textarea.style.boxSizing = 'border-box';
|
|
331
|
+
|
|
332
|
+
// **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
|
|
333
|
+
// The padding will now be correctly contained *inside* this width.
|
|
334
|
+
this.textarea.style.width = `${width}px`;
|
|
335
|
+
this.textarea.style.height = '100%'; // Let hostDiv control height
|
|
321
336
|
this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
|
|
322
|
-
|
|
337
|
+
|
|
338
|
+
// Apply all other font and text styles to match Fabric
|
|
339
|
+
const letterSpacingPx = ((target.charSpacing || 0) / 1000) * finalFontSize;
|
|
340
|
+
|
|
323
341
|
this.textarea.style.fontSize = `${finalFontSize}px`;
|
|
324
|
-
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
342
|
+
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
325
343
|
this.textarea.style.fontFamily = target.fontFamily || 'Arial';
|
|
326
344
|
this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
|
|
327
345
|
this.textarea.style.fontStyle = target.fontStyle || 'normal';
|
|
328
346
|
this.textarea.style.textAlign = (target as any).textAlign || 'left';
|
|
329
347
|
this.textarea.style.color = target.fill?.toString() || '#000';
|
|
330
|
-
this.textarea.style.letterSpacing = `${
|
|
331
|
-
this.textarea.style.direction =
|
|
332
|
-
|
|
333
|
-
|
|
348
|
+
this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
|
|
349
|
+
this.textarea.style.direction =
|
|
350
|
+
(target as any).direction ||
|
|
351
|
+
this.firstStrongDir(this.textarea.value || '');
|
|
334
352
|
this.textarea.style.fontVariant = 'normal';
|
|
335
353
|
this.textarea.style.fontStretch = 'normal';
|
|
336
|
-
this.textarea.style.textRendering = '
|
|
337
|
-
this.textarea.style.fontKerning = '
|
|
338
|
-
this.textarea.style.
|
|
354
|
+
this.textarea.style.textRendering = 'optimizeLegibility';
|
|
355
|
+
this.textarea.style.fontKerning = 'normal';
|
|
356
|
+
this.textarea.style.fontFeatureSettings = 'normal';
|
|
357
|
+
this.textarea.style.fontVariationSettings = 'normal';
|
|
339
358
|
this.textarea.style.margin = '0';
|
|
340
359
|
this.textarea.style.border = 'none';
|
|
341
360
|
this.textarea.style.outline = 'none';
|
|
342
361
|
this.textarea.style.background = 'transparent';
|
|
343
|
-
this.textarea.style.
|
|
362
|
+
this.textarea.style.overflowWrap = 'break-word';
|
|
344
363
|
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
364
|
+
this.textarea.style.hyphens = 'none';
|
|
345
365
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.log(' zoom =', zoom);
|
|
355
|
-
console.log(' finalFontSize =', finalFontSize);
|
|
356
|
-
console.log(' fabricLineHeight =', fabricLineHeight);
|
|
366
|
+
(this.textarea.style as any).webkitFontSmoothing = 'antialiased';
|
|
367
|
+
(this.textarea.style as any).mozOsxFontSmoothing = 'grayscale';
|
|
368
|
+
|
|
369
|
+
// Debug: Compare textarea and canvas object bounding boxes
|
|
370
|
+
this.debugBoundingBoxComparison();
|
|
371
|
+
|
|
372
|
+
// Debug: Compare text wrapping behavior
|
|
373
|
+
this.debugTextWrapping();
|
|
357
374
|
|
|
358
375
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
376
|
+
}
|
|
359
377
|
|
|
360
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Debug method to compare textarea and canvas object bounding boxes
|
|
380
|
+
*/
|
|
381
|
+
private debugBoundingBoxComparison(): void {
|
|
382
|
+
const target = this.target;
|
|
383
|
+
const canvas = this.canvas;
|
|
384
|
+
const zoom = canvas.getZoom();
|
|
385
|
+
|
|
386
|
+
// Get textarea bounding box (in screen coordinates)
|
|
387
|
+
const textareaRect = this.textarea.getBoundingClientRect();
|
|
388
|
+
const hostRect = this.hostDiv.getBoundingClientRect();
|
|
389
|
+
|
|
390
|
+
// Get canvas object bounding box (in screen coordinates)
|
|
391
|
+
const canvasBounds = target.getBoundingRect();
|
|
392
|
+
const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
|
|
393
|
+
|
|
394
|
+
// Convert canvas object bounds to screen coordinates
|
|
395
|
+
const vpt = canvas.viewportTransform;
|
|
396
|
+
const screenObjectBounds = {
|
|
397
|
+
left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
|
|
398
|
+
top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
|
|
399
|
+
width: canvasBounds.width * zoom,
|
|
400
|
+
height: canvasBounds.height * zoom,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
404
|
+
console.log('📦 Textarea Rect:', {
|
|
405
|
+
left: Math.round(textareaRect.left * 100) / 100,
|
|
406
|
+
top: Math.round(textareaRect.top * 100) / 100,
|
|
407
|
+
width: Math.round(textareaRect.width * 100) / 100,
|
|
408
|
+
height: Math.round(textareaRect.height * 100) / 100,
|
|
409
|
+
});
|
|
410
|
+
console.log('📦 Host Div Rect:', {
|
|
411
|
+
left: Math.round(hostRect.left * 100) / 100,
|
|
412
|
+
top: Math.round(hostRect.top * 100) / 100,
|
|
413
|
+
width: Math.round(hostRect.width * 100) / 100,
|
|
414
|
+
height: Math.round(hostRect.height * 100) / 100,
|
|
415
|
+
});
|
|
416
|
+
console.log('📦 Canvas Object Bounds (screen):', {
|
|
417
|
+
left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
418
|
+
top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
419
|
+
width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
420
|
+
height: Math.round(screenObjectBounds.height * 100) / 100,
|
|
421
|
+
});
|
|
422
|
+
console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
423
|
+
|
|
424
|
+
// Calculate differences
|
|
425
|
+
const hostVsObject = {
|
|
426
|
+
leftDiff:
|
|
427
|
+
Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
428
|
+
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
429
|
+
widthDiff:
|
|
430
|
+
Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
431
|
+
heightDiff:
|
|
432
|
+
Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const textareaVsObject = {
|
|
436
|
+
leftDiff:
|
|
437
|
+
Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
438
|
+
topDiff:
|
|
439
|
+
Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
440
|
+
widthDiff:
|
|
441
|
+
Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
442
|
+
heightDiff:
|
|
443
|
+
Math.round((textareaRect.height - screenObjectBounds.height) * 100) /
|
|
444
|
+
100,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
448
|
+
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
449
|
+
|
|
450
|
+
// Check if they're aligned (within 2px tolerance)
|
|
451
|
+
const tolerance = 2;
|
|
452
|
+
const hostAligned =
|
|
453
|
+
Math.abs(hostVsObject.leftDiff) < tolerance &&
|
|
454
|
+
Math.abs(hostVsObject.topDiff) < tolerance &&
|
|
455
|
+
Math.abs(hostVsObject.widthDiff) < tolerance &&
|
|
456
|
+
Math.abs(hostVsObject.heightDiff) < tolerance;
|
|
457
|
+
|
|
458
|
+
const textareaAligned =
|
|
459
|
+
Math.abs(textareaVsObject.leftDiff) < tolerance &&
|
|
460
|
+
Math.abs(textareaVsObject.topDiff) < tolerance &&
|
|
461
|
+
Math.abs(textareaVsObject.widthDiff) < tolerance &&
|
|
462
|
+
Math.abs(textareaVsObject.heightDiff) < tolerance;
|
|
463
|
+
|
|
464
|
+
console.log(
|
|
465
|
+
hostAligned
|
|
466
|
+
? '✅ Host Div ALIGNED with canvas object'
|
|
467
|
+
: '❌ Host Div MISALIGNED with canvas object',
|
|
468
|
+
);
|
|
469
|
+
console.log(
|
|
470
|
+
textareaAligned
|
|
471
|
+
? '✅ Textarea ALIGNED with canvas object'
|
|
472
|
+
: '❌ Textarea MISALIGNED with canvas object',
|
|
473
|
+
);
|
|
474
|
+
console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
475
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
361
476
|
}
|
|
362
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
480
|
+
*/
|
|
481
|
+
private debugTextWrapping(): void {
|
|
482
|
+
const target = this.target;
|
|
483
|
+
const text = this.textarea.value;
|
|
484
|
+
|
|
485
|
+
console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
486
|
+
console.log('📄 Text Content:', `"${text}"`);
|
|
487
|
+
console.log('📄 Text Length:', text.length);
|
|
488
|
+
|
|
489
|
+
// Analyze line breaks
|
|
490
|
+
const explicitLines = text.split('\n');
|
|
491
|
+
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
492
|
+
explicitLines.forEach((line, i) => {
|
|
493
|
+
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Get textarea computed styles for wrapping analysis
|
|
497
|
+
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
498
|
+
console.log('📐 Textarea Wrapping Styles:');
|
|
499
|
+
console.log(' width:', textareaStyles.width);
|
|
500
|
+
console.log(' fontSize:', textareaStyles.fontSize);
|
|
501
|
+
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
502
|
+
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
503
|
+
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
504
|
+
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
505
|
+
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
506
|
+
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
507
|
+
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
508
|
+
console.log(' direction:', textareaStyles.direction);
|
|
509
|
+
console.log(' textAlign:', textareaStyles.textAlign);
|
|
510
|
+
|
|
511
|
+
// Get Fabric text object properties for comparison
|
|
512
|
+
console.log('📐 Fabric Text Object Properties:');
|
|
513
|
+
console.log(' width:', (target as any).width);
|
|
514
|
+
console.log(' fontSize:', target.fontSize);
|
|
515
|
+
console.log(' fontFamily:', target.fontFamily);
|
|
516
|
+
console.log(' fontWeight:', target.fontWeight);
|
|
517
|
+
console.log(' charSpacing:', target.charSpacing);
|
|
518
|
+
console.log(' lineHeight:', target.lineHeight);
|
|
519
|
+
console.log(' direction:', (target as any).direction);
|
|
520
|
+
console.log(' textAlign:', (target as any).textAlign);
|
|
521
|
+
console.log(' scaleX:', target.scaleX);
|
|
522
|
+
console.log(' scaleY:', target.scaleY);
|
|
523
|
+
|
|
524
|
+
// Calculate effective dimensions for comparison - use actual rendered width
|
|
525
|
+
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
526
|
+
const fabricEffectiveWidth = this.target.getBoundingRect().width;
|
|
527
|
+
// Use the exact width set on textarea for comparison
|
|
528
|
+
const textareaComputedWidth = parseFloat(
|
|
529
|
+
window.getComputedStyle(this.textarea).width,
|
|
530
|
+
);
|
|
531
|
+
const textareaEffectiveWidth =
|
|
532
|
+
textareaComputedWidth / this.canvas.getZoom();
|
|
533
|
+
const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
|
|
534
|
+
|
|
535
|
+
console.log('📏 Effective Width Comparison:');
|
|
536
|
+
console.log(' Textarea Effective Width:', textareaEffectiveWidth);
|
|
537
|
+
console.log(' Fabric Effective Width:', fabricEffectiveWidth);
|
|
538
|
+
console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
|
|
539
|
+
console.log(
|
|
540
|
+
widthDiff < 1
|
|
541
|
+
? '✅ Widths MATCH for wrapping'
|
|
542
|
+
: '❌ Width MISMATCH may cause different wrapping',
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// Check text direction and bidi handling
|
|
546
|
+
const hasRTLText =
|
|
547
|
+
/[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(
|
|
548
|
+
text,
|
|
549
|
+
);
|
|
550
|
+
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
551
|
+
|
|
552
|
+
console.log('🌍 Text Direction Analysis:');
|
|
553
|
+
console.log(' Has RTL characters:', hasRTLText);
|
|
554
|
+
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
555
|
+
console.log(' Textarea direction:', textareaStyles.direction);
|
|
556
|
+
console.log(' Fabric direction:', (target as any).direction || 'auto');
|
|
557
|
+
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
558
|
+
|
|
559
|
+
// Measure actual rendered line count
|
|
560
|
+
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
561
|
+
const textareaLineHeight =
|
|
562
|
+
parseFloat(textareaStyles.lineHeight) ||
|
|
563
|
+
parseFloat(textareaStyles.fontSize) * 1.2;
|
|
564
|
+
const estimatedTextareaLines = Math.round(
|
|
565
|
+
textareaScrollHeight / textareaLineHeight,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
console.log('📊 Line Count Analysis:');
|
|
569
|
+
console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
570
|
+
console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
571
|
+
console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
572
|
+
console.log(' Explicit line breaks:', explicitLines.length);
|
|
573
|
+
|
|
574
|
+
if (estimatedTextareaLines > explicitLines.length) {
|
|
575
|
+
console.log('🔄 Text wrapping detected in textarea');
|
|
576
|
+
console.log(
|
|
577
|
+
' Wrapped lines:',
|
|
578
|
+
estimatedTextareaLines - explicitLines.length,
|
|
579
|
+
);
|
|
580
|
+
} else {
|
|
581
|
+
console.log('📏 No text wrapping in textarea');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
363
588
|
/**
|
|
364
589
|
* Focus the textarea and position cursor at end
|
|
365
590
|
*/
|
|
366
591
|
private focusTextarea(): void {
|
|
367
|
-
|
|
368
|
-
|
|
369
592
|
// For overlay editing, we want to keep the object in "selection mode" not "editing mode"
|
|
370
593
|
// This means keeping selected=true and isEditing=false to show boundaries
|
|
371
|
-
|
|
594
|
+
|
|
372
595
|
// Hide the text content only (not the entire object)
|
|
373
596
|
this.target.opacity = 0.01; // Nearly transparent but not fully hidden
|
|
374
|
-
|
|
597
|
+
|
|
375
598
|
// Ensure object stays selected to show boundaries
|
|
376
599
|
(this.target as any).selected = true;
|
|
377
600
|
(this.target as any).isEditing = false; // Override any editing state
|
|
378
|
-
|
|
601
|
+
|
|
379
602
|
// Make sure controls are enabled and movement is allowed during overlay editing
|
|
380
603
|
this.target.set({
|
|
381
604
|
hasControls: true,
|
|
382
605
|
hasBorders: true,
|
|
383
606
|
selectable: true,
|
|
384
607
|
lockMovementX: false,
|
|
385
|
-
lockMovementY: false
|
|
608
|
+
lockMovementY: false,
|
|
386
609
|
});
|
|
387
|
-
|
|
610
|
+
|
|
388
611
|
// Keep as active object
|
|
389
612
|
this.canvas.setActiveObject(this.target);
|
|
390
|
-
|
|
613
|
+
|
|
391
614
|
this.canvas.requestRenderAll();
|
|
392
615
|
this.target.setCoords();
|
|
393
616
|
this.applyOverlayStyle();
|
|
394
617
|
|
|
395
|
-
|
|
396
|
-
|
|
397
618
|
this.textarea.focus();
|
|
619
|
+
|
|
398
620
|
this.textarea.setSelectionRange(
|
|
399
621
|
this.textarea.value.length,
|
|
400
622
|
this.textarea.value.length,
|
|
401
623
|
);
|
|
402
|
-
|
|
624
|
+
|
|
403
625
|
// Ensure the object stays selected even after textarea focus
|
|
404
626
|
this.canvas.setActiveObject(this.target);
|
|
405
627
|
this.canvas.requestRenderAll();
|
|
@@ -470,6 +692,7 @@ export class OverlayEditor {
|
|
|
470
692
|
// Live update target text
|
|
471
693
|
this.target.text = this.textarea.value;
|
|
472
694
|
|
|
695
|
+
|
|
473
696
|
// Auto-resize textarea to match new content
|
|
474
697
|
this.autoResizeTextarea();
|
|
475
698
|
|
|
@@ -482,25 +705,40 @@ export class OverlayEditor {
|
|
|
482
705
|
}
|
|
483
706
|
|
|
484
707
|
private autoResizeTextarea(): void {
|
|
485
|
-
//
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
708
|
+
// Store the scroll position and the container's old height for comparison.
|
|
709
|
+
const scrollTop = this.textarea.scrollTop;
|
|
710
|
+
const oldHeight = parseFloat(this.hostDiv.style.height || '0');
|
|
711
|
+
|
|
712
|
+
// 1. **Force a reliable reflow.**
|
|
713
|
+
// First, reset the textarea's height to a minimal value. This is the crucial step
|
|
714
|
+
// that forces the browser to recalculate the content's height from scratch,
|
|
715
|
+
// ignoring the hostDiv's larger, stale height.
|
|
716
|
+
this.textarea.style.height = '1px';
|
|
717
|
+
|
|
718
|
+
// 2. Read the now-accurate scrollHeight. This value reflects the minimum
|
|
719
|
+
// height required for the content, whether it's single or multi-line.
|
|
490
720
|
const scrollHeight = this.textarea.scrollHeight;
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
const
|
|
494
|
-
const newHeight =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// Only update object bounds if height actually changed
|
|
721
|
+
|
|
722
|
+
// A small buffer for rendering consistency across browsers.
|
|
723
|
+
const buffer = 2;
|
|
724
|
+
const newHeight = scrollHeight + buffer;
|
|
725
|
+
|
|
726
|
+
// Check if the height has changed significantly.
|
|
727
|
+
const heightChanged = Math.abs(newHeight - oldHeight) > 1;
|
|
728
|
+
|
|
729
|
+
// 4. Only update heights and object bounds if there was a change.
|
|
501
730
|
if (heightChanged) {
|
|
731
|
+
this.textarea.style.height = `${newHeight}px`;
|
|
732
|
+
this.hostDiv.style.height = `${newHeight}px`;
|
|
502
733
|
this.updateObjectBounds();
|
|
734
|
+
} else {
|
|
735
|
+
// If no significant change, ensure the textarea's height matches the container
|
|
736
|
+
// to prevent any minor visual misalignment.
|
|
737
|
+
this.textarea.style.height = this.hostDiv.style.height;
|
|
503
738
|
}
|
|
739
|
+
|
|
740
|
+
// 5. Restore the original scroll position.
|
|
741
|
+
this.textarea.scrollTop = scrollTop;
|
|
504
742
|
}
|
|
505
743
|
|
|
506
744
|
private handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -510,6 +748,23 @@ export class OverlayEditor {
|
|
|
510
748
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
511
749
|
e.preventDefault();
|
|
512
750
|
this.destroy(true); // Commit
|
|
751
|
+
} else if (
|
|
752
|
+
e.key === 'Enter' ||
|
|
753
|
+
e.key === 'Backspace' ||
|
|
754
|
+
e.key === 'Delete'
|
|
755
|
+
) {
|
|
756
|
+
// For keys that might change the height, schedule a resize check
|
|
757
|
+
// Use both immediate and delayed checks to catch all scenarios
|
|
758
|
+
requestAnimationFrame(() => {
|
|
759
|
+
if (!this.isDestroyed) {
|
|
760
|
+
this.autoResizeTextarea();
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
if (!this.isDestroyed) {
|
|
765
|
+
this.autoResizeTextarea();
|
|
766
|
+
}
|
|
767
|
+
}, 10); // Small delay to ensure DOM is updated
|
|
513
768
|
}
|
|
514
769
|
}
|
|
515
770
|
|
|
@@ -553,9 +808,10 @@ export class OverlayEditor {
|
|
|
553
808
|
private setupViewportChangeDetection(): void {
|
|
554
809
|
// Store original methods
|
|
555
810
|
(this.canvas as any).__originalSetZoom = this.canvas.setZoom;
|
|
556
|
-
(this.canvas as any).__originalSetViewportTransform =
|
|
811
|
+
(this.canvas as any).__originalSetViewportTransform =
|
|
812
|
+
this.canvas.setViewportTransform;
|
|
557
813
|
(this.canvas as any).__overlayEditor = this;
|
|
558
|
-
|
|
814
|
+
|
|
559
815
|
// Override setZoom to detect zoom changes
|
|
560
816
|
const originalSetZoom = this.canvas.setZoom.bind(this.canvas);
|
|
561
817
|
this.canvas.setZoom = (value: number) => {
|
|
@@ -565,9 +821,11 @@ export class OverlayEditor {
|
|
|
565
821
|
}
|
|
566
822
|
return result;
|
|
567
823
|
};
|
|
568
|
-
|
|
824
|
+
|
|
569
825
|
// Override setViewportTransform to detect pan changes
|
|
570
|
-
const originalSetViewportTransform = this.canvas.setViewportTransform.bind(
|
|
826
|
+
const originalSetViewportTransform = this.canvas.setViewportTransform.bind(
|
|
827
|
+
this.canvas,
|
|
828
|
+
);
|
|
571
829
|
this.canvas.setViewportTransform = (vpt: TMat2D) => {
|
|
572
830
|
const result = originalSetViewportTransform(vpt);
|
|
573
831
|
if ((this.canvas as any).__overlayEditor && !this.isDestroyed) {
|
|
@@ -576,7 +834,7 @@ export class OverlayEditor {
|
|
|
576
834
|
return result;
|
|
577
835
|
};
|
|
578
836
|
}
|
|
579
|
-
|
|
837
|
+
|
|
580
838
|
/**
|
|
581
839
|
* Restore original viewport methods
|
|
582
840
|
*/
|
|
@@ -586,13 +844,13 @@ export class OverlayEditor {
|
|
|
586
844
|
delete (this.canvas as any).__originalSetZoom;
|
|
587
845
|
}
|
|
588
846
|
if ((this.canvas as any).__originalSetViewportTransform) {
|
|
589
|
-
this.canvas.setViewportTransform = (
|
|
847
|
+
this.canvas.setViewportTransform = (
|
|
848
|
+
this.canvas as any
|
|
849
|
+
).__originalSetViewportTransform;
|
|
590
850
|
delete (this.canvas as any).__originalSetViewportTransform;
|
|
591
851
|
}
|
|
592
852
|
delete (this.canvas as any).__overlayEditor;
|
|
593
853
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
854
|
}
|
|
597
855
|
|
|
598
856
|
/**
|
|
@@ -622,7 +880,7 @@ export function enterTextOverlayEdit(
|
|
|
622
880
|
});
|
|
623
881
|
|
|
624
882
|
// We no longer change fill, so no need to store it
|
|
625
|
-
|
|
883
|
+
|
|
626
884
|
// Store reference on target for cleanup
|
|
627
885
|
(target as any).__overlayEditor = editor;
|
|
628
886
|
|