@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
package/src/shapes/Textbox.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { TClassProperties, TOptions } from '../typedefs';
|
|
|
2
2
|
import { IText } from './IText/IText';
|
|
3
3
|
import { classRegistry } from '../ClassRegistry';
|
|
4
4
|
import { createTextboxDefaultControls } from '../controls/commonControls';
|
|
5
|
-
import { JUSTIFY } from './Text/constants';
|
|
5
|
+
import { JUSTIFY, JUSTIFY_CENTER } from './Text/constants';
|
|
6
6
|
import type { TextStyleDeclaration } from './Text/StyledText';
|
|
7
7
|
import type { SerializedITextProps, ITextProps } from './IText/IText';
|
|
8
8
|
import type { ITextEvents } from './IText/ITextBehavior';
|
|
9
9
|
import type { TextLinesInfo } from './Text/Text';
|
|
10
10
|
import type { Control } from '../controls/Control';
|
|
11
|
+
import { fontLacksEnglishGlyphsCached } from '../text/measure';
|
|
11
12
|
import { layoutText } from '../text/layout';
|
|
12
13
|
|
|
13
14
|
// @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
|
|
@@ -108,6 +109,7 @@ export class Textbox<
|
|
|
108
109
|
*/
|
|
109
110
|
constructor(text: string, options?: Props) {
|
|
110
111
|
super(text, { ...Textbox.ownDefaults, ...options } as Props);
|
|
112
|
+
this.initializeEventListeners();
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -127,8 +129,28 @@ export class Textbox<
|
|
|
127
129
|
*/
|
|
128
130
|
initDimensions() {
|
|
129
131
|
if (!this.initialized) {
|
|
132
|
+
this.initialized = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Prevent rapid recalculations during moves
|
|
136
|
+
if ((this as any)._usingBrowserWrapping) {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
const lastCall = (this as any)._lastInitDimensionsTime || 0;
|
|
139
|
+
const isRapidCall = now - lastCall < 100;
|
|
140
|
+
const isDuringLoading = (this as any)._jsonLoading || !(this as any)._browserWrapInitialized;
|
|
141
|
+
|
|
142
|
+
if (isRapidCall && !isDuringLoading) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
(this as any)._lastInitDimensionsTime = now;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Skip if nothing changed
|
|
149
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
150
|
+
if ((this as any)._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
130
151
|
return;
|
|
131
152
|
}
|
|
153
|
+
(this as any)._lastDimensionState = currentState;
|
|
132
154
|
|
|
133
155
|
// Use advanced layout if enabled
|
|
134
156
|
if (this.enableAdvancedLayout) {
|
|
@@ -140,17 +162,142 @@ export class Textbox<
|
|
|
140
162
|
// clear dynamicMinWidth as it will be different after we re-wrap line
|
|
141
163
|
this.dynamicMinWidth = 0;
|
|
142
164
|
// wrap lines
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
const splitTextResult = this._splitText();
|
|
166
|
+
this._styleMap = this._generateStyleMap(splitTextResult);
|
|
167
|
+
|
|
168
|
+
// For browser wrapping, ensure _textLines is set from browser results
|
|
169
|
+
if ((this as any)._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
|
|
170
|
+
this._textLines = splitTextResult.lines.map(line => line.split(''));
|
|
171
|
+
|
|
172
|
+
// Store justify measurements and browser height
|
|
173
|
+
const justifyMeasurements = (splitTextResult as any).justifySpaceMeasurements;
|
|
174
|
+
if (justifyMeasurements) {
|
|
175
|
+
(this._styleMap as any).justifySpaceMeasurements = justifyMeasurements;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const actualHeight = (splitTextResult as any).actualBrowserHeight;
|
|
179
|
+
if (actualHeight) {
|
|
180
|
+
(this as any)._actualBrowserHeight = actualHeight;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Don't auto-resize width when using browser wrapping to prevent width increases during moves
|
|
184
|
+
if (!((this as any)._usingBrowserWrapping) && this.dynamicMinWidth > this.width) {
|
|
146
185
|
this._set('width', this.dynamicMinWidth);
|
|
147
186
|
}
|
|
187
|
+
|
|
188
|
+
// For browser wrapping fonts (like STV), ensure minimum width for new textboxes
|
|
189
|
+
// since these fonts can't measure English characters properly
|
|
190
|
+
if ((this as any)._usingBrowserWrapping && this.width < 50) {
|
|
191
|
+
console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
|
|
192
|
+
this.width = 300;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Mark browser wrapping as initialized when complete
|
|
196
|
+
if ((this as any)._usingBrowserWrapping) {
|
|
197
|
+
(this as any)._browserWrapInitialized = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
148
200
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
201
|
+
// For browser wrapping fonts, apply browser-calculated justify spaces
|
|
202
|
+
if ((this as any)._usingBrowserWrapping) {
|
|
203
|
+
console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
|
|
204
|
+
this._applyBrowserJustifySpaces();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Don't apply justify alignment during drag operations to prevent snapping
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const lastDragTime = (this as any)._lastInitDimensionsTime || 0;
|
|
211
|
+
const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
|
|
212
|
+
|
|
213
|
+
if (isDuringDrag) {
|
|
214
|
+
console.log('🔤 Skipping justify during drag operation to prevent snapping');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// For non-browser-wrapping fonts, use Fabric's justify system
|
|
149
219
|
// once text is measured we need to make space fatter to make justified text.
|
|
150
|
-
|
|
220
|
+
// Ensure __charBounds exists and fonts are ready before applying justify
|
|
221
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
222
|
+
// Check if font is ready for accurate justify calculations
|
|
223
|
+
const fontReady = this._isFontReady ? this._isFontReady() : true;
|
|
224
|
+
if (fontReady) {
|
|
225
|
+
this.enlargeSpaces();
|
|
226
|
+
} else {
|
|
227
|
+
console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
|
|
228
|
+
// Defer justify calculation until font is ready
|
|
229
|
+
this._scheduleJustifyAfterFontLoad();
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
233
|
+
// Defer the justify calculation until the next frame
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
236
|
+
console.log('🔧 Applying deferred Textbox justify alignment');
|
|
237
|
+
this.enlargeSpaces();
|
|
238
|
+
this.canvas?.requestRenderAll();
|
|
239
|
+
}
|
|
240
|
+
}, 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Calculate height - use Fabric's calculation for proper text rendering space
|
|
244
|
+
if ((this as any)._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
|
|
245
|
+
const actualBrowserHeight = (this as any)._actualBrowserHeight;
|
|
246
|
+
const oldHeight = this.height;
|
|
247
|
+
// Use Fabric's height calculation since it knows how much space text rendering needs
|
|
248
|
+
this.height = this.calcTextHeight();
|
|
249
|
+
|
|
250
|
+
// Force canvas refresh and control update if height changed significantly
|
|
251
|
+
if (Math.abs(this.height - oldHeight) > 1) {
|
|
252
|
+
this.setCoords();
|
|
253
|
+
this.canvas?.requestRenderAll();
|
|
254
|
+
|
|
255
|
+
// DEBUG: Log exact positioning details
|
|
256
|
+
console.log(`🎯 POSITIONING DEBUG:`);
|
|
257
|
+
console.log(` Textbox height: ${this.height}px`);
|
|
258
|
+
console.log(` Textbox top: ${this.top}px`);
|
|
259
|
+
console.log(` Textbox left: ${this.left}px`);
|
|
260
|
+
console.log(` Text lines: ${this._textLines?.length || 0}`);
|
|
261
|
+
console.log(` Font size: ${this.fontSize}px`);
|
|
262
|
+
console.log(` Line height: ${this.lineHeight || 1.16}`);
|
|
263
|
+
console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
|
|
264
|
+
console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
|
|
265
|
+
console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
|
|
266
|
+
console.log(` Browser height: ${actualBrowserHeight}px`);
|
|
267
|
+
console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
this.height = this.calcTextHeight();
|
|
151
271
|
}
|
|
152
|
-
|
|
153
|
-
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Schedule justify calculation after font loads (Textbox-specific)
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_scheduleJustifyAfterFontLoad(): void {
|
|
279
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Only schedule if not already waiting
|
|
284
|
+
if ((this as any)._fontJustifyScheduled) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
(this as any)._fontJustifyScheduled = true;
|
|
288
|
+
|
|
289
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
290
|
+
document.fonts.load(fontSpec).then(() => {
|
|
291
|
+
(this as any)._fontJustifyScheduled = false;
|
|
292
|
+
console.log('🔧 Textbox: Font loaded, applying justify alignment');
|
|
293
|
+
|
|
294
|
+
// Re-run initDimensions to ensure proper justify calculation
|
|
295
|
+
this.initDimensions();
|
|
296
|
+
this.canvas?.requestRenderAll();
|
|
297
|
+
}).catch(() => {
|
|
298
|
+
(this as any)._fontJustifyScheduled = false;
|
|
299
|
+
console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
|
|
300
|
+
});
|
|
154
301
|
}
|
|
155
302
|
|
|
156
303
|
/**
|
|
@@ -545,20 +692,35 @@ export class Textbox<
|
|
|
545
692
|
const { word, width: wordWidth } = data[i];
|
|
546
693
|
offset += word.length;
|
|
547
694
|
|
|
548
|
-
|
|
549
|
-
|
|
695
|
+
// Predictive wrapping: check if adding this word would exceed the width
|
|
696
|
+
const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
|
|
697
|
+
// Use exact width to match overlay editor behavior
|
|
698
|
+
const conservativeMaxWidth = maxWidth; // No artificial buffer
|
|
699
|
+
|
|
700
|
+
// Debug logging for wrapping decisions
|
|
701
|
+
const currentLineText = line.join('');
|
|
702
|
+
console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
|
|
703
|
+
|
|
704
|
+
if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
|
|
705
|
+
// This word would exceed the width, wrap before adding it
|
|
706
|
+
console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
|
|
550
707
|
graphemeLines.push(line);
|
|
551
708
|
line = [];
|
|
552
|
-
lineWidth = wordWidth;
|
|
709
|
+
lineWidth = wordWidth; // Start new line with just this word
|
|
553
710
|
lineJustStarted = true;
|
|
554
711
|
} else {
|
|
555
|
-
|
|
712
|
+
// Word fits, add it to current line
|
|
713
|
+
lineWidth = potentialLineWidth + additionalSpace;
|
|
556
714
|
}
|
|
557
715
|
|
|
558
716
|
if (!lineJustStarted && !splitByGrapheme) {
|
|
559
717
|
line.push(infix);
|
|
560
718
|
}
|
|
561
719
|
line = line.concat(word);
|
|
720
|
+
|
|
721
|
+
// Debug: show current line after adding word
|
|
722
|
+
console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
|
|
723
|
+
|
|
562
724
|
|
|
563
725
|
infixWidth = splitByGrapheme
|
|
564
726
|
? 0
|
|
@@ -572,9 +734,20 @@ export class Textbox<
|
|
|
572
734
|
// TODO: this code is probably not necessary anymore.
|
|
573
735
|
// it can be moved out of this function since largestWordWidth is now
|
|
574
736
|
// known in advance
|
|
575
|
-
|
|
737
|
+
// Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
|
|
738
|
+
if (!((this as any)._usingBrowserWrapping) && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
|
|
739
|
+
console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
|
|
576
740
|
this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
|
|
741
|
+
} else if ((this as any)._usingBrowserWrapping) {
|
|
742
|
+
console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
|
|
577
743
|
}
|
|
744
|
+
|
|
745
|
+
// Debug: show final wrapped lines
|
|
746
|
+
console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
|
|
747
|
+
graphemeLines.forEach((line, i) => {
|
|
748
|
+
console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
|
|
749
|
+
});
|
|
750
|
+
|
|
578
751
|
return graphemeLines;
|
|
579
752
|
}
|
|
580
753
|
|
|
@@ -618,6 +791,284 @@ export class Textbox<
|
|
|
618
791
|
* @override
|
|
619
792
|
*/
|
|
620
793
|
_splitTextIntoLines(text: string) {
|
|
794
|
+
// Check if we need browser wrapping using smart font detection
|
|
795
|
+
const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
796
|
+
|
|
797
|
+
if (needsBrowserWrapping) {
|
|
798
|
+
// Cache key based on text content, width, font properties, AND text alignment
|
|
799
|
+
const textHash = text.length + text.slice(0, 50); // Include text content in cache key
|
|
800
|
+
const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
801
|
+
|
|
802
|
+
// Check if we have a cached result and nothing has changed
|
|
803
|
+
if ((this as any)._browserWrapCache && (this as any)._browserWrapCache.key === cacheKey) {
|
|
804
|
+
const cachedResult = (this as any)._browserWrapCache.result;
|
|
805
|
+
|
|
806
|
+
// For justify alignment, ensure we have the measurements
|
|
807
|
+
if (this.textAlign.includes('justify') && !(cachedResult as any).justifySpaceMeasurements) {
|
|
808
|
+
// Fall through to recalculate
|
|
809
|
+
} else {
|
|
810
|
+
return cachedResult;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const result = this._splitTextIntoLinesWithBrowser(text);
|
|
815
|
+
|
|
816
|
+
// Cache the result
|
|
817
|
+
(this as any)._browserWrapCache = { key: cacheKey, result };
|
|
818
|
+
|
|
819
|
+
// Mark that we used browser wrapping to prevent dynamicMinWidth modifications
|
|
820
|
+
(this as any)._usingBrowserWrapping = true;
|
|
821
|
+
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Clear the browser wrapping flag when using regular wrapping
|
|
826
|
+
(this as any)._usingBrowserWrapping = false;
|
|
827
|
+
|
|
828
|
+
// Default Fabric wrapping for other fonts
|
|
829
|
+
const newText = super._splitTextIntoLines(text),
|
|
830
|
+
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
831
|
+
lines = new Array(graphemeLines.length);
|
|
832
|
+
for (let i = 0; i < graphemeLines.length; i++) {
|
|
833
|
+
lines[i] = graphemeLines[i].join('');
|
|
834
|
+
}
|
|
835
|
+
newText.lines = lines;
|
|
836
|
+
newText.graphemeLines = graphemeLines;
|
|
837
|
+
return newText;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Use browser's native text wrapping for accurate handling of fonts without English glyphs
|
|
842
|
+
* @private
|
|
843
|
+
*/
|
|
844
|
+
_splitTextIntoLinesWithBrowser(text: string) {
|
|
845
|
+
if (typeof document === 'undefined') {
|
|
846
|
+
// Fallback to regular wrapping in Node.js
|
|
847
|
+
return this._splitTextIntoLinesDefault(text);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Create a hidden element that mimics the overlay editor
|
|
851
|
+
const testElement = document.createElement('div');
|
|
852
|
+
testElement.style.position = 'absolute';
|
|
853
|
+
testElement.style.left = '-9999px';
|
|
854
|
+
testElement.style.visibility = 'hidden';
|
|
855
|
+
testElement.style.fontSize = `${this.fontSize}px`;
|
|
856
|
+
testElement.style.fontFamily = `"${this.fontFamily}"`;
|
|
857
|
+
testElement.style.fontWeight = String(this.fontWeight || 'normal');
|
|
858
|
+
testElement.style.fontStyle = String(this.fontStyle || 'normal');
|
|
859
|
+
testElement.style.lineHeight = String(this.lineHeight || 1.16);
|
|
860
|
+
|
|
861
|
+
testElement.style.width = `${this.width}px`;
|
|
862
|
+
|
|
863
|
+
testElement.style.direction = this.direction || 'ltr';
|
|
864
|
+
testElement.style.whiteSpace = 'pre-wrap';
|
|
865
|
+
testElement.style.wordBreak = 'normal';
|
|
866
|
+
testElement.style.overflowWrap = 'break-word';
|
|
867
|
+
|
|
868
|
+
// Set browser-native text alignment (including justify)
|
|
869
|
+
if (this.textAlign.includes('justify')) {
|
|
870
|
+
testElement.style.textAlign = 'justify';
|
|
871
|
+
testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
|
|
872
|
+
} else {
|
|
873
|
+
testElement.style.textAlign = this.textAlign;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
testElement.textContent = text;
|
|
877
|
+
|
|
878
|
+
document.body.appendChild(testElement);
|
|
879
|
+
|
|
880
|
+
// Get the browser's natural line breaks
|
|
881
|
+
const range = document.createRange();
|
|
882
|
+
const lines: string[] = [];
|
|
883
|
+
const graphemeLines: string[][] = [];
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
// Simple approach: split by measuring character positions
|
|
887
|
+
const textNode = testElement.firstChild;
|
|
888
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
889
|
+
let currentLineStart = 0;
|
|
890
|
+
const textLength = text.length;
|
|
891
|
+
let previousBottom = 0;
|
|
892
|
+
|
|
893
|
+
for (let i = 0; i <= textLength; i++) {
|
|
894
|
+
range.setStart(textNode, currentLineStart);
|
|
895
|
+
range.setEnd(textNode, i);
|
|
896
|
+
const rect = range.getBoundingClientRect();
|
|
897
|
+
|
|
898
|
+
if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
|
|
899
|
+
// New line detected or end of text
|
|
900
|
+
const lineEnd = i === textLength ? i : i - 1;
|
|
901
|
+
const lineText = text.substring(currentLineStart, lineEnd).trim();
|
|
902
|
+
if (lineText) {
|
|
903
|
+
lines.push(lineText);
|
|
904
|
+
// Convert to graphemes for compatibility
|
|
905
|
+
const graphemeLine = lineText.split('');
|
|
906
|
+
graphemeLines.push(graphemeLine);
|
|
907
|
+
}
|
|
908
|
+
currentLineStart = lineEnd;
|
|
909
|
+
previousBottom = rect.bottom;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} catch (error) {
|
|
914
|
+
console.warn('Browser wrapping failed, using fallback:', error);
|
|
915
|
+
document.body.removeChild(testElement);
|
|
916
|
+
return this._splitTextIntoLinesDefault(text);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Extract actual browser height BEFORE removing element
|
|
920
|
+
const actualBrowserHeight = testElement.scrollHeight;
|
|
921
|
+
const offsetHeight = testElement.offsetHeight;
|
|
922
|
+
const clientHeight = testElement.clientHeight;
|
|
923
|
+
const boundingRect = testElement.getBoundingClientRect();
|
|
924
|
+
|
|
925
|
+
console.log(`🔤 Browser element measurements:`);
|
|
926
|
+
console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
|
|
927
|
+
console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
|
|
928
|
+
console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
|
|
929
|
+
console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
|
|
930
|
+
console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
|
|
931
|
+
|
|
932
|
+
// For justify alignment, extract space measurements from browser BEFORE removing element
|
|
933
|
+
let justifySpaceMeasurements = null;
|
|
934
|
+
if (this.textAlign.includes('justify')) {
|
|
935
|
+
justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
document.body.removeChild(testElement);
|
|
939
|
+
|
|
940
|
+
console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
|
|
941
|
+
|
|
942
|
+
// Try different height measurements to find the most accurate
|
|
943
|
+
let bestHeight = actualBrowserHeight;
|
|
944
|
+
|
|
945
|
+
// If scrollHeight and offsetHeight differ significantly, investigate
|
|
946
|
+
if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
|
|
947
|
+
console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
|
|
951
|
+
if (boundingRect.height > bestHeight) {
|
|
952
|
+
console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
|
|
953
|
+
bestHeight = boundingRect.height;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Font-specific height adjustments for accurate bounding box
|
|
957
|
+
let adjustedHeight = bestHeight;
|
|
958
|
+
|
|
959
|
+
// Fonts without English glyphs need additional height buffer due to different font metrics
|
|
960
|
+
const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
961
|
+
if (lacksEnglishGlyphs) {
|
|
962
|
+
const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
|
|
963
|
+
adjustedHeight = bestHeight + glyphBuffer;
|
|
964
|
+
console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
|
|
965
|
+
} else {
|
|
966
|
+
console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
_unwrappedLines: [text.split('')],
|
|
971
|
+
lines: lines,
|
|
972
|
+
graphemeText: text.split(''),
|
|
973
|
+
graphemeLines: graphemeLines,
|
|
974
|
+
justifySpaceMeasurements: justifySpaceMeasurements,
|
|
975
|
+
actualBrowserHeight: adjustedHeight,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Extract justify space measurements from browser
|
|
984
|
+
* @private
|
|
985
|
+
*/
|
|
986
|
+
_extractJustifySpaceMeasurements(element: HTMLElement, lines: string[]) {
|
|
987
|
+
console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
|
|
988
|
+
|
|
989
|
+
// For now, we'll use a simplified approach:
|
|
990
|
+
// Apply uniform space expansion to match the line width
|
|
991
|
+
const spaceWidths: number[][] = [];
|
|
992
|
+
|
|
993
|
+
lines.forEach((line, lineIndex) => {
|
|
994
|
+
const lineSpaces: number[] = [];
|
|
995
|
+
const spaceCount = (line.match(/\s/g) || []).length;
|
|
996
|
+
|
|
997
|
+
if (spaceCount > 0 && lineIndex < lines.length - 1) { // Don't justify last line
|
|
998
|
+
// Calculate how much space expansion is needed
|
|
999
|
+
const normalSpaceWidth = 6.4; // Default space width for STV font
|
|
1000
|
+
const lineWidth = this.width;
|
|
1001
|
+
|
|
1002
|
+
// Estimate natural line width
|
|
1003
|
+
const charCount = line.length - spaceCount;
|
|
1004
|
+
const avgCharWidth = 12; // Approximate for STV font
|
|
1005
|
+
const naturalWidth = charCount * avgCharWidth + spaceCount * normalSpaceWidth;
|
|
1006
|
+
|
|
1007
|
+
// Calculate expanded space width
|
|
1008
|
+
const remainingSpace = lineWidth - (charCount * avgCharWidth);
|
|
1009
|
+
const expandedSpaceWidth = remainingSpace / spaceCount;
|
|
1010
|
+
|
|
1011
|
+
console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
|
|
1012
|
+
|
|
1013
|
+
// Fill array with expanded space widths for this line
|
|
1014
|
+
for (let i = 0; i < spaceCount; i++) {
|
|
1015
|
+
lineSpaces.push(expandedSpaceWidth);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
spaceWidths.push(lineSpaces);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
return spaceWidths;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Apply browser-calculated justify space measurements
|
|
1027
|
+
* @private
|
|
1028
|
+
*/
|
|
1029
|
+
_applyBrowserJustifySpaces() {
|
|
1030
|
+
if (!this._textLines || !this.__charBounds) {
|
|
1031
|
+
console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Get space measurements from browser wrapping result
|
|
1036
|
+
const styleMap = this._styleMap as any;
|
|
1037
|
+
if (!styleMap || !styleMap.justifySpaceMeasurements) {
|
|
1038
|
+
console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const spaceWidths = styleMap.justifySpaceMeasurements as number[][];
|
|
1043
|
+
console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
|
|
1044
|
+
|
|
1045
|
+
// Apply space widths to character bounds
|
|
1046
|
+
this._textLines.forEach((line, lineIndex) => {
|
|
1047
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
|
|
1048
|
+
|
|
1049
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
1050
|
+
const lineSpaceWidths = spaceWidths[lineIndex];
|
|
1051
|
+
let spaceIndex = 0;
|
|
1052
|
+
|
|
1053
|
+
for (let charIndex = 0; charIndex < line.length; charIndex++) {
|
|
1054
|
+
if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
|
|
1055
|
+
const expandedWidth = lineSpaceWidths[spaceIndex];
|
|
1056
|
+
if (lineBounds[charIndex]) {
|
|
1057
|
+
const oldWidth = lineBounds[charIndex].width;
|
|
1058
|
+
lineBounds[charIndex].width = expandedWidth;
|
|
1059
|
+
console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
|
|
1060
|
+
}
|
|
1061
|
+
spaceIndex++;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Fallback to default Fabric wrapping
|
|
1069
|
+
* @private
|
|
1070
|
+
*/
|
|
1071
|
+
_splitTextIntoLinesDefault(text: string) {
|
|
621
1072
|
const newText = super._splitTextIntoLines(text),
|
|
622
1073
|
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
623
1074
|
lines = new Array(graphemeLines.length);
|
|
@@ -649,6 +1100,206 @@ export class Textbox<
|
|
|
649
1100
|
}
|
|
650
1101
|
}
|
|
651
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Initialize event listeners for safety snap functionality
|
|
1105
|
+
* @private
|
|
1106
|
+
*/
|
|
1107
|
+
private initializeEventListeners(): void {
|
|
1108
|
+
// Track which side is being used for resize to handle position compensation
|
|
1109
|
+
let resizeOrigin: 'left' | 'right' | null = null;
|
|
1110
|
+
|
|
1111
|
+
// Detect resize origin during resizing
|
|
1112
|
+
this.on('resizing', (e: any) => {
|
|
1113
|
+
// Check transform origin to determine which side is being resized
|
|
1114
|
+
if (e.transform) {
|
|
1115
|
+
const { originX } = e.transform;
|
|
1116
|
+
// originX tells us which side is the anchor - opposite side is being dragged
|
|
1117
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
1118
|
+
} else if (e.originX) {
|
|
1119
|
+
const { originX } = e;
|
|
1120
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Only trigger safety snap after resize is complete (not during)
|
|
1125
|
+
// Use 'modified' event which fires after user releases the mouse
|
|
1126
|
+
this.on('modified', () => {
|
|
1127
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
1128
|
+
// Small delay to ensure text layout is updated
|
|
1129
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
1130
|
+
resizeOrigin = null; // Reset after capturing
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// Also listen to canvas-level modified event as backup
|
|
1134
|
+
this.canvas?.on('object:modified', (e) => {
|
|
1135
|
+
if (e.target === this) {
|
|
1136
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
1137
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
1138
|
+
resizeOrigin = null; // Reset after capturing
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Safety snap to prevent glyph clipping after manual resize.
|
|
1145
|
+
* Similar to Polotno - checks if any glyphs are too close to edges
|
|
1146
|
+
* and automatically expands width if needed.
|
|
1147
|
+
* @private
|
|
1148
|
+
* @param resizeOrigin - Which side was used for resizing ('left' or 'right')
|
|
1149
|
+
*/
|
|
1150
|
+
private safetySnapWidth(resizeOrigin?: 'left' | 'right' | null): void {
|
|
1151
|
+
// For Textbox objects, we always want to check for clipping regardless of isWrapping flag
|
|
1152
|
+
if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const lineCount = this._textLines.length;
|
|
1157
|
+
if (lineCount === 0) return;
|
|
1158
|
+
|
|
1159
|
+
// Check all lines, not just the last one
|
|
1160
|
+
let maxActualLineWidth = 0; // Actual measured width without buffers
|
|
1161
|
+
let maxRequiredWidth = 0; // Width including RTL buffer
|
|
1162
|
+
|
|
1163
|
+
for (let i = 0; i < lineCount; i++) {
|
|
1164
|
+
const lineText = this._textLines[i].join(''); // Convert grapheme array to string
|
|
1165
|
+
const lineWidth = this.getLineWidth(i);
|
|
1166
|
+
maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
|
|
1167
|
+
|
|
1168
|
+
// RTL detection - regex for Arabic, Hebrew, and other RTL characters
|
|
1169
|
+
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
1170
|
+
if (rtlRegex.test(lineText)) {
|
|
1171
|
+
// Add minimal RTL compensation buffer - just enough to prevent clipping
|
|
1172
|
+
const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
|
|
1173
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
|
|
1174
|
+
} else {
|
|
1175
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Safety margin - how close glyphs can get before we snap
|
|
1180
|
+
const safetyThreshold = 2; // px - very subtle trigger
|
|
1181
|
+
|
|
1182
|
+
if (maxRequiredWidth > this.width - safetyThreshold) {
|
|
1183
|
+
// Set width to exactly what's needed + minimal safety margin
|
|
1184
|
+
const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
|
|
1185
|
+
|
|
1186
|
+
// Store original position before width change
|
|
1187
|
+
const originalLeft = this.left;
|
|
1188
|
+
const originalTop = this.top;
|
|
1189
|
+
const widthIncrease = newWidth - this.width;
|
|
1190
|
+
|
|
1191
|
+
// Change width
|
|
1192
|
+
this.set('width', newWidth);
|
|
1193
|
+
|
|
1194
|
+
// Force text layout recalculation
|
|
1195
|
+
this.initDimensions();
|
|
1196
|
+
|
|
1197
|
+
// Only compensate position when resizing from left handle
|
|
1198
|
+
// Right handle resize doesn't shift the text position
|
|
1199
|
+
if (resizeOrigin === 'left') {
|
|
1200
|
+
// When resizing from left, the expansion pushes text right
|
|
1201
|
+
// Compensate by moving the textbox left by the width increase
|
|
1202
|
+
this.set({
|
|
1203
|
+
'left': originalLeft - widthIncrease,
|
|
1204
|
+
'top': originalTop
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
this.setCoords();
|
|
1209
|
+
|
|
1210
|
+
// Also refresh the overlay editor if it exists
|
|
1211
|
+
if ((this as any).__overlayEditor) {
|
|
1212
|
+
setTimeout(() => {
|
|
1213
|
+
(this as any).__overlayEditor.refresh();
|
|
1214
|
+
}, 0);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
this.canvas?.requestRenderAll();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Fix character selection mismatch after JSON loading for browser-wrapped fonts
|
|
1223
|
+
* @private
|
|
1224
|
+
*/
|
|
1225
|
+
_fixCharacterMappingAfterJsonLoad(): void {
|
|
1226
|
+
if ((this as any)._usingBrowserWrapping) {
|
|
1227
|
+
// Clear all cached states to force fresh text layout calculation
|
|
1228
|
+
(this as any)._browserWrapCache = null;
|
|
1229
|
+
(this as any)._lastDimensionState = null;
|
|
1230
|
+
|
|
1231
|
+
// Force complete re-initialization
|
|
1232
|
+
this.initDimensions();
|
|
1233
|
+
this._forceClearCache = true;
|
|
1234
|
+
|
|
1235
|
+
// Ensure canvas refresh
|
|
1236
|
+
this.setCoords();
|
|
1237
|
+
if (this.canvas) {
|
|
1238
|
+
this.canvas.requestRenderAll();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Force complete textbox re-initialization (useful after JSON loading)
|
|
1245
|
+
* Overrides Text version with Textbox-specific logic
|
|
1246
|
+
*/
|
|
1247
|
+
forceTextReinitialization(): void {
|
|
1248
|
+
console.log('🔄 Force reinitializing Textbox object');
|
|
1249
|
+
|
|
1250
|
+
// CRITICAL: Ensure textbox is marked as initialized
|
|
1251
|
+
this.initialized = true;
|
|
1252
|
+
|
|
1253
|
+
// Clear all caches and force dirty state
|
|
1254
|
+
this._clearCache();
|
|
1255
|
+
this.dirty = true;
|
|
1256
|
+
this.dynamicMinWidth = 0;
|
|
1257
|
+
|
|
1258
|
+
// Force isEditing false to ensure clean state
|
|
1259
|
+
this.isEditing = false;
|
|
1260
|
+
|
|
1261
|
+
console.log(' → Set initialized=true, dirty=true, cleared caches');
|
|
1262
|
+
|
|
1263
|
+
// Re-initialize dimensions (this will handle justify properly)
|
|
1264
|
+
this.initDimensions();
|
|
1265
|
+
|
|
1266
|
+
// Double-check that justify was applied by checking space widths
|
|
1267
|
+
if (this.textAlign.includes('justify') && this.__charBounds) {
|
|
1268
|
+
setTimeout(() => {
|
|
1269
|
+
// Verify justify was applied by checking if space widths vary
|
|
1270
|
+
let hasVariableSpaces = false;
|
|
1271
|
+
this.__charBounds.forEach((lineBounds, i) => {
|
|
1272
|
+
if (lineBounds && this._textLines && this._textLines[i]) {
|
|
1273
|
+
const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
|
|
1274
|
+
if (spaces.length > 1) {
|
|
1275
|
+
const firstSpaceWidth = spaces[0].width;
|
|
1276
|
+
hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
if (!hasVariableSpaces && this.__charBounds.length > 0) {
|
|
1282
|
+
console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
|
|
1283
|
+
if (this.enlargeSpaces) {
|
|
1284
|
+
this.enlargeSpaces();
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
console.log(' ✅ Justify spaces properly expanded');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Ensure height is recalculated - use browser height if available
|
|
1291
|
+
if ((this as any)._usingBrowserWrapping && (this as any)._actualBrowserHeight) {
|
|
1292
|
+
this.height = (this as any)._actualBrowserHeight;
|
|
1293
|
+
console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
|
|
1294
|
+
} else {
|
|
1295
|
+
this.height = this.calcTextHeight();
|
|
1296
|
+
console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
|
|
1297
|
+
}
|
|
1298
|
+
this.canvas?.requestRenderAll();
|
|
1299
|
+
}, 10);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
652
1303
|
/**
|
|
653
1304
|
* Returns object representation of an instance
|
|
654
1305
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|