@nasser-sw/fabric 7.0.0-beta1 → 7.0.1-beta10
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/0 +0 -0
- package/debug/{konva → konva-master}/CHANGELOG.md +2 -1
- package/debug/{konva → konva-master}/README.md +7 -3
- package/debug/{konva → konva-master}/package.json +1 -1
- package/debug/{konva → konva-master}/release.sh +1 -4
- package/debug/{konva → konva-master}/src/Canvas.ts +37 -0
- package/debug/{konva → konva-master}/src/shapes/Text.ts +2 -2
- package/dist/index.js +2198 -272
- 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 +2198 -272
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +2198 -272
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +2198 -272
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/Line.d.ts +33 -86
- package/dist/src/shapes/Line.d.ts.map +1 -1
- package/dist/src/shapes/Line.min.mjs +1 -1
- package/dist/src/shapes/Line.min.mjs.map +1 -1
- package/dist/src/shapes/Line.mjs +405 -159
- package/dist/src/shapes/Line.mjs.map +1 -1
- package/dist/src/shapes/Polyline.d.ts +7 -0
- package/dist/src/shapes/Polyline.d.ts.map +1 -1
- package/dist/src/shapes/Polyline.min.mjs +1 -1
- package/dist/src/shapes/Polyline.min.mjs.map +1 -1
- package/dist/src/shapes/Polyline.mjs +48 -16
- package/dist/src/shapes/Polyline.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts +19 -0
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +302 -16
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +56 -1
- 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 +633 -11
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/shapes/Triangle.d.ts +27 -2
- package/dist/src/shapes/Triangle.d.ts.map +1 -1
- package/dist/src/shapes/Triangle.min.mjs +1 -1
- package/dist/src/shapes/Triangle.min.mjs.map +1 -1
- package/dist/src/shapes/Triangle.mjs +72 -12
- package/dist/src/shapes/Triangle.mjs.map +1 -1
- package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
- package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
- package/dist/src/text/measure.d.ts +9 -0
- package/dist/src/text/measure.d.ts.map +1 -1
- package/dist/src/text/measure.min.mjs +1 -1
- package/dist/src/text/measure.min.mjs.map +1 -1
- package/dist/src/text/measure.mjs +175 -4
- package/dist/src/text/measure.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 +395 -56
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/text/scriptUtils.d.ts +142 -0
- package/dist/src/text/scriptUtils.d.ts.map +1 -0
- package/dist/src/text/scriptUtils.min.mjs +2 -0
- package/dist/src/text/scriptUtils.min.mjs.map +1 -0
- package/dist/src/text/scriptUtils.mjs +212 -0
- package/dist/src/text/scriptUtils.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
- package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.mjs +181 -0
- package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
- package/dist-extensions/src/shapes/Line.d.ts +33 -86
- package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
- package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +56 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
- package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
- package/dist-extensions/src/text/measure.d.ts +9 -0
- package/dist-extensions/src/text/measure.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/dist-extensions/src/text/scriptUtils.d.ts +142 -0
- package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/fabric-test-editor.html +3552 -0
- package/fabric-test2.html +647 -0
- package/fabric.ts +182 -182
- package/fonts/STV Bold.ttf +0 -0
- package/fonts/STV Light.ttf +0 -0
- package/fonts/STV Regular.ttf +0 -0
- package/package.json +164 -164
- package/src/shapes/Line.ts +484 -157
- package/src/shapes/Polyline.ts +70 -29
- package/src/shapes/Text/Text.ts +317 -19
- package/src/shapes/Textbox.ts +663 -12
- package/src/shapes/Triangle.spec.ts +76 -0
- package/src/shapes/Triangle.ts +85 -15
- package/src/text/measure.ts +200 -50
- package/src/text/overlayEditor.ts +504 -94
- package/src/util/misc/cornerRadius.spec.ts +141 -0
- package/src/util/misc/cornerRadius.ts +269 -0
- /package/debug/{konva → konva-master}/LICENSE +0 -0
- /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
- /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
- /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
- /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
- /package/debug/{konva → konva-master}/src/Container.ts +0 -0
- /package/debug/{konva → konva-master}/src/Context.ts +0 -0
- /package/debug/{konva → konva-master}/src/Core.ts +0 -0
- /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
- /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
- /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Global.ts +0 -0
- /package/debug/{konva → konva-master}/src/Group.ts +0 -0
- /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Node.ts +0 -0
- /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
- /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
- /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
- /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
- /package/debug/{konva → konva-master}/src/Util.ts +0 -0
- /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
- /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
- /package/debug/{konva → konva-master}/src/index.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
- /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/types.ts +0 -0
- /package/debug/{konva → konva-master}/tsconfig.json +0 -0
- /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
|
@@ -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,440 @@ 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`;
|
|
337
|
+
|
|
338
|
+
// Apply all other font and text styles to match Fabric
|
|
339
|
+
const letterSpacingPx = ((target.charSpacing || 0) / 1000) * finalFontSize;
|
|
322
340
|
|
|
341
|
+
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
342
|
+
if (target.dirty !== false && target.initDimensions) {
|
|
343
|
+
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
344
|
+
// Force re-initialization if the text object seems to be in a dirty state
|
|
345
|
+
target.initDimensions();
|
|
346
|
+
}
|
|
347
|
+
|
|
323
348
|
this.textarea.style.fontSize = `${finalFontSize}px`;
|
|
324
|
-
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
349
|
+
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
325
350
|
this.textarea.style.fontFamily = target.fontFamily || 'Arial';
|
|
326
351
|
this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
|
|
327
352
|
this.textarea.style.fontStyle = target.fontStyle || 'normal';
|
|
328
|
-
|
|
353
|
+
// Handle text alignment and justification
|
|
354
|
+
const textAlign = (target as any).textAlign || 'left';
|
|
355
|
+
let cssTextAlign = textAlign;
|
|
356
|
+
|
|
357
|
+
// Detect text direction from content for proper justify handling
|
|
358
|
+
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
359
|
+
|
|
360
|
+
// DEBUG: Log alignment details
|
|
361
|
+
console.log('🔍 ALIGNMENT DEBUG:');
|
|
362
|
+
console.log(' Fabric textAlign:', textAlign);
|
|
363
|
+
console.log(' Fabric direction:', (target as any).direction);
|
|
364
|
+
console.log(' Text content:', JSON.stringify(target.text));
|
|
365
|
+
console.log(' Detected direction:', autoDetectedDirection);
|
|
366
|
+
|
|
367
|
+
// Map fabric.js justify to CSS
|
|
368
|
+
if (textAlign.includes('justify')) {
|
|
369
|
+
// Try to match fabric.js justify behavior more precisely
|
|
370
|
+
try {
|
|
371
|
+
// For justify, we need to replicate fabric.js space expansion
|
|
372
|
+
// Use CSS justify but with specific settings to match fabric.js better
|
|
373
|
+
cssTextAlign = 'justify';
|
|
374
|
+
|
|
375
|
+
// Set text-align-last based on justify type and detected direction
|
|
376
|
+
// Smart justify: respect detected direction even when fabric alignment doesn't match
|
|
377
|
+
if (textAlign === 'justify') {
|
|
378
|
+
this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
|
|
379
|
+
} else if (textAlign === 'justify-left') {
|
|
380
|
+
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
381
|
+
if (autoDetectedDirection === 'rtl') {
|
|
382
|
+
this.textarea.style.textAlignLast = 'right';
|
|
383
|
+
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
384
|
+
} else {
|
|
385
|
+
this.textarea.style.textAlignLast = 'left';
|
|
386
|
+
}
|
|
387
|
+
} else if (textAlign === 'justify-right') {
|
|
388
|
+
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
389
|
+
if (autoDetectedDirection === 'ltr') {
|
|
390
|
+
this.textarea.style.textAlignLast = 'left';
|
|
391
|
+
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
392
|
+
} else {
|
|
393
|
+
this.textarea.style.textAlignLast = 'right';
|
|
394
|
+
}
|
|
395
|
+
} else if (textAlign === 'justify-center') {
|
|
396
|
+
this.textarea.style.textAlignLast = 'center';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Enhanced justify settings for better fabric.js matching
|
|
400
|
+
(this.textarea.style as any).textJustify = 'inter-word';
|
|
401
|
+
(this.textarea.style as any).wordSpacing = 'normal';
|
|
402
|
+
|
|
403
|
+
// Additional CSS properties for better justify matching
|
|
404
|
+
this.textarea.style.textAlign = 'justify';
|
|
405
|
+
this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
|
|
406
|
+
|
|
407
|
+
// Try to force better justify behavior
|
|
408
|
+
(this.textarea.style as any).textJustifyTrim = 'none';
|
|
409
|
+
(this.textarea.style as any).textAutospace = 'none';
|
|
410
|
+
|
|
411
|
+
console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
414
|
+
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
this.textarea.style.textAlignLast = 'auto';
|
|
418
|
+
(this.textarea.style as any).textJustify = 'auto';
|
|
419
|
+
(this.textarea.style as any).wordSpacing = 'normal';
|
|
420
|
+
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.textarea.style.textAlign = cssTextAlign;
|
|
329
424
|
this.textarea.style.color = target.fill?.toString() || '#000';
|
|
330
|
-
this.textarea.style.letterSpacing = `${
|
|
331
|
-
|
|
425
|
+
this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
|
|
426
|
+
|
|
427
|
+
// Use the already detected direction from above
|
|
428
|
+
const fabricDirection = (target as any).direction;
|
|
332
429
|
|
|
333
|
-
//
|
|
430
|
+
// Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
|
|
431
|
+
this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
|
|
334
432
|
this.textarea.style.fontVariant = 'normal';
|
|
335
433
|
this.textarea.style.fontStretch = 'normal';
|
|
336
|
-
this.textarea.style.textRendering = 'auto';
|
|
337
|
-
this.textarea.style.fontKerning = '
|
|
338
|
-
this.textarea.style.
|
|
434
|
+
this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
|
|
435
|
+
this.textarea.style.fontKerning = 'normal';
|
|
436
|
+
this.textarea.style.fontFeatureSettings = 'normal';
|
|
437
|
+
this.textarea.style.fontVariationSettings = 'normal';
|
|
339
438
|
this.textarea.style.margin = '0';
|
|
340
439
|
this.textarea.style.border = 'none';
|
|
341
440
|
this.textarea.style.outline = 'none';
|
|
342
441
|
this.textarea.style.background = 'transparent';
|
|
343
|
-
this.textarea.style.
|
|
442
|
+
this.textarea.style.overflowWrap = 'break-word';
|
|
344
443
|
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
444
|
+
this.textarea.style.hyphens = 'none';
|
|
445
|
+
|
|
446
|
+
// DEBUG: Log final CSS properties
|
|
447
|
+
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
448
|
+
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
449
|
+
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
450
|
+
console.log(' direction:', this.textarea.style.direction);
|
|
451
|
+
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
452
|
+
console.log(' width:', this.textarea.style.width);
|
|
453
|
+
console.log(' textJustify:', (this.textarea.style as any).textJustify);
|
|
454
|
+
console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
|
|
455
|
+
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
456
|
+
|
|
457
|
+
// If justify, log Fabric object dimensions for comparison
|
|
458
|
+
if (textAlign.includes('justify')) {
|
|
459
|
+
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
460
|
+
console.log(' Fabric width:', (target as any).width);
|
|
461
|
+
console.log(' Fabric calcTextWidth:', (target as any).calcTextWidth?.());
|
|
462
|
+
console.log(' Fabric textAlign:', (target as any).textAlign);
|
|
463
|
+
console.log(' Text lines:', (target as any).textLines);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Debug font properties matching
|
|
467
|
+
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
468
|
+
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
469
|
+
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
470
|
+
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
471
|
+
console.log(' Fabric fontSize:', target.fontSize);
|
|
472
|
+
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
473
|
+
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
474
|
+
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
475
|
+
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
476
|
+
|
|
477
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
478
|
+
|
|
479
|
+
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
480
|
+
// Default to auto for more natural rendering
|
|
481
|
+
(this.textarea.style as any).webkitFontSmoothing = 'auto';
|
|
482
|
+
(this.textarea.style as any).mozOsxFontSmoothing = 'auto';
|
|
483
|
+
(this.textarea.style as any).fontSmooth = 'auto';
|
|
484
|
+
(this.textarea.style as any).textSizeAdjust = 'none';
|
|
485
|
+
|
|
486
|
+
// For bold fonts, use subpixel rendering to match canvas thickness better
|
|
487
|
+
const fontWeight = String(target.fontWeight || 'normal');
|
|
488
|
+
const isBold = fontWeight === 'bold' || fontWeight === '700' ||
|
|
489
|
+
(parseInt(fontWeight) >= 600);
|
|
490
|
+
|
|
491
|
+
if (isBold) {
|
|
492
|
+
(this.textarea.style as any).webkitFontSmoothing = 'subpixel-antialiased';
|
|
493
|
+
(this.textarea.style as any).mozOsxFontSmoothing = 'unset';
|
|
494
|
+
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
495
|
+
}
|
|
345
496
|
|
|
346
|
-
|
|
347
|
-
console.log('
|
|
348
|
-
console.log('
|
|
349
|
-
|
|
350
|
-
console.log(' textarea padding =', this.textarea.style.padding);
|
|
351
|
-
console.log(' paddingX =', paddingX, 'paddingY =', paddingY);
|
|
352
|
-
console.log(' baseFontSize =', baseFontSize);
|
|
353
|
-
console.log(' scaleX =', scaleX);
|
|
354
|
-
console.log(' zoom =', zoom);
|
|
355
|
-
console.log(' finalFontSize =', finalFontSize);
|
|
356
|
-
console.log(' fabricLineHeight =', fabricLineHeight);
|
|
497
|
+
console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
498
|
+
console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
|
|
499
|
+
console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
|
|
500
|
+
|
|
357
501
|
|
|
358
502
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
503
|
+
}
|
|
359
504
|
|
|
360
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Debug method to compare textarea and canvas object bounding boxes
|
|
507
|
+
*/
|
|
508
|
+
private debugBoundingBoxComparison(): void {
|
|
509
|
+
const target = this.target;
|
|
510
|
+
const canvas = this.canvas;
|
|
511
|
+
const zoom = canvas.getZoom();
|
|
512
|
+
|
|
513
|
+
// Get textarea bounding box (in screen coordinates)
|
|
514
|
+
const textareaRect = this.textarea.getBoundingClientRect();
|
|
515
|
+
const hostRect = this.hostDiv.getBoundingClientRect();
|
|
516
|
+
|
|
517
|
+
// Get canvas object bounding box (in screen coordinates)
|
|
518
|
+
const canvasBounds = target.getBoundingRect();
|
|
519
|
+
const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
|
|
520
|
+
|
|
521
|
+
// Convert canvas object bounds to screen coordinates
|
|
522
|
+
const vpt = canvas.viewportTransform;
|
|
523
|
+
const screenObjectBounds = {
|
|
524
|
+
left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
|
|
525
|
+
top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
|
|
526
|
+
width: canvasBounds.width * zoom,
|
|
527
|
+
height: canvasBounds.height * zoom,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
531
|
+
console.log('📦 Textarea Rect:', {
|
|
532
|
+
left: Math.round(textareaRect.left * 100) / 100,
|
|
533
|
+
top: Math.round(textareaRect.top * 100) / 100,
|
|
534
|
+
width: Math.round(textareaRect.width * 100) / 100,
|
|
535
|
+
height: Math.round(textareaRect.height * 100) / 100,
|
|
536
|
+
});
|
|
537
|
+
console.log('📦 Host Div Rect:', {
|
|
538
|
+
left: Math.round(hostRect.left * 100) / 100,
|
|
539
|
+
top: Math.round(hostRect.top * 100) / 100,
|
|
540
|
+
width: Math.round(hostRect.width * 100) / 100,
|
|
541
|
+
height: Math.round(hostRect.height * 100) / 100,
|
|
542
|
+
});
|
|
543
|
+
console.log('📦 Canvas Object Bounds (screen):', {
|
|
544
|
+
left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
545
|
+
top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
546
|
+
width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
547
|
+
height: Math.round(screenObjectBounds.height * 100) / 100,
|
|
548
|
+
});
|
|
549
|
+
console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
550
|
+
|
|
551
|
+
// Calculate differences
|
|
552
|
+
const hostVsObject = {
|
|
553
|
+
leftDiff:
|
|
554
|
+
Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
555
|
+
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
556
|
+
widthDiff:
|
|
557
|
+
Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
558
|
+
heightDiff:
|
|
559
|
+
Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const textareaVsObject = {
|
|
563
|
+
leftDiff:
|
|
564
|
+
Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
565
|
+
topDiff:
|
|
566
|
+
Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
567
|
+
widthDiff:
|
|
568
|
+
Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
569
|
+
heightDiff:
|
|
570
|
+
Math.round((textareaRect.height - screenObjectBounds.height) * 100) /
|
|
571
|
+
100,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
575
|
+
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
576
|
+
|
|
577
|
+
// Check if they're aligned (within 2px tolerance)
|
|
578
|
+
const tolerance = 2;
|
|
579
|
+
const hostAligned =
|
|
580
|
+
Math.abs(hostVsObject.leftDiff) < tolerance &&
|
|
581
|
+
Math.abs(hostVsObject.topDiff) < tolerance &&
|
|
582
|
+
Math.abs(hostVsObject.widthDiff) < tolerance &&
|
|
583
|
+
Math.abs(hostVsObject.heightDiff) < tolerance;
|
|
584
|
+
|
|
585
|
+
const textareaAligned =
|
|
586
|
+
Math.abs(textareaVsObject.leftDiff) < tolerance &&
|
|
587
|
+
Math.abs(textareaVsObject.topDiff) < tolerance &&
|
|
588
|
+
Math.abs(textareaVsObject.widthDiff) < tolerance &&
|
|
589
|
+
Math.abs(textareaVsObject.heightDiff) < tolerance;
|
|
590
|
+
|
|
591
|
+
console.log(
|
|
592
|
+
hostAligned
|
|
593
|
+
? '✅ Host Div ALIGNED with canvas object'
|
|
594
|
+
: '❌ Host Div MISALIGNED with canvas object',
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
textareaAligned
|
|
598
|
+
? '✅ Textarea ALIGNED with canvas object'
|
|
599
|
+
: '❌ Textarea MISALIGNED with canvas object',
|
|
600
|
+
);
|
|
601
|
+
console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
602
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
361
603
|
}
|
|
362
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
607
|
+
*/
|
|
608
|
+
private debugTextWrapping(): void {
|
|
609
|
+
const target = this.target;
|
|
610
|
+
const text = this.textarea.value;
|
|
611
|
+
|
|
612
|
+
console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
613
|
+
console.log('📄 Text Content:', `"${text}"`);
|
|
614
|
+
console.log('📄 Text Length:', text.length);
|
|
615
|
+
|
|
616
|
+
// Analyze line breaks
|
|
617
|
+
const explicitLines = text.split('\n');
|
|
618
|
+
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
619
|
+
explicitLines.forEach((line, i) => {
|
|
620
|
+
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Get textarea computed styles for wrapping analysis
|
|
624
|
+
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
625
|
+
console.log('📐 Textarea Wrapping Styles:');
|
|
626
|
+
console.log(' width:', textareaStyles.width);
|
|
627
|
+
console.log(' fontSize:', textareaStyles.fontSize);
|
|
628
|
+
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
629
|
+
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
630
|
+
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
631
|
+
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
632
|
+
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
633
|
+
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
634
|
+
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
635
|
+
console.log(' direction:', textareaStyles.direction);
|
|
636
|
+
console.log(' textAlign:', textareaStyles.textAlign);
|
|
637
|
+
|
|
638
|
+
// Get Fabric text object properties for comparison
|
|
639
|
+
console.log('📐 Fabric Text Object Properties:');
|
|
640
|
+
console.log(' width:', (target as any).width);
|
|
641
|
+
console.log(' fontSize:', target.fontSize);
|
|
642
|
+
console.log(' fontFamily:', target.fontFamily);
|
|
643
|
+
console.log(' fontWeight:', target.fontWeight);
|
|
644
|
+
console.log(' charSpacing:', target.charSpacing);
|
|
645
|
+
console.log(' lineHeight:', target.lineHeight);
|
|
646
|
+
console.log(' direction:', (target as any).direction);
|
|
647
|
+
console.log(' textAlign:', (target as any).textAlign);
|
|
648
|
+
console.log(' scaleX:', target.scaleX);
|
|
649
|
+
console.log(' scaleY:', target.scaleY);
|
|
650
|
+
|
|
651
|
+
// Calculate effective dimensions for comparison - use actual rendered width
|
|
652
|
+
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
653
|
+
const fabricEffectiveWidth = this.target.getBoundingRect().width;
|
|
654
|
+
// Use the exact width set on textarea for comparison
|
|
655
|
+
const textareaComputedWidth = parseFloat(
|
|
656
|
+
window.getComputedStyle(this.textarea).width,
|
|
657
|
+
);
|
|
658
|
+
const textareaEffectiveWidth =
|
|
659
|
+
textareaComputedWidth / this.canvas.getZoom();
|
|
660
|
+
const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
|
|
661
|
+
|
|
662
|
+
console.log('📏 Effective Width Comparison:');
|
|
663
|
+
console.log(' Textarea Effective Width:', textareaEffectiveWidth);
|
|
664
|
+
console.log(' Fabric Effective Width:', fabricEffectiveWidth);
|
|
665
|
+
console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
|
|
666
|
+
console.log(
|
|
667
|
+
widthDiff < 1
|
|
668
|
+
? '✅ Widths MATCH for wrapping'
|
|
669
|
+
: '❌ Width MISMATCH may cause different wrapping',
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Check text direction and bidi handling
|
|
673
|
+
const hasRTLText =
|
|
674
|
+
/[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(
|
|
675
|
+
text,
|
|
676
|
+
);
|
|
677
|
+
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
678
|
+
|
|
679
|
+
console.log('🌍 Text Direction Analysis:');
|
|
680
|
+
console.log(' Has RTL characters:', hasRTLText);
|
|
681
|
+
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
682
|
+
console.log(' Textarea direction:', textareaStyles.direction);
|
|
683
|
+
console.log(' Fabric direction:', (target as any).direction || 'auto');
|
|
684
|
+
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
685
|
+
|
|
686
|
+
// Measure actual rendered line count
|
|
687
|
+
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
688
|
+
const textareaLineHeight =
|
|
689
|
+
parseFloat(textareaStyles.lineHeight) ||
|
|
690
|
+
parseFloat(textareaStyles.fontSize) * 1.2;
|
|
691
|
+
const estimatedTextareaLines = Math.round(
|
|
692
|
+
textareaScrollHeight / textareaLineHeight,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
console.log('📊 Line Count Analysis:');
|
|
696
|
+
console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
697
|
+
console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
698
|
+
console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
699
|
+
console.log(' Explicit line breaks:', explicitLines.length);
|
|
700
|
+
|
|
701
|
+
if (estimatedTextareaLines > explicitLines.length) {
|
|
702
|
+
console.log('🔄 Text wrapping detected in textarea');
|
|
703
|
+
console.log(
|
|
704
|
+
' Wrapped lines:',
|
|
705
|
+
estimatedTextareaLines - explicitLines.length,
|
|
706
|
+
);
|
|
707
|
+
} else {
|
|
708
|
+
console.log('📏 No text wrapping in textarea');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
|
|
363
715
|
/**
|
|
364
716
|
* Focus the textarea and position cursor at end
|
|
365
717
|
*/
|
|
366
718
|
private focusTextarea(): void {
|
|
367
|
-
|
|
368
|
-
|
|
369
719
|
// For overlay editing, we want to keep the object in "selection mode" not "editing mode"
|
|
370
720
|
// This means keeping selected=true and isEditing=false to show boundaries
|
|
371
|
-
|
|
721
|
+
|
|
372
722
|
// Hide the text content only (not the entire object)
|
|
373
723
|
this.target.opacity = 0.01; // Nearly transparent but not fully hidden
|
|
374
|
-
|
|
724
|
+
|
|
375
725
|
// Ensure object stays selected to show boundaries
|
|
376
726
|
(this.target as any).selected = true;
|
|
377
727
|
(this.target as any).isEditing = false; // Override any editing state
|
|
378
|
-
|
|
728
|
+
|
|
379
729
|
// Make sure controls are enabled and movement is allowed during overlay editing
|
|
380
730
|
this.target.set({
|
|
381
731
|
hasControls: true,
|
|
382
732
|
hasBorders: true,
|
|
383
733
|
selectable: true,
|
|
384
734
|
lockMovementX: false,
|
|
385
|
-
lockMovementY: false
|
|
735
|
+
lockMovementY: false,
|
|
386
736
|
});
|
|
387
|
-
|
|
737
|
+
|
|
388
738
|
// Keep as active object
|
|
389
739
|
this.canvas.setActiveObject(this.target);
|
|
390
|
-
|
|
740
|
+
|
|
391
741
|
this.canvas.requestRenderAll();
|
|
392
742
|
this.target.setCoords();
|
|
393
743
|
this.applyOverlayStyle();
|
|
394
744
|
|
|
395
|
-
|
|
745
|
+
// Fix character mapping issues after JSON loading for browser-wrapped fonts
|
|
746
|
+
if ((this.target as any)._fixCharacterMappingAfterJsonLoad) {
|
|
747
|
+
(this.target as any)._fixCharacterMappingAfterJsonLoad();
|
|
748
|
+
}
|
|
396
749
|
|
|
397
750
|
this.textarea.focus();
|
|
751
|
+
|
|
398
752
|
this.textarea.setSelectionRange(
|
|
399
753
|
this.textarea.value.length,
|
|
400
754
|
this.textarea.value.length,
|
|
401
755
|
);
|
|
402
|
-
|
|
756
|
+
|
|
403
757
|
// Ensure the object stays selected even after textarea focus
|
|
404
758
|
this.canvas.setActiveObject(this.target);
|
|
405
759
|
this.canvas.requestRenderAll();
|
|
@@ -442,6 +796,26 @@ export class OverlayEditor {
|
|
|
442
796
|
// Handle commit/cancel after restoring visibility
|
|
443
797
|
if (commit && !this.isComposing) {
|
|
444
798
|
const finalText = this.textarea.value;
|
|
799
|
+
|
|
800
|
+
// Auto-detect text direction and update fabric object if needed
|
|
801
|
+
const detectedDirection = this.firstStrongDir(finalText);
|
|
802
|
+
const currentDirection = (this.target as any).direction || 'ltr';
|
|
803
|
+
|
|
804
|
+
if (detectedDirection && detectedDirection !== currentDirection) {
|
|
805
|
+
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
806
|
+
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
807
|
+
|
|
808
|
+
// Update the fabric object's direction
|
|
809
|
+
(this.target as any).set('direction', detectedDirection);
|
|
810
|
+
|
|
811
|
+
// Force a re-render to apply the direction change
|
|
812
|
+
this.canvas.requestRenderAll();
|
|
813
|
+
|
|
814
|
+
console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
815
|
+
} else {
|
|
816
|
+
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
817
|
+
}
|
|
818
|
+
|
|
445
819
|
if (this.onCommit) {
|
|
446
820
|
this.onCommit(finalText);
|
|
447
821
|
}
|
|
@@ -470,6 +844,7 @@ export class OverlayEditor {
|
|
|
470
844
|
// Live update target text
|
|
471
845
|
this.target.text = this.textarea.value;
|
|
472
846
|
|
|
847
|
+
|
|
473
848
|
// Auto-resize textarea to match new content
|
|
474
849
|
this.autoResizeTextarea();
|
|
475
850
|
|
|
@@ -482,25 +857,40 @@ export class OverlayEditor {
|
|
|
482
857
|
}
|
|
483
858
|
|
|
484
859
|
private autoResizeTextarea(): void {
|
|
485
|
-
//
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
860
|
+
// Store the scroll position and the container's old height for comparison.
|
|
861
|
+
const scrollTop = this.textarea.scrollTop;
|
|
862
|
+
const oldHeight = parseFloat(this.hostDiv.style.height || '0');
|
|
863
|
+
|
|
864
|
+
// 1. **Force a reliable reflow.**
|
|
865
|
+
// First, reset the textarea's height to a minimal value. This is the crucial step
|
|
866
|
+
// that forces the browser to recalculate the content's height from scratch,
|
|
867
|
+
// ignoring the hostDiv's larger, stale height.
|
|
868
|
+
this.textarea.style.height = '1px';
|
|
869
|
+
|
|
870
|
+
// 2. Read the now-accurate scrollHeight. This value reflects the minimum
|
|
871
|
+
// height required for the content, whether it's single or multi-line.
|
|
490
872
|
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
|
|
873
|
+
|
|
874
|
+
// A small buffer for rendering consistency across browsers.
|
|
875
|
+
const buffer = 2;
|
|
876
|
+
const newHeight = scrollHeight + buffer;
|
|
877
|
+
|
|
878
|
+
// Check if the height has changed significantly.
|
|
879
|
+
const heightChanged = Math.abs(newHeight - oldHeight) > 1;
|
|
880
|
+
|
|
881
|
+
// 4. Only update heights and object bounds if there was a change.
|
|
501
882
|
if (heightChanged) {
|
|
883
|
+
this.textarea.style.height = `${newHeight}px`;
|
|
884
|
+
this.hostDiv.style.height = `${newHeight}px`;
|
|
502
885
|
this.updateObjectBounds();
|
|
886
|
+
} else {
|
|
887
|
+
// If no significant change, ensure the textarea's height matches the container
|
|
888
|
+
// to prevent any minor visual misalignment.
|
|
889
|
+
this.textarea.style.height = this.hostDiv.style.height;
|
|
503
890
|
}
|
|
891
|
+
|
|
892
|
+
// 5. Restore the original scroll position.
|
|
893
|
+
this.textarea.scrollTop = scrollTop;
|
|
504
894
|
}
|
|
505
895
|
|
|
506
896
|
private handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -510,6 +900,23 @@ export class OverlayEditor {
|
|
|
510
900
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
511
901
|
e.preventDefault();
|
|
512
902
|
this.destroy(true); // Commit
|
|
903
|
+
} else if (
|
|
904
|
+
e.key === 'Enter' ||
|
|
905
|
+
e.key === 'Backspace' ||
|
|
906
|
+
e.key === 'Delete'
|
|
907
|
+
) {
|
|
908
|
+
// For keys that might change the height, schedule a resize check
|
|
909
|
+
// Use both immediate and delayed checks to catch all scenarios
|
|
910
|
+
requestAnimationFrame(() => {
|
|
911
|
+
if (!this.isDestroyed) {
|
|
912
|
+
this.autoResizeTextarea();
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
setTimeout(() => {
|
|
916
|
+
if (!this.isDestroyed) {
|
|
917
|
+
this.autoResizeTextarea();
|
|
918
|
+
}
|
|
919
|
+
}, 10); // Small delay to ensure DOM is updated
|
|
513
920
|
}
|
|
514
921
|
}
|
|
515
922
|
|
|
@@ -553,9 +960,10 @@ export class OverlayEditor {
|
|
|
553
960
|
private setupViewportChangeDetection(): void {
|
|
554
961
|
// Store original methods
|
|
555
962
|
(this.canvas as any).__originalSetZoom = this.canvas.setZoom;
|
|
556
|
-
(this.canvas as any).__originalSetViewportTransform =
|
|
963
|
+
(this.canvas as any).__originalSetViewportTransform =
|
|
964
|
+
this.canvas.setViewportTransform;
|
|
557
965
|
(this.canvas as any).__overlayEditor = this;
|
|
558
|
-
|
|
966
|
+
|
|
559
967
|
// Override setZoom to detect zoom changes
|
|
560
968
|
const originalSetZoom = this.canvas.setZoom.bind(this.canvas);
|
|
561
969
|
this.canvas.setZoom = (value: number) => {
|
|
@@ -565,9 +973,11 @@ export class OverlayEditor {
|
|
|
565
973
|
}
|
|
566
974
|
return result;
|
|
567
975
|
};
|
|
568
|
-
|
|
976
|
+
|
|
569
977
|
// Override setViewportTransform to detect pan changes
|
|
570
|
-
const originalSetViewportTransform = this.canvas.setViewportTransform.bind(
|
|
978
|
+
const originalSetViewportTransform = this.canvas.setViewportTransform.bind(
|
|
979
|
+
this.canvas,
|
|
980
|
+
);
|
|
571
981
|
this.canvas.setViewportTransform = (vpt: TMat2D) => {
|
|
572
982
|
const result = originalSetViewportTransform(vpt);
|
|
573
983
|
if ((this.canvas as any).__overlayEditor && !this.isDestroyed) {
|
|
@@ -576,7 +986,7 @@ export class OverlayEditor {
|
|
|
576
986
|
return result;
|
|
577
987
|
};
|
|
578
988
|
}
|
|
579
|
-
|
|
989
|
+
|
|
580
990
|
/**
|
|
581
991
|
* Restore original viewport methods
|
|
582
992
|
*/
|
|
@@ -586,13 +996,13 @@ export class OverlayEditor {
|
|
|
586
996
|
delete (this.canvas as any).__originalSetZoom;
|
|
587
997
|
}
|
|
588
998
|
if ((this.canvas as any).__originalSetViewportTransform) {
|
|
589
|
-
this.canvas.setViewportTransform = (
|
|
999
|
+
this.canvas.setViewportTransform = (
|
|
1000
|
+
this.canvas as any
|
|
1001
|
+
).__originalSetViewportTransform;
|
|
590
1002
|
delete (this.canvas as any).__originalSetViewportTransform;
|
|
591
1003
|
}
|
|
592
1004
|
delete (this.canvas as any).__overlayEditor;
|
|
593
1005
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
1006
|
}
|
|
597
1007
|
|
|
598
1008
|
/**
|
|
@@ -622,7 +1032,7 @@ export function enterTextOverlayEdit(
|
|
|
622
1032
|
});
|
|
623
1033
|
|
|
624
1034
|
// We no longer change fill, so no need to store it
|
|
625
|
-
|
|
1035
|
+
|
|
626
1036
|
// Store reference on target for cleanup
|
|
627
1037
|
(target as any).__overlayEditor = editor;
|
|
628
1038
|
|